From 21b0d00eb3b87a547c0f0b38e76b9c61d92ca8a8 Mon Sep 17 00:00:00 2001 From: "nikita.smirnov" Date: Fri, 3 Mar 2023 19:10:22 +0300 Subject: [PATCH] Init grpc-wiremock --- .image-build-args | 10 + COPYRIGHT | 13 + Dockerfile | 112 +++ LICENSE | 190 ++++ Makefile | 21 + NOTICE | 2 + README.md | 59 +- benchmarks/Dockerfile-benchmarks | 20 + benchmarks/Dockerfile-wiremock | 15 + benchmarks/Makefile | 17 + benchmarks/docker-compose.yaml | 39 + benchmarks/output/api_count_1/report_8000.txt | 42 + .../output/api_count_10/report_8000.txt | 42 + .../output/api_count_10/report_8001.txt | 42 + .../output/api_count_10/report_8002.txt | 42 + .../output/api_count_10/report_8003.txt | 42 + .../output/api_count_10/report_8004.txt | 42 + .../output/api_count_10/report_8005.txt | 42 + .../output/api_count_10/report_8006.txt | 42 + .../output/api_count_10/report_8007.txt | 42 + .../output/api_count_10/report_8008.txt | 42 + .../output/api_count_10/report_8009.txt | 42 + benchmarks/output/api_count_2/report_8000.txt | 42 + benchmarks/output/api_count_2/report_8001.txt | 42 + .../output/api_count_20/report_8000.txt | 42 + .../output/api_count_20/report_8001.txt | 42 + .../output/api_count_20/report_8002.txt | 42 + .../output/api_count_20/report_8003.txt | 42 + .../output/api_count_20/report_8004.txt | 42 + .../output/api_count_20/report_8005.txt | 42 + .../output/api_count_20/report_8006.txt | 42 + .../output/api_count_20/report_8007.txt | 42 + .../output/api_count_20/report_8008.txt | 42 + .../output/api_count_20/report_8009.txt | 42 + .../output/api_count_20/report_8010.txt | 42 + .../output/api_count_20/report_8011.txt | 42 + .../output/api_count_20/report_8012.txt | 42 + .../output/api_count_20/report_8013.txt | 42 + .../output/api_count_20/report_8014.txt | 42 + .../output/api_count_20/report_8015.txt | 42 + .../output/api_count_20/report_8016.txt | 42 + .../output/api_count_20/report_8017.txt | 42 + .../output/api_count_20/report_8018.txt | 42 + .../output/api_count_20/report_8019.txt | 42 + benchmarks/output/api_count_5/report_8000.txt | 42 + benchmarks/output/api_count_5/report_8001.txt | 42 + benchmarks/output/api_count_5/report_8002.txt | 42 + benchmarks/output/api_count_5/report_8003.txt | 42 + benchmarks/output/api_count_5/report_8004.txt | 42 + benchmarks/output/comparison.png | Bin 0 -> 108081 bytes benchmarks/output/report.json | 32 + benchmarks/scripts/benchmark.sh | 20 + benchmarks/scripts/entrypoint.sh | 7 + benchmarks/scripts/health.sh | 13 + benchmarks/scripts/report.sh | 18 + benchmarks/scripts/run.sh | 19 + .../wiremock/mappings/test_mock_200.json | 13 + cmd/certgen/Makefile | 11 + cmd/certgen/commands/certgen.go | 41 + cmd/certgen/commands/commands.go | 7 + cmd/certgen/main.go | 15 + cmd/confgen/Makefile | 11 + cmd/confgen/commands/commands.go | 7 + cmd/confgen/commands/confgen.go | 76 ++ cmd/confgen/main.go | 15 + cmd/grpc2http/Makefile | 11 + cmd/grpc2http/main.go | 49 + cmd/reload/Makefile | 11 + cmd/reload/commands/commands.go | 7 + cmd/reload/commands/reload.go | 31 + cmd/reload/main.go | 15 + cmd/watcher/Makefile | 11 + cmd/watcher/commands/commands.go | 7 + cmd/watcher/commands/watch.go | 69 ++ cmd/watcher/main.go | 15 + docs/benchmarks.md | 50 + docs/comparsion.md | 18 + docs/images/comparison.png | Bin 0 -> 108081 bytes docs/images/grpc-wiremock.png | Bin 0 -> 210884 bytes docs/mocks.md | 1 + docs/proxy.md | 253 +++++ etc/nginx/http.d/default.conf | 34 + etc/rsyslog.d/wiremock.conf | 9 + etc/supervisord/supervisord.conf | 27 + example/Makefile | 6 + example/cmd/main.go | 17 + example/go.mod | 22 + example/go.sum | 193 ++++ go.mod | 46 + go.sum | 564 +++++++++++ internal/usecases/certgen/generate.go | 37 + internal/usecases/confgen/generate.go | 246 +++++ internal/usecases/confgen/generate_test.go | 50 + .../usecases/confgen/mocks/generate_mock.go | 50 + internal/usecases/confgen/options.go | 65 ++ internal/usecases/grpc2http/generate.go | 173 ++++ internal/usecases/watcher/setup.go | 213 ++++ internal/usecases/watcher/setup_test.go | 32 + internal/usecases/watcher/watch.go | 52 + pkg/blacklist/blacklist.go | 45 + pkg/blacklist/blacklist_test.go | 29 + pkg/builder/update.go | 61 ++ pkg/builder/updaters/gopackage.go | 78 ++ pkg/builder/updaters/option.go | 168 ++++ pkg/compiler/command/command.go | 140 +++ pkg/compiler/command/command_test.go | 122 +++ pkg/compiler/compilecontract/contract.go | 7 + pkg/compiler/compiler.go | 95 ++ pkg/compiler/const.go | 8 + pkg/environment/environment.go | 113 +++ pkg/errgroup/errgroup.go | 33 + pkg/fstesting/comparator.go | 140 +++ pkg/fstesting/entry/entry.go | 13 + pkg/fstesting/entrymock/entry_mock.go | 53 + pkg/fstesting/setup.go | 42 + pkg/generators/certificates/cert.go | 74 ++ pkg/generators/certificates/cert_test.go | 58 ++ pkg/generators/certificates/domains.go | 62 ++ pkg/generators/certificates/generate.go | 201 ++++ pkg/generators/certificates/generate_test.go | 80 ++ pkg/generators/certificates/test/tester.go | 58 ++ pkg/generators/configs/generate.go | 61 ++ pkg/generators/configs/nginx/configure.go | 51 + pkg/generators/configs/nginx/reload.go | 25 + .../configs/supervisord/configure.go | 52 + pkg/generators/configs/supervisord/reload.go | 25 + pkg/generators/proxy/const.go | 14 + pkg/generators/proxy/packages.go | 56 ++ pkg/generators/proxy/project.go | 54 ++ pkg/generators/proxy/proxy.go | 78 ++ pkg/generators/proxy/stubs.go | 120 +++ pkg/generators/proxy/views.go | 136 +++ pkg/generators/proxy/views_test.go | 187 ++++ pkg/models/basecontract/contract.go | 93 ++ pkg/models/basecontract/from_openapi.go | 23 + pkg/models/basecontract/from_proto.go | 73 ++ pkg/models/basecontract/loader/load.go | 64 ++ .../basecontract/loader/openapi/openapi.go | 49 + pkg/models/basecontract/loader/proto/proto.go | 48 + .../basecontract/parser/openapi/openapi.go | 111 +++ pkg/models/basecontract/parser/proto/proto.go | 103 ++ pkg/models/basecontract/traverser/traverse.go | 32 + .../basecontract/unifier/openapi/openapi.go | 51 + .../basecontract/unifier/proto/proto.go | 140 +++ pkg/models/basecontract/unifier/unify.go | 59 ++ pkg/models/protocontract/contract.go | 104 ++ pkg/models/protocontract/contract_test.go | 48 + pkg/models/protocontract/from_proto.go | 143 +++ pkg/models/protocontract/loader/load.go | 63 ++ pkg/models/protocontract/loader/load_test.go | 116 +++ pkg/models/protocontract/loader/proto.go | 42 + pkg/models/protocontract/parser/proto.go | 93 ++ .../protocontract/traverser/traverse.go | 25 + .../protocontract/traverser/traverse_test.go | 56 ++ pkg/printer/print.go | 18 + pkg/renderer/funcs.go | 17 + pkg/renderer/renderer.go | 68 ++ pkg/renderer/resolver.go | 65 ++ pkg/renderer/resolver_test.go | 67 ++ pkg/runner/errors.go | 29 + pkg/runner/runner.go | 33 + pkg/runner/runner_test.go | 58 ++ pkg/sourcer/sourcer.go | 118 +++ pkg/sourcer/sourcer_test.go | 218 +++++ pkg/sourcer/types/types.go | 88 ++ pkg/svctesting/client/health.pb.go | 284 ++++++ pkg/svctesting/client/health_grpc.pb.go | 169 ++++ pkg/svctesting/svcrunner/runner.go | 67 ++ pkg/svctesting/testing.go | 79 ++ pkg/timespec/time_darwin.go | 7 + pkg/timespec/time_linux.go | 7 + pkg/utils/executils/fakecmd.go | 21 + pkg/utils/executils/look.go | 17 + pkg/utils/fsutils/compress.go | 85 ++ pkg/utils/fsutils/copydir.go | 104 ++ .../fsutils/fsutils-tests/fsutils_test.go | 278 ++++++ pkg/utils/fsutils/fsutils.go | 189 ++++ pkg/utils/httputils/request.go | 57 ++ pkg/utils/sliceutils/sliceutils.go | 21 + pkg/utils/sliceutils/sliceutils_test.go | 49 + pkg/utils/strutils/golang.go | 35 + pkg/utils/strutils/strutils.go | 76 ++ pkg/utils/strutils/strutils_test.go | 122 +++ pkg/utils/testutils/testing.go | 115 +++ pkg/utils/testutils/testing_test.go | 46 + pkg/watcher/notifier.go | 35 + pkg/watcher/types.go | 104 ++ pkg/watcher/watch.go | 200 ++++ pkg/watcher/watch_test.go | 175 ++++ pkg/wiremock/client/mocks.go | 51 + pkg/wiremock/client/wiremock.go | 40 + pkg/wiremock/config/config.go | 26 + pkg/wiremock/config/ports.go | 42 + pkg/wiremock/config/ports_test.go | 26 + pkg/wiremock/configopener/open.go | 87 ++ pkg/wiremock/configopener/open_test.go | 80 ++ pkg/wiremock/configsync/sync.go | 46 + pkg/wiremock/configsync/sync_test.go | 167 ++++ scripts/entrypoint.sh | 19 + scripts/init.sh | 62 ++ scripts/mocks/init.sh | 12 + scripts/multiapi/init.sh | 7 + scripts/other/check_dependencies.sh | 41 + scripts/other/log.sh | 11 + scripts/other/wait_for_it.sh | 182 ++++ scripts/proxy/init.sh | 5 + scripts/proxy/install.sh | 27 + scripts/proxy/run.sh | 5 + scripts/proxy/watch.sh | 13 + scripts/routing/init.sh | 14 + scripts/routing/logs.sh | 6 + scripts/run_wiremock.sh | 7 + static/health/health.proto | 23 + .../google/api/annotations.proto | 31 + .../proto-annotations/google/api/http.proto | 375 +++++++ .../proto-includes/google/protobuf/any.proto | 158 +++ .../proto-includes/google/protobuf/api.proto | 208 ++++ .../google/protobuf/compiler/plugin.proto | 183 ++++ .../google/protobuf/descriptor.proto | 911 ++++++++++++++++++ .../google/protobuf/duration.proto | 116 +++ .../google/protobuf/empty.proto | 52 + .../google/protobuf/field_mask.proto | 245 +++++ .../google/protobuf/source_context.proto | 48 + .../google/protobuf/struct.proto | 95 ++ .../google/protobuf/timestamp.proto | 147 +++ .../proto-includes/google/protobuf/type.proto | 187 ++++ .../google/protobuf/wrappers.proto | 123 +++ static/proxy-nginx/files/nginx.conf.tpl | 18 + static/proxy/files/main.go.tpl | 94 ++ .../method-bidirectional-streaming.go.tpl | 78 ++ .../files/method-client-streaming.go.tpl | 61 ++ .../files/method-server-streaming.go.tpl | 68 ++ static/proxy/files/method-unary.go.tpl | 43 + static/proxy/files/service.go.tpl | 19 + static/proxy/template/layout/Makefile | 10 + static/proxy/template/layout/cmd/.keep | 0 static/proxy/template/layout/go.mod.rename.me | 22 + static/proxy/template/layout/go.sum | 0 .../template/layout/internal/health/health.go | 33 + .../proxy/template/layout/pkg/getenv/const.go | 5 + .../template/layout/pkg/getenv/getenv.go | 17 + .../gitlab.sbmt.io/paas/health/health.pb.go | 284 ++++++ .../paas/health/health_grpc.pb.go | 169 ++++ .../template/layout/pkg/status/status.go | 49 + .../template/layout/pkg/wiremock/client.go | 102 ++ static/static.go | 14 + .../default-config-path/mocks/keep | 0 .../default-config-path/supervisord.conf | 16 + static/supervisord/files/supervisord.conf.tpl | 6 + static/tests-statuses.yml | 11 + .../tests/data/openapi/petstore/petstore.yaml | 29 + .../data/openapi/petstore/schemas/error.yml | 10 + .../data/openapi/petstore/schemas/pet.yml | 10 + static/tests/data/openapi/petstore/users.yaml | 47 + .../simple/api/grpc/example.proto | 16 + .../two-service/api/grpc/example.proto | 23 + .../api/grpc/example.proto | 20 + .../api/grpc/example.proto | 15 + .../api/grpc/fallthrough/fallthrough.proto | 7 + .../api/grpc/go/custom.proto | 7 + .../api/grpc/example.proto | 18 + .../api/grpc/local/custom.proto | 7 + .../with-local-imports/api/grpc/example.proto | 20 + .../api/grpc/import/custom.proto | 7 + .../api/grpc/another.proto | 5 + .../api/grpc/example.proto | 15 + .../api/grpc/import/another.proto | 5 + .../without-gopackage/api/grpc/example.proto | 14 + .../without-methods/api/grpc/example.proto | 11 + .../without-proto-files/api/grpc/example.txt | 11 + .../data/supervisord/empty/supervisord.conf | 6 + .../one-service/mocks/mock-awesome.conf | 3 + .../supervisord/one-service/supervisord.conf | 6 + .../data/supervisord/simple/supervisord.conf | 2 + .../two-services/mocks/mock-awesome.conf | 3 + .../two-services/mocks/mock-push-sender.conf | 3 + .../supervisord/two-services/supervisord.conf | 6 + .../with-includes/mocks/mock-awesome.conf | 3 + .../with-includes/supervisord.conf | 6 + .../data/wiremock/with-domains/awesome/keep | 0 .../wiremock/with-domains/push-sender/keep | 0 tests/.keep | 0 282 files changed, 17310 insertions(+), 1 deletion(-) create mode 100644 .image-build-args create mode 100644 COPYRIGHT create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 NOTICE create mode 100644 benchmarks/Dockerfile-benchmarks create mode 100644 benchmarks/Dockerfile-wiremock create mode 100644 benchmarks/Makefile create mode 100644 benchmarks/docker-compose.yaml create mode 100644 benchmarks/output/api_count_1/report_8000.txt create mode 100644 benchmarks/output/api_count_10/report_8000.txt create mode 100644 benchmarks/output/api_count_10/report_8001.txt create mode 100644 benchmarks/output/api_count_10/report_8002.txt create mode 100644 benchmarks/output/api_count_10/report_8003.txt create mode 100644 benchmarks/output/api_count_10/report_8004.txt create mode 100644 benchmarks/output/api_count_10/report_8005.txt create mode 100644 benchmarks/output/api_count_10/report_8006.txt create mode 100644 benchmarks/output/api_count_10/report_8007.txt create mode 100644 benchmarks/output/api_count_10/report_8008.txt create mode 100644 benchmarks/output/api_count_10/report_8009.txt create mode 100644 benchmarks/output/api_count_2/report_8000.txt create mode 100644 benchmarks/output/api_count_2/report_8001.txt create mode 100644 benchmarks/output/api_count_20/report_8000.txt create mode 100644 benchmarks/output/api_count_20/report_8001.txt create mode 100644 benchmarks/output/api_count_20/report_8002.txt create mode 100644 benchmarks/output/api_count_20/report_8003.txt create mode 100644 benchmarks/output/api_count_20/report_8004.txt create mode 100644 benchmarks/output/api_count_20/report_8005.txt create mode 100644 benchmarks/output/api_count_20/report_8006.txt create mode 100644 benchmarks/output/api_count_20/report_8007.txt create mode 100644 benchmarks/output/api_count_20/report_8008.txt create mode 100644 benchmarks/output/api_count_20/report_8009.txt create mode 100644 benchmarks/output/api_count_20/report_8010.txt create mode 100644 benchmarks/output/api_count_20/report_8011.txt create mode 100644 benchmarks/output/api_count_20/report_8012.txt create mode 100644 benchmarks/output/api_count_20/report_8013.txt create mode 100644 benchmarks/output/api_count_20/report_8014.txt create mode 100644 benchmarks/output/api_count_20/report_8015.txt create mode 100644 benchmarks/output/api_count_20/report_8016.txt create mode 100644 benchmarks/output/api_count_20/report_8017.txt create mode 100644 benchmarks/output/api_count_20/report_8018.txt create mode 100644 benchmarks/output/api_count_20/report_8019.txt create mode 100644 benchmarks/output/api_count_5/report_8000.txt create mode 100644 benchmarks/output/api_count_5/report_8001.txt create mode 100644 benchmarks/output/api_count_5/report_8002.txt create mode 100644 benchmarks/output/api_count_5/report_8003.txt create mode 100644 benchmarks/output/api_count_5/report_8004.txt create mode 100644 benchmarks/output/comparison.png create mode 100644 benchmarks/output/report.json create mode 100644 benchmarks/scripts/benchmark.sh create mode 100644 benchmarks/scripts/entrypoint.sh create mode 100644 benchmarks/scripts/health.sh create mode 100644 benchmarks/scripts/report.sh create mode 100644 benchmarks/scripts/run.sh create mode 100644 benchmarks/wiremock/mappings/test_mock_200.json create mode 100644 cmd/certgen/Makefile create mode 100644 cmd/certgen/commands/certgen.go create mode 100644 cmd/certgen/commands/commands.go create mode 100644 cmd/certgen/main.go create mode 100644 cmd/confgen/Makefile create mode 100644 cmd/confgen/commands/commands.go create mode 100644 cmd/confgen/commands/confgen.go create mode 100644 cmd/confgen/main.go create mode 100644 cmd/grpc2http/Makefile create mode 100644 cmd/grpc2http/main.go create mode 100644 cmd/reload/Makefile create mode 100644 cmd/reload/commands/commands.go create mode 100644 cmd/reload/commands/reload.go create mode 100644 cmd/reload/main.go create mode 100644 cmd/watcher/Makefile create mode 100644 cmd/watcher/commands/commands.go create mode 100644 cmd/watcher/commands/watch.go create mode 100644 cmd/watcher/main.go create mode 100644 docs/benchmarks.md create mode 100644 docs/comparsion.md create mode 100644 docs/images/comparison.png create mode 100644 docs/images/grpc-wiremock.png create mode 100644 docs/mocks.md create mode 100644 docs/proxy.md create mode 100644 etc/nginx/http.d/default.conf create mode 100644 etc/rsyslog.d/wiremock.conf create mode 100644 etc/supervisord/supervisord.conf create mode 100644 example/Makefile create mode 100644 example/cmd/main.go create mode 100644 example/go.mod create mode 100644 example/go.sum create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/usecases/certgen/generate.go create mode 100644 internal/usecases/confgen/generate.go create mode 100644 internal/usecases/confgen/generate_test.go create mode 100644 internal/usecases/confgen/mocks/generate_mock.go create mode 100644 internal/usecases/confgen/options.go create mode 100644 internal/usecases/grpc2http/generate.go create mode 100644 internal/usecases/watcher/setup.go create mode 100644 internal/usecases/watcher/setup_test.go create mode 100644 internal/usecases/watcher/watch.go create mode 100644 pkg/blacklist/blacklist.go create mode 100644 pkg/blacklist/blacklist_test.go create mode 100644 pkg/builder/update.go create mode 100644 pkg/builder/updaters/gopackage.go create mode 100644 pkg/builder/updaters/option.go create mode 100644 pkg/compiler/command/command.go create mode 100644 pkg/compiler/command/command_test.go create mode 100644 pkg/compiler/compilecontract/contract.go create mode 100644 pkg/compiler/compiler.go create mode 100644 pkg/compiler/const.go create mode 100644 pkg/environment/environment.go create mode 100644 pkg/errgroup/errgroup.go create mode 100644 pkg/fstesting/comparator.go create mode 100644 pkg/fstesting/entry/entry.go create mode 100644 pkg/fstesting/entrymock/entry_mock.go create mode 100644 pkg/fstesting/setup.go create mode 100644 pkg/generators/certificates/cert.go create mode 100644 pkg/generators/certificates/cert_test.go create mode 100644 pkg/generators/certificates/domains.go create mode 100644 pkg/generators/certificates/generate.go create mode 100644 pkg/generators/certificates/generate_test.go create mode 100644 pkg/generators/certificates/test/tester.go create mode 100644 pkg/generators/configs/generate.go create mode 100644 pkg/generators/configs/nginx/configure.go create mode 100644 pkg/generators/configs/nginx/reload.go create mode 100644 pkg/generators/configs/supervisord/configure.go create mode 100644 pkg/generators/configs/supervisord/reload.go create mode 100644 pkg/generators/proxy/const.go create mode 100644 pkg/generators/proxy/packages.go create mode 100644 pkg/generators/proxy/project.go create mode 100644 pkg/generators/proxy/proxy.go create mode 100644 pkg/generators/proxy/stubs.go create mode 100644 pkg/generators/proxy/views.go create mode 100644 pkg/generators/proxy/views_test.go create mode 100644 pkg/models/basecontract/contract.go create mode 100644 pkg/models/basecontract/from_openapi.go create mode 100644 pkg/models/basecontract/from_proto.go create mode 100644 pkg/models/basecontract/loader/load.go create mode 100644 pkg/models/basecontract/loader/openapi/openapi.go create mode 100644 pkg/models/basecontract/loader/proto/proto.go create mode 100644 pkg/models/basecontract/parser/openapi/openapi.go create mode 100644 pkg/models/basecontract/parser/proto/proto.go create mode 100644 pkg/models/basecontract/traverser/traverse.go create mode 100644 pkg/models/basecontract/unifier/openapi/openapi.go create mode 100644 pkg/models/basecontract/unifier/proto/proto.go create mode 100644 pkg/models/basecontract/unifier/unify.go create mode 100644 pkg/models/protocontract/contract.go create mode 100644 pkg/models/protocontract/contract_test.go create mode 100644 pkg/models/protocontract/from_proto.go create mode 100644 pkg/models/protocontract/loader/load.go create mode 100644 pkg/models/protocontract/loader/load_test.go create mode 100644 pkg/models/protocontract/loader/proto.go create mode 100644 pkg/models/protocontract/parser/proto.go create mode 100644 pkg/models/protocontract/traverser/traverse.go create mode 100644 pkg/models/protocontract/traverser/traverse_test.go create mode 100644 pkg/printer/print.go create mode 100644 pkg/renderer/funcs.go create mode 100644 pkg/renderer/renderer.go create mode 100644 pkg/renderer/resolver.go create mode 100644 pkg/renderer/resolver_test.go create mode 100644 pkg/runner/errors.go create mode 100644 pkg/runner/runner.go create mode 100644 pkg/runner/runner_test.go create mode 100644 pkg/sourcer/sourcer.go create mode 100644 pkg/sourcer/sourcer_test.go create mode 100644 pkg/sourcer/types/types.go create mode 100644 pkg/svctesting/client/health.pb.go create mode 100644 pkg/svctesting/client/health_grpc.pb.go create mode 100644 pkg/svctesting/svcrunner/runner.go create mode 100644 pkg/svctesting/testing.go create mode 100644 pkg/timespec/time_darwin.go create mode 100644 pkg/timespec/time_linux.go create mode 100644 pkg/utils/executils/fakecmd.go create mode 100644 pkg/utils/executils/look.go create mode 100644 pkg/utils/fsutils/compress.go create mode 100644 pkg/utils/fsutils/copydir.go create mode 100644 pkg/utils/fsutils/fsutils-tests/fsutils_test.go create mode 100644 pkg/utils/fsutils/fsutils.go create mode 100644 pkg/utils/httputils/request.go create mode 100644 pkg/utils/sliceutils/sliceutils.go create mode 100644 pkg/utils/sliceutils/sliceutils_test.go create mode 100644 pkg/utils/strutils/golang.go create mode 100644 pkg/utils/strutils/strutils.go create mode 100644 pkg/utils/strutils/strutils_test.go create mode 100644 pkg/utils/testutils/testing.go create mode 100644 pkg/utils/testutils/testing_test.go create mode 100644 pkg/watcher/notifier.go create mode 100644 pkg/watcher/types.go create mode 100644 pkg/watcher/watch.go create mode 100644 pkg/watcher/watch_test.go create mode 100644 pkg/wiremock/client/mocks.go create mode 100644 pkg/wiremock/client/wiremock.go create mode 100644 pkg/wiremock/config/config.go create mode 100644 pkg/wiremock/config/ports.go create mode 100644 pkg/wiremock/config/ports_test.go create mode 100644 pkg/wiremock/configopener/open.go create mode 100644 pkg/wiremock/configopener/open_test.go create mode 100644 pkg/wiremock/configsync/sync.go create mode 100644 pkg/wiremock/configsync/sync_test.go create mode 100644 scripts/entrypoint.sh create mode 100644 scripts/init.sh create mode 100644 scripts/mocks/init.sh create mode 100644 scripts/multiapi/init.sh create mode 100644 scripts/other/check_dependencies.sh create mode 100644 scripts/other/log.sh create mode 100644 scripts/other/wait_for_it.sh create mode 100644 scripts/proxy/init.sh create mode 100644 scripts/proxy/install.sh create mode 100644 scripts/proxy/run.sh create mode 100644 scripts/proxy/watch.sh create mode 100644 scripts/routing/init.sh create mode 100644 scripts/routing/logs.sh create mode 100644 scripts/run_wiremock.sh create mode 100644 static/health/health.proto create mode 100644 static/proto-annotations/google/api/annotations.proto create mode 100644 static/proto-annotations/google/api/http.proto create mode 100644 static/proto-includes/google/protobuf/any.proto create mode 100644 static/proto-includes/google/protobuf/api.proto create mode 100644 static/proto-includes/google/protobuf/compiler/plugin.proto create mode 100644 static/proto-includes/google/protobuf/descriptor.proto create mode 100644 static/proto-includes/google/protobuf/duration.proto create mode 100644 static/proto-includes/google/protobuf/empty.proto create mode 100644 static/proto-includes/google/protobuf/field_mask.proto create mode 100644 static/proto-includes/google/protobuf/source_context.proto create mode 100644 static/proto-includes/google/protobuf/struct.proto create mode 100644 static/proto-includes/google/protobuf/timestamp.proto create mode 100644 static/proto-includes/google/protobuf/type.proto create mode 100644 static/proto-includes/google/protobuf/wrappers.proto create mode 100644 static/proxy-nginx/files/nginx.conf.tpl create mode 100644 static/proxy/files/main.go.tpl create mode 100644 static/proxy/files/method-bidirectional-streaming.go.tpl create mode 100644 static/proxy/files/method-client-streaming.go.tpl create mode 100644 static/proxy/files/method-server-streaming.go.tpl create mode 100644 static/proxy/files/method-unary.go.tpl create mode 100644 static/proxy/files/service.go.tpl create mode 100644 static/proxy/template/layout/Makefile create mode 100644 static/proxy/template/layout/cmd/.keep create mode 100644 static/proxy/template/layout/go.mod.rename.me create mode 100644 static/proxy/template/layout/go.sum create mode 100644 static/proxy/template/layout/internal/health/health.go create mode 100644 static/proxy/template/layout/pkg/getenv/const.go create mode 100644 static/proxy/template/layout/pkg/getenv/getenv.go create mode 100644 static/proxy/template/layout/pkg/gitlab.sbmt.io/paas/health/health.pb.go create mode 100644 static/proxy/template/layout/pkg/gitlab.sbmt.io/paas/health/health_grpc.pb.go create mode 100644 static/proxy/template/layout/pkg/status/status.go create mode 100644 static/proxy/template/layout/pkg/wiremock/client.go create mode 100644 static/static.go create mode 100644 static/supervisord/default-config-path/mocks/keep create mode 100644 static/supervisord/default-config-path/supervisord.conf create mode 100644 static/supervisord/files/supervisord.conf.tpl create mode 100644 static/tests-statuses.yml create mode 100644 static/tests/data/openapi/petstore/petstore.yaml create mode 100644 static/tests/data/openapi/petstore/schemas/error.yml create mode 100644 static/tests/data/openapi/petstore/schemas/pet.yml create mode 100644 static/tests/data/openapi/petstore/users.yaml create mode 100644 static/tests/data/static-examples/simple/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/two-service/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/with-common-imports/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/fallthrough/fallthrough.proto create mode 100644 static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/go/custom.proto create mode 100644 static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/local/custom.proto create mode 100644 static/tests/data/static-examples/with-local-imports/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/with-local-imports/api/grpc/import/custom.proto create mode 100644 static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/another.proto create mode 100644 static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/import/another.proto create mode 100644 static/tests/data/static-examples/without-gopackage/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/without-methods/api/grpc/example.proto create mode 100644 static/tests/data/static-examples/without-proto-files/api/grpc/example.txt create mode 100644 static/tests/data/supervisord/empty/supervisord.conf create mode 100644 static/tests/data/supervisord/one-service/mocks/mock-awesome.conf create mode 100644 static/tests/data/supervisord/one-service/supervisord.conf create mode 100644 static/tests/data/supervisord/simple/supervisord.conf create mode 100644 static/tests/data/supervisord/two-services/mocks/mock-awesome.conf create mode 100644 static/tests/data/supervisord/two-services/mocks/mock-push-sender.conf create mode 100644 static/tests/data/supervisord/two-services/supervisord.conf create mode 100644 static/tests/data/supervisord/with-includes/mocks/mock-awesome.conf create mode 100644 static/tests/data/supervisord/with-includes/supervisord.conf create mode 100644 static/tests/data/wiremock/with-domains/awesome/keep create mode 100644 static/tests/data/wiremock/with-domains/push-sender/keep create mode 100644 tests/.keep diff --git a/.image-build-args b/.image-build-args new file mode 100644 index 0000000..fe15f66 --- /dev/null +++ b/.image-build-args @@ -0,0 +1,10 @@ +GOLANG_IMAGE_REPO="registry.hub.docker.com" +GOLANG_IMAGE_NAME="library/golang" +GOLANG_IMAGE_TAG="1.20.3" + + +WIREMOCK_IMAGE_REPO="docker.io" +WIREMOCK_IMAGE_NAME="wiremock/wiremock" +WIREMOCK_IMAGE_TAG="2.32.0-alpine" + +# See COPYRIGHT file. diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..1942dcf --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,13 @@ +Copyright (C) 2011 Thomas Akehurst + +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. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a88eefe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,112 @@ +ARG GOLANG_IMAGE_REPO +ARG GOLANG_IMAGE_NAME +ARG GOLANG_IMAGE_TAG + +ARG WIREMOCK_IMAGE_TAG +ARG WIREMOCK_IMAGE_REPO +ARG WIREMOCK_IMAGE_NAME + +FROM ${GOLANG_IMAGE_REPO}/${GOLANG_IMAGE_NAME}:${GOLANG_IMAGE_TAG} as golang + +ENV CGO_ENABLED=0 +ENV GOBIN="/go/bin" + +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 \ + && go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 \ + && go install github.com/githubnemo/CompileDaemon@v1.2.1 \ + && go install github.com/google/gnostic/cmd/protoc-gen-openapi@v0.6.8 + +COPY . /code +RUN make install-cli -C /code + +FROM ${GOLANG_IMAGE_REPO}/${GOLANG_IMAGE_NAME}:${GOLANG_IMAGE_TAG} as gopath + +ENV CGO_ENABLED=0 +ENV GOPATH="/go" +ENV GOCACHE="/go/go-build" + +# Warm up go mod & build cache +COPY ./example /tmp/gocache +RUN make install -C /tmp/gocache && \ + rm -rf /tmp/gocache + +FROM ${WIREMOCK_IMAGE_REPO}/${WIREMOCK_IMAGE_NAME}:${WIREMOCK_IMAGE_TAG} + +# Install tools +RUN apk add --no-cache \ + sudo \ + iptables \ + rsyslog \ + jq \ + gettext \ + make \ + protobuf-dev \ + lsof \ + perl \ + curl \ + nginx \ + yq \ + tree \ + git + + +# Create the user +ARG USERNAME=mock +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +ARG HOME=/home/$USERNAME + +RUN addgroup -g $USER_GID $USERNAME && \ + adduser -u $USER_UID -G $USERNAME --disabled-password -h /home/$USERNAME $USERNAME && \ + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME && \ + chmod 0440 /etc/sudoers.d/$USERNAME + +# Go is required to generate a proxy +ENV CGO_ENABLED=0 +ENV GOROOT="/usr/local/go" +ENV GOPATH="/go" +ENV GOBIN="/go/bin" +ENV GOCACHE="/go/go-build" +ENV PATH="${GOBIN}:${GOROOT}/bin:/scripts:${PATH}" + +## Go root +COPY --from=golang ${GOROOT} ${GOROOT} + +# Go mod & build cache +COPY --chown=${USERNAME}:${USERNAME} --from=gopath ${GOPATH} ${GOPATH} +## Assuming that GOCACHE is included into GOPATH +#COPY --chown=${USERNAME}:${USERNAME} --from=gopath ${GOCACHE} ${GOCACHE} + +# Go installed binaries +COPY --chown=${USERNAME}:${USERNAME} --from=golang ${GOBIN} ${GOBIN} + +COPY --from=ochinchina/supervisord:latest /usr/local/bin/supervisord /usr/local/bin/supervisord + +# Proxy +ARG PROXY_DIR="/var/proxy" +RUN mkdir ${PROXY_DIR} && \ + chown -R "${USER_UID}:${USER_UID}" ${PROXY_DIR} + +# Logging +COPY ./etc/rsyslog.d/wiremock.conf /etc/rsyslog.d/wiremock.conf + +# Scripts +COPY scripts /scripts +RUN chmod -R +x /scripts + +# Nginx +ARG NGINX_DIR="/etc/nginx/http.d" +COPY etc/nginx/http.d ${NGINX_DIR} +RUN chown -R "${USER_UID}:${USER_UID}" ${NGINX_DIR} + +# Supervisord +ARG SUPERVISORD_DIR="/etc/supervisord" +COPY etc/supervisord ${SUPERVISORD_DIR} +RUN chown -R "${USER_UID}:${USER_UID}" ${SUPERVISORD_DIR} + + +USER ${USERNAME} +WORKDIR ${HOME} + + +ENTRYPOINT ["/scripts/init.sh"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab539c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Copyright 2023 LLC "Instamart Technologies" + + 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. + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dcbbcd1 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +SHELL=/bin/bash -euo pipefail + +.PHONY: check_environment +check_environment: + @$(shell pwd)/scripts/helpers/check_dependencies.sh + +.PHONY: coverage +coverage: + @go-acc $(shell go list ./... | grep -v -e static) && go tool cover -func=coverage.txt + +.PHONY: unit-tests +unit-tests: + @go test $(shell go list ./... | grep -v -e static) -count 1 -race + +.PHONY: install-cli +install-cli: + make install -C cmd/grpc2http + make install -C cmd/confgen + make install -C cmd/watcher + make install -C cmd/certgen + make install -C cmd/reload diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..1e8e163 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). \ No newline at end of file diff --git a/README.md b/README.md index 8dc8624..ce100e0 100644 --- a/README.md +++ b/README.md @@ -1 +1,58 @@ -# grpc-wiremock \ No newline at end of file +# grpc-wiremock + +[Wiremock](https://wiremock.org/docs) is a great way to test your connected services. +But it has one drawback. And that is support for **proto** contracts. + +**grpc-wiremock** is designed to solve this problem, +and also provide a handy *tool for generating mocks*. And their *automatic reloading*. + +[Here](docs/comparsion.md) you can compare the functionality with existing solutions. + +## Getting started + +### Quick start +Check out our [wearable](https://github.com/nktch1/wearable) repo +with an example service. + +There you will find how the service interacts with the mock and +a docker-compose file for quick startup. + +### Interface + +```bash +MOCKS_PATH="$(PWD)/test/wiremock" +CERTS_PATH="$(PWD)/certs" +CONTRACTS_PATH="$(PWD)/deps" +WIREMOCK_GUI_PORT=9000 + +# grpc-wiremock supports multiple APIs simultaneously. +# This means that the another APIs will go up +# on port 8001, 8002, etc. + +YOUR_MOCK_API=8000 + +docker run \ + -p ${WIREMOCK_GUI_PORT}:${WIREMOCK_GUI_PORT} \ + -p ${YOUR_MOCK_API}:${YOUR_MOCK_API} \ + -v ${MOCKS_PATH}:/home/mock \ + -v ${CERTS_PATH}:/etc/ssl/mock/share \ + -v ${CONTRACTS_PATH}:/proto \ + SberMarket-Tech/grpc-wiremock@latest +``` +## Overview + +In general, **grpc-wiremock** contains two main components. + +You can read more about each of them here: +- [mocks generator](docs/mocks.md) (**COMING SOON**); +- [grpc-to-http-proxy generator](docs/proxy.md). + +In the diagram you can see how your requests are distributed within the **grpc-wiremock** container. + +![grpc-wiremock](docs/images/grpc-wiremock.png) + +### Benchmarks +You can also read about performance with multiple mock APIs [here](docs/benchmarks.md). + +### License +**grpc-wiremock** is under the Apache License, Version 2.0. See the LICENSE file for details. \ No newline at end of file diff --git a/benchmarks/Dockerfile-benchmarks b/benchmarks/Dockerfile-benchmarks new file mode 100644 index 0000000..51178f9 --- /dev/null +++ b/benchmarks/Dockerfile-benchmarks @@ -0,0 +1,20 @@ +ARG BASE_IMAGE_TAG="1.19" +ARG BASE_IMAGE_REPO="registry.hub.docker.com/library" +ARG BASE_IMAGE_NAME="golang" + +ARG WIREMOCK_API_COUNT + +FROM ${BASE_IMAGE_REPO}/${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG} + +ENV WIREMOCK_API_COUNT=${WIREMOCK_API_COUNT} + +RUN apt-get update && apt-get install apache2-utils + +COPY scripts /benchmarks +COPY Makefile /benchmarks/Makefile + +WORKDIR /benchmarks + +#RUN go mod init test && go mod tidy + +ENTRYPOINT ["make", "benchmark"] diff --git a/benchmarks/Dockerfile-wiremock b/benchmarks/Dockerfile-wiremock new file mode 100644 index 0000000..0001e20 --- /dev/null +++ b/benchmarks/Dockerfile-wiremock @@ -0,0 +1,15 @@ +ARG WIREMOCK_IMAGE_TAG="2.32.0" +ARG WIREMOCK_IMAGE_REPO="wiremock" +ARG WIREMOCK_IMAGE_NAME="wiremock" + +ARG WIREMOCK_API_COUNT + +FROM ${WIREMOCK_IMAGE_REPO}/${WIREMOCK_IMAGE_NAME}:${WIREMOCK_IMAGE_TAG} + +# Scripts +COPY scripts /scripts +RUN chmod -R +x /scripts + +ENV WIREMOCK_API_COUNT=${WIREMOCK_API_COUNT} + +ENTRYPOINT ["/scripts/entrypoint.sh"] diff --git a/benchmarks/Makefile b/benchmarks/Makefile new file mode 100644 index 0000000..590e2f2 --- /dev/null +++ b/benchmarks/Makefile @@ -0,0 +1,17 @@ +OUTPUT=output + +.PHONY: clean +clean: + @rm -rf "${OUTPUT}" && mkdir "${OUTPUT}" + +.PHONY: up +up: + @WIREMOCK_API_COUNT=$(WIREMOCK_API_COUNT) && docker compose up --force-recreate --build + +.PHONY: down +down: + docker compose down + +.PHONY: benchmark +benchmark: + bash benchmark.sh diff --git a/benchmarks/docker-compose.yaml b/benchmarks/docker-compose.yaml new file mode 100644 index 0000000..073ffca --- /dev/null +++ b/benchmarks/docker-compose.yaml @@ -0,0 +1,39 @@ +services: + wiremock: + build: + context: . + dockerfile: Dockerfile-wiremock + + environment: + - WIREMOCK_API_COUNT=${WIREMOCK_API_COUNT} + + ports: + - "8000-8100:8000-8100" + + volumes: + - type: bind + source: wiremock + target: /home/wiremock + + healthcheck: + test: ["CMD-SHELL", "/scripts/health.sh || exit 1"] + interval: 1s + timeout: 10s + retries: 50 + + benchmarks: + build: + context: . + dockerfile: Dockerfile-benchmarks + + environment: + - WIREMOCK_API_COUNT=${WIREMOCK_API_COUNT} + + volumes: + - type: bind + source: output + target: /benchmarks/output + + depends_on: + wiremock: + condition: service_healthy diff --git a/benchmarks/output/api_count_1/report_8000.txt b/benchmarks/output/api_count_1/report_8000.txt new file mode 100644 index 0000000..18d3f78 --- /dev/null +++ b/benchmarks/output/api_count_1/report_8000.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8000 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.114 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 879.02 [#/sec] (mean) +Time per request: 11.376 [ms] (mean) +Time per request: 1.138 [ms] (mean, across all concurrent requests) +Transfer rate: 127.05 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 1 11 7.3 9 31 +Waiting: 1 10 7.2 8 31 +Total: 1 11 7.4 9 31 + +Percentage of the requests served within a certain time (ms) + 50% 9 + 66% 13 + 75% 15 + 80% 18 + 90% 23 + 95% 25 + 98% 27 + 99% 31 + 100% 31 (longest request) diff --git a/benchmarks/output/api_count_10/report_8000.txt b/benchmarks/output/api_count_10/report_8000.txt new file mode 100644 index 0000000..cc6ecfc --- /dev/null +++ b/benchmarks/output/api_count_10/report_8000.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8000 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.796 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 125.66 [#/sec] (mean) +Time per request: 79.582 [ms] (mean) +Time per request: 7.958 [ms] (mean, across all concurrent requests) +Transfer rate: 18.16 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.3 0 3 +Processing: 2 77 99.2 33 415 +Waiting: 2 75 98.5 30 415 +Total: 2 77 99.2 33 415 + +Percentage of the requests served within a certain time (ms) + 50% 33 + 66% 53 + 75% 108 + 80% 152 + 90% 242 + 95% 337 + 98% 371 + 99% 415 + 100% 415 (longest request) diff --git a/benchmarks/output/api_count_10/report_8001.txt b/benchmarks/output/api_count_10/report_8001.txt new file mode 100644 index 0000000..d04da07 --- /dev/null +++ b/benchmarks/output/api_count_10/report_8001.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8001 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.790 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 126.58 [#/sec] (mean) +Time per request: 79.000 [ms] (mean) +Time per request: 7.900 [ms] (mean, across all concurrent requests) +Transfer rate: 18.30 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 1 1.6 0 7 +Processing: 4 76 76.3 42 316 +Waiting: 4 73 75.7 37 316 +Total: 6 77 76.2 43 317 + +Percentage of the requests served within a certain time (ms) + 50% 43 + 66% 90 + 75% 113 + 80% 131 + 90% 199 + 95% 227 + 98% 297 + 99% 317 + 100% 317 (longest request) diff --git a/benchmarks/output/api_count_10/report_8002.txt b/benchmarks/output/api_count_10/report_8002.txt new file mode 100644 index 0000000..81efaf6 --- /dev/null +++ b/benchmarks/output/api_count_10/report_8002.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8002 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.808 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 123.69 [#/sec] (mean) +Time per request: 80.847 [ms] (mean) +Time per request: 8.085 [ms] (mean, across all concurrent requests) +Transfer rate: 17.88 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 4 77 73.5 56 335 +Waiting: 3 75 74.2 51 335 +Total: 4 77 73.5 56 336 + +Percentage of the requests served within a certain time (ms) + 50% 56 + 66% 74 + 75% 106 + 80% 117 + 90% 195 + 95% 278 + 98% 319 + 99% 336 + 100% 336 (longest request) diff --git a/benchmarks/output/api_count_10/report_8003.txt b/benchmarks/output/api_count_10/report_8003.txt new file mode 100644 index 0000000..8da15c5 --- /dev/null +++ b/benchmarks/output/api_count_10/report_8003.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8003 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.827 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 120.87 [#/sec] (mean) +Time per request: 82.730 [ms] (mean) +Time per request: 8.273 [ms] (mean, across all concurrent requests) +Transfer rate: 17.47 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 2 79 74.2 64 244 +Waiting: 2 74 69.9 60 243 +Total: 2 79 74.2 64 244 + +Percentage of the requests served within a certain time (ms) + 50% 64 + 66% 105 + 75% 137 + 80% 160 + 90% 196 + 95% 230 + 98% 241 + 99% 244 + 100% 244 (longest request) diff --git a/benchmarks/output/api_count_10/report_8004.txt b/benchmarks/output/api_count_10/report_8004.txt new file mode 100644 index 0000000..2550da8 --- /dev/null +++ b/benchmarks/output/api_count_10/report_8004.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8004 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.727 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 137.61 [#/sec] (mean) +Time per request: 72.667 [ms] (mean) +Time per request: 7.267 [ms] (mean, across all concurrent requests) +Transfer rate: 19.89 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 2 64 49.9 65 300 +Waiting: 2 63 49.9 65 299 +Total: 2 64 49.9 65 300 + +Percentage of the requests served within a certain time (ms) + 50% 65 + 66% 80 + 75% 89 + 80% 114 + 90% 130 + 95% 140 + 98% 166 + 99% 300 + 100% 300 (longest request) diff --git a/benchmarks/output/api_count_10/report_8005.txt b/benchmarks/output/api_count_10/report_8005.txt new file mode 100644 index 0000000..e73c8c8 --- /dev/null +++ b/benchmarks/output/api_count_10/report_8005.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8005 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.807 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 123.97 [#/sec] (mean) +Time per request: 80.663 [ms] (mean) +Time per request: 8.066 [ms] (mean, across all concurrent requests) +Transfer rate: 17.92 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 2 78 63.7 64 235 +Waiting: 2 75 62.5 64 229 +Total: 2 78 63.7 64 235 + +Percentage of the requests served within a certain time (ms) + 50% 64 + 66% 119 + 75% 129 + 80% 138 + 90% 168 + 95% 183 + 98% 220 + 99% 235 + 100% 235 (longest request) diff --git a/benchmarks/output/api_count_10/report_8006.txt b/benchmarks/output/api_count_10/report_8006.txt new file mode 100644 index 0000000..83bdad5 --- /dev/null +++ b/benchmarks/output/api_count_10/report_8006.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8006 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.757 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 132.16 [#/sec] (mean) +Time per request: 75.667 [ms] (mean) +Time per request: 7.567 [ms] (mean, across all concurrent requests) +Transfer rate: 19.10 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.2 0 1 +Processing: 2 72 55.5 69 209 +Waiting: 2 70 54.3 65 208 +Total: 2 73 55.5 69 209 + +Percentage of the requests served within a certain time (ms) + 50% 69 + 66% 87 + 75% 100 + 80% 116 + 90% 159 + 95% 194 + 98% 205 + 99% 209 + 100% 209 (longest request) diff --git a/benchmarks/output/api_count_10/report_8007.txt b/benchmarks/output/api_count_10/report_8007.txt new file mode 100644 index 0000000..8120c58 --- /dev/null +++ b/benchmarks/output/api_count_10/report_8007.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8007 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.732 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 136.66 [#/sec] (mean) +Time per request: 73.174 [ms] (mean) +Time per request: 7.317 [ms] (mean, across all concurrent requests) +Transfer rate: 19.75 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.2 0 1 +Processing: 3 66 50.7 54 304 +Waiting: 2 61 45.1 51 220 +Total: 3 66 50.7 55 304 + +Percentage of the requests served within a certain time (ms) + 50% 55 + 66% 84 + 75% 89 + 80% 98 + 90% 132 + 95% 159 + 98% 220 + 99% 304 + 100% 304 (longest request) diff --git a/benchmarks/output/api_count_10/report_8008.txt b/benchmarks/output/api_count_10/report_8008.txt new file mode 100644 index 0000000..d9f6093 --- /dev/null +++ b/benchmarks/output/api_count_10/report_8008.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8008 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.677 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 147.62 [#/sec] (mean) +Time per request: 67.741 [ms] (mean) +Time per request: 6.774 [ms] (mean, across all concurrent requests) +Transfer rate: 21.34 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 2 66 58.0 47 233 +Waiting: 2 63 57.5 43 233 +Total: 2 66 57.9 47 233 + +Percentage of the requests served within a certain time (ms) + 50% 47 + 66% 71 + 75% 112 + 80% 133 + 90% 150 + 95% 181 + 98% 218 + 99% 233 + 100% 233 (longest request) diff --git a/benchmarks/output/api_count_10/report_8009.txt b/benchmarks/output/api_count_10/report_8009.txt new file mode 100644 index 0000000..d9dc46e --- /dev/null +++ b/benchmarks/output/api_count_10/report_8009.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8009 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.604 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 165.69 [#/sec] (mean) +Time per request: 60.354 [ms] (mean) +Time per request: 6.035 [ms] (mean, across all concurrent requests) +Transfer rate: 23.95 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 3 54 38.0 52 160 +Waiting: 3 53 38.0 50 160 +Total: 3 54 38.0 52 161 + +Percentage of the requests served within a certain time (ms) + 50% 52 + 66% 74 + 75% 86 + 80% 89 + 90% 105 + 95% 123 + 98% 143 + 99% 161 + 100% 161 (longest request) diff --git a/benchmarks/output/api_count_2/report_8000.txt b/benchmarks/output/api_count_2/report_8000.txt new file mode 100644 index 0000000..320e701 --- /dev/null +++ b/benchmarks/output/api_count_2/report_8000.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8000 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.194 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 514.88 [#/sec] (mean) +Time per request: 19.422 [ms] (mean) +Time per request: 1.942 [ms] (mean, across all concurrent requests) +Transfer rate: 74.42 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.2 0 1 +Processing: 3 18 11.8 16 65 +Waiting: 3 17 11.2 14 47 +Total: 3 18 11.9 16 65 + +Percentage of the requests served within a certain time (ms) + 50% 16 + 66% 23 + 75% 26 + 80% 29 + 90% 34 + 95% 43 + 98% 47 + 99% 65 + 100% 65 (longest request) diff --git a/benchmarks/output/api_count_2/report_8001.txt b/benchmarks/output/api_count_2/report_8001.txt new file mode 100644 index 0000000..66904f1 --- /dev/null +++ b/benchmarks/output/api_count_2/report_8001.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8001 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.175 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 571.37 [#/sec] (mean) +Time per request: 17.502 [ms] (mean) +Time per request: 1.750 [ms] (mean, across all concurrent requests) +Transfer rate: 82.58 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.1 0 1 +Processing: 2 16 12.5 13 59 +Waiting: 2 15 12.4 11 58 +Total: 2 16 12.6 13 59 + +Percentage of the requests served within a certain time (ms) + 50% 13 + 66% 19 + 75% 22 + 80% 25 + 90% 37 + 95% 42 + 98% 58 + 99% 59 + 100% 59 (longest request) diff --git a/benchmarks/output/api_count_20/report_8000.txt b/benchmarks/output/api_count_20/report_8000.txt new file mode 100644 index 0000000..56bd3b7 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8000.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8000 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.527 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 65.47 [#/sec] (mean) +Time per request: 152.749 [ms] (mean) +Time per request: 15.275 [ms] (mean, across all concurrent requests) +Transfer rate: 9.46 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.9 0 9 +Processing: 3 139 140.1 100 638 +Waiting: 3 134 139.9 98 637 +Total: 4 139 140.1 100 638 + +Percentage of the requests served within a certain time (ms) + 50% 100 + 66% 160 + 75% 184 + 80% 250 + 90% 401 + 95% 447 + 98% 498 + 99% 638 + 100% 638 (longest request) diff --git a/benchmarks/output/api_count_20/report_8001.txt b/benchmarks/output/api_count_20/report_8001.txt new file mode 100644 index 0000000..e58210a --- /dev/null +++ b/benchmarks/output/api_count_20/report_8001.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8001 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.741 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 57.44 [#/sec] (mean) +Time per request: 174.108 [ms] (mean) +Time per request: 17.411 [ms] (mean, across all concurrent requests) +Transfer rate: 8.30 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.4 0 3 +Processing: 2 162 176.7 87 698 +Waiting: 2 156 172.7 87 698 +Total: 3 162 176.8 87 698 + +Percentage of the requests served within a certain time (ms) + 50% 87 + 66% 167 + 75% 296 + 80% 351 + 90% 446 + 95% 549 + 98% 648 + 99% 698 + 100% 698 (longest request) diff --git a/benchmarks/output/api_count_20/report_8002.txt b/benchmarks/output/api_count_20/report_8002.txt new file mode 100644 index 0000000..0d9b7c5 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8002.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8002 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.652 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 60.54 [#/sec] (mean) +Time per request: 165.175 [ms] (mean) +Time per request: 16.517 [ms] (mean, across all concurrent requests) +Transfer rate: 8.75 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.4 0 3 +Processing: 2 162 189.6 72 674 +Waiting: 2 148 176.8 55 646 +Total: 3 163 189.6 72 674 + +Percentage of the requests served within a certain time (ms) + 50% 72 + 66% 178 + 75% 219 + 80% 329 + 90% 557 + 95% 602 + 98% 654 + 99% 674 + 100% 674 (longest request) diff --git a/benchmarks/output/api_count_20/report_8003.txt b/benchmarks/output/api_count_20/report_8003.txt new file mode 100644 index 0000000..ff06d13 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8003.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8003 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.512 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 66.15 [#/sec] (mean) +Time per request: 151.164 [ms] (mean) +Time per request: 15.116 [ms] (mean, across all concurrent requests) +Transfer rate: 9.56 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.9 0 9 +Processing: 3 132 106.1 144 375 +Waiting: 3 127 107.0 120 375 +Total: 3 133 106.1 144 375 + +Percentage of the requests served within a certain time (ms) + 50% 144 + 66% 167 + 75% 193 + 80% 256 + 90% 291 + 95% 322 + 98% 338 + 99% 375 + 100% 375 (longest request) diff --git a/benchmarks/output/api_count_20/report_8004.txt b/benchmarks/output/api_count_20/report_8004.txt new file mode 100644 index 0000000..fddbb42 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8004.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8004 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.222 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 81.82 [#/sec] (mean) +Time per request: 122.219 [ms] (mean) +Time per request: 12.222 [ms] (mean, across all concurrent requests) +Transfer rate: 11.83 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.9 0 9 +Processing: 3 109 129.1 72 594 +Waiting: 3 103 126.3 67 594 +Total: 3 109 129.1 72 594 + +Percentage of the requests served within a certain time (ms) + 50% 72 + 66% 106 + 75% 146 + 80% 179 + 90% 356 + 95% 425 + 98% 459 + 99% 594 + 100% 594 (longest request) diff --git a/benchmarks/output/api_count_20/report_8005.txt b/benchmarks/output/api_count_20/report_8005.txt new file mode 100644 index 0000000..fb3ebf6 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8005.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8005 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.855 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 53.90 [#/sec] (mean) +Time per request: 185.522 [ms] (mean) +Time per request: 18.552 [ms] (mean, across all concurrent requests) +Transfer rate: 7.79 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.8 0 4 +Processing: 2 181 289.7 39 1149 +Waiting: 2 180 289.4 39 1149 +Total: 2 181 289.7 39 1149 + +Percentage of the requests served within a certain time (ms) + 50% 39 + 66% 158 + 75% 212 + 80% 241 + 90% 886 + 95% 1003 + 98% 1046 + 99% 1149 + 100% 1149 (longest request) diff --git a/benchmarks/output/api_count_20/report_8006.txt b/benchmarks/output/api_count_20/report_8006.txt new file mode 100644 index 0000000..0d2b56f --- /dev/null +++ b/benchmarks/output/api_count_20/report_8006.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8006 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.835 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 119.81 [#/sec] (mean) +Time per request: 83.466 [ms] (mean) +Time per request: 8.347 [ms] (mean, across all concurrent requests) +Transfer rate: 17.32 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.4 0 2 +Processing: 4 75 68.5 60 388 +Waiting: 4 71 68.4 56 365 +Total: 4 75 68.4 60 388 + +Percentage of the requests served within a certain time (ms) + 50% 60 + 66% 74 + 75% 112 + 80% 118 + 90% 167 + 95% 219 + 98% 280 + 99% 388 + 100% 388 (longest request) diff --git a/benchmarks/output/api_count_20/report_8007.txt b/benchmarks/output/api_count_20/report_8007.txt new file mode 100644 index 0000000..470eda3 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8007.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8007 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.464 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 68.29 [#/sec] (mean) +Time per request: 146.436 [ms] (mean) +Time per request: 14.644 [ms] (mean, across all concurrent requests) +Transfer rate: 9.87 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.9 0 5 +Processing: 2 115 145.2 46 595 +Waiting: 2 107 137.1 40 572 +Total: 2 115 145.1 48 595 + +Percentage of the requests served within a certain time (ms) + 50% 48 + 66% 103 + 75% 175 + 80% 188 + 90% 325 + 95% 514 + 98% 572 + 99% 595 + 100% 595 (longest request) diff --git a/benchmarks/output/api_count_20/report_8008.txt b/benchmarks/output/api_count_20/report_8008.txt new file mode 100644 index 0000000..25bce6a --- /dev/null +++ b/benchmarks/output/api_count_20/report_8008.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8008 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.779 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 56.21 [#/sec] (mean) +Time per request: 177.905 [ms] (mean) +Time per request: 17.791 [ms] (mean, across all concurrent requests) +Transfer rate: 8.12 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 1.1 0 4 +Processing: 4 171 202.9 63 787 +Waiting: 2 158 191.6 56 787 +Total: 4 172 202.9 63 791 + +Percentage of the requests served within a certain time (ms) + 50% 63 + 66% 227 + 75% 276 + 80% 373 + 90% 488 + 95% 662 + 98% 770 + 99% 791 + 100% 791 (longest request) diff --git a/benchmarks/output/api_count_20/report_8009.txt b/benchmarks/output/api_count_20/report_8009.txt new file mode 100644 index 0000000..79874b4 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8009.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8009 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.563 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 63.99 [#/sec] (mean) +Time per request: 156.283 [ms] (mean) +Time per request: 15.628 [ms] (mean, across all concurrent requests) +Transfer rate: 9.25 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.4 0 2 +Processing: 3 145 149.2 98 666 +Waiting: 3 140 146.2 98 665 +Total: 4 145 149.3 98 667 + +Percentage of the requests served within a certain time (ms) + 50% 98 + 66% 152 + 75% 227 + 80% 268 + 90% 399 + 95% 458 + 98% 576 + 99% 667 + 100% 667 (longest request) diff --git a/benchmarks/output/api_count_20/report_8010.txt b/benchmarks/output/api_count_20/report_8010.txt new file mode 100644 index 0000000..9b2d862 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8010.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8010 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.316 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 76.01 [#/sec] (mean) +Time per request: 131.567 [ms] (mean) +Time per request: 13.157 [ms] (mean, across all concurrent requests) +Transfer rate: 10.99 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.3 0 2 +Processing: 2 112 103.4 89 354 +Waiting: 2 110 103.4 87 353 +Total: 2 112 103.5 90 355 + +Percentage of the requests served within a certain time (ms) + 50% 90 + 66% 115 + 75% 154 + 80% 192 + 90% 325 + 95% 339 + 98% 355 + 99% 355 + 100% 355 (longest request) diff --git a/benchmarks/output/api_count_20/report_8011.txt b/benchmarks/output/api_count_20/report_8011.txt new file mode 100644 index 0000000..1022dbc --- /dev/null +++ b/benchmarks/output/api_count_20/report_8011.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8011 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.740 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 57.47 [#/sec] (mean) +Time per request: 174.012 [ms] (mean) +Time per request: 17.401 [ms] (mean, across all concurrent requests) +Transfer rate: 8.31 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 1 2.5 0 9 +Processing: 2 161 162.0 114 649 +Waiting: 2 155 156.6 109 648 +Total: 2 162 163.2 115 658 + +Percentage of the requests served within a certain time (ms) + 50% 115 + 66% 166 + 75% 297 + 80% 316 + 90% 387 + 95% 503 + 98% 653 + 99% 658 + 100% 658 (longest request) diff --git a/benchmarks/output/api_count_20/report_8012.txt b/benchmarks/output/api_count_20/report_8012.txt new file mode 100644 index 0000000..6216a93 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8012.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8012 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.730 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 57.80 [#/sec] (mean) +Time per request: 173.012 [ms] (mean) +Time per request: 17.301 [ms] (mean, across all concurrent requests) +Transfer rate: 8.35 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.3 0 2 +Processing: 2 168 253.2 55 1046 +Waiting: 2 164 251.7 50 1045 +Total: 2 168 253.3 56 1046 + +Percentage of the requests served within a certain time (ms) + 50% 56 + 66% 139 + 75% 155 + 80% 253 + 90% 631 + 95% 856 + 98% 945 + 99% 1046 + 100% 1046 (longest request) diff --git a/benchmarks/output/api_count_20/report_8013.txt b/benchmarks/output/api_count_20/report_8013.txt new file mode 100644 index 0000000..de7ff3c --- /dev/null +++ b/benchmarks/output/api_count_20/report_8013.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8013 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.641 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 60.94 [#/sec] (mean) +Time per request: 164.097 [ms] (mean) +Time per request: 16.410 [ms] (mean, across all concurrent requests) +Transfer rate: 8.81 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.3 0 2 +Processing: 3 156 195.5 65 816 +Waiting: 3 149 191.5 61 816 +Total: 3 157 195.6 65 816 + +Percentage of the requests served within a certain time (ms) + 50% 65 + 66% 174 + 75% 254 + 80% 283 + 90% 392 + 95% 754 + 98% 774 + 99% 816 + 100% 816 (longest request) diff --git a/benchmarks/output/api_count_20/report_8014.txt b/benchmarks/output/api_count_20/report_8014.txt new file mode 100644 index 0000000..6c316c3 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8014.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8014 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.474 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 67.86 [#/sec] (mean) +Time per request: 147.352 [ms] (mean) +Time per request: 14.735 [ms] (mean, across all concurrent requests) +Transfer rate: 9.81 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.6 0 5 +Processing: 2 141 152.0 104 635 +Waiting: 2 136 151.6 98 635 +Total: 2 141 152.0 104 635 + +Percentage of the requests served within a certain time (ms) + 50% 104 + 66% 130 + 75% 185 + 80% 222 + 90% 384 + 95% 535 + 98% 628 + 99% 635 + 100% 635 (longest request) diff --git a/benchmarks/output/api_count_20/report_8015.txt b/benchmarks/output/api_count_20/report_8015.txt new file mode 100644 index 0000000..a7c8121 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8015.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8015 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.835 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 54.50 [#/sec] (mean) +Time per request: 183.484 [ms] (mean) +Time per request: 18.348 [ms] (mean, across all concurrent requests) +Transfer rate: 7.88 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.2 0 1 +Processing: 3 179 189.1 108 731 +Waiting: 3 177 189.2 108 731 +Total: 3 179 189.2 108 731 + +Percentage of the requests served within a certain time (ms) + 50% 108 + 66% 215 + 75% 334 + 80% 349 + 90% 474 + 95% 580 + 98% 625 + 99% 731 + 100% 731 (longest request) diff --git a/benchmarks/output/api_count_20/report_8016.txt b/benchmarks/output/api_count_20/report_8016.txt new file mode 100644 index 0000000..e818500 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8016.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8016 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.699 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 58.87 [#/sec] (mean) +Time per request: 169.875 [ms] (mean) +Time per request: 16.987 [ms] (mean, across all concurrent requests) +Transfer rate: 8.51 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.3 0 1 +Processing: 2 164 177.5 124 755 +Waiting: 2 161 178.3 121 754 +Total: 3 164 177.6 124 755 + +Percentage of the requests served within a certain time (ms) + 50% 124 + 66% 209 + 75% 259 + 80% 274 + 90% 423 + 95% 550 + 98% 693 + 99% 755 + 100% 755 (longest request) diff --git a/benchmarks/output/api_count_20/report_8017.txt b/benchmarks/output/api_count_20/report_8017.txt new file mode 100644 index 0000000..2302bd0 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8017.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8017 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.807 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 55.35 [#/sec] (mean) +Time per request: 180.655 [ms] (mean) +Time per request: 18.065 [ms] (mean, across all concurrent requests) +Transfer rate: 8.00 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.3 0 2 +Processing: 3 176 264.8 58 1111 +Waiting: 2 174 264.6 54 1111 +Total: 3 176 264.8 58 1111 + +Percentage of the requests served within a certain time (ms) + 50% 58 + 66% 115 + 75% 266 + 80% 340 + 90% 432 + 95% 815 + 98% 1110 + 99% 1111 + 100% 1111 (longest request) diff --git a/benchmarks/output/api_count_20/report_8018.txt b/benchmarks/output/api_count_20/report_8018.txt new file mode 100644 index 0000000..8c1f689 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8018.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8018 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.724 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 58.01 [#/sec] (mean) +Time per request: 172.397 [ms] (mean) +Time per request: 17.240 [ms] (mean, across all concurrent requests) +Transfer rate: 8.38 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.2 0 1 +Processing: 3 165 188.4 81 686 +Waiting: 2 160 187.9 76 685 +Total: 3 165 188.5 81 686 + +Percentage of the requests served within a certain time (ms) + 50% 81 + 66% 146 + 75% 175 + 80% 256 + 90% 539 + 95% 632 + 98% 635 + 99% 686 + 100% 686 (longest request) diff --git a/benchmarks/output/api_count_20/report_8019.txt b/benchmarks/output/api_count_20/report_8019.txt new file mode 100644 index 0000000..e5935e7 --- /dev/null +++ b/benchmarks/output/api_count_20/report_8019.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8019 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 1.179 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 84.78 [#/sec] (mean) +Time per request: 117.950 [ms] (mean) +Time per request: 11.795 [ms] (mean, across all concurrent requests) +Transfer rate: 12.25 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.3 0 1 +Processing: 3 108 91.4 92 405 +Waiting: 3 106 91.6 91 405 +Total: 4 109 91.4 92 405 + +Percentage of the requests served within a certain time (ms) + 50% 92 + 66% 137 + 75% 168 + 80% 200 + 90% 258 + 95% 275 + 98% 280 + 99% 405 + 100% 405 (longest request) diff --git a/benchmarks/output/api_count_5/report_8000.txt b/benchmarks/output/api_count_5/report_8000.txt new file mode 100644 index 0000000..e3dc291 --- /dev/null +++ b/benchmarks/output/api_count_5/report_8000.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8000 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.440 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 227.08 [#/sec] (mean) +Time per request: 44.037 [ms] (mean) +Time per request: 4.404 [ms] (mean, across all concurrent requests) +Transfer rate: 32.82 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.6 0 3 +Processing: 3 42 34.9 32 146 +Waiting: 2 39 34.6 28 146 +Total: 3 42 35.0 32 146 + +Percentage of the requests served within a certain time (ms) + 50% 32 + 66% 52 + 75% 70 + 80% 76 + 90% 87 + 95% 115 + 98% 127 + 99% 146 + 100% 146 (longest request) diff --git a/benchmarks/output/api_count_5/report_8001.txt b/benchmarks/output/api_count_5/report_8001.txt new file mode 100644 index 0000000..2a8fe34 --- /dev/null +++ b/benchmarks/output/api_count_5/report_8001.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8001 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.368 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 271.70 [#/sec] (mean) +Time per request: 36.805 [ms] (mean) +Time per request: 3.681 [ms] (mean, across all concurrent requests) +Transfer rate: 39.27 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.2 0 1 +Processing: 2 35 28.6 26 122 +Waiting: 2 34 28.0 26 120 +Total: 2 35 28.6 27 122 + +Percentage of the requests served within a certain time (ms) + 50% 27 + 66% 42 + 75% 47 + 80% 59 + 90% 84 + 95% 96 + 98% 110 + 99% 122 + 100% 122 (longest request) diff --git a/benchmarks/output/api_count_5/report_8002.txt b/benchmarks/output/api_count_5/report_8002.txt new file mode 100644 index 0000000..fd39415 --- /dev/null +++ b/benchmarks/output/api_count_5/report_8002.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8002 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.442 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 226.12 [#/sec] (mean) +Time per request: 44.223 [ms] (mean) +Time per request: 4.422 [ms] (mean, across all concurrent requests) +Transfer rate: 32.68 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.4 0 2 +Processing: 2 42 36.3 33 161 +Waiting: 2 40 35.4 31 160 +Total: 2 42 36.4 33 161 + +Percentage of the requests served within a certain time (ms) + 50% 33 + 66% 47 + 75% 66 + 80% 71 + 90% 97 + 95% 115 + 98% 142 + 99% 161 + 100% 161 (longest request) diff --git a/benchmarks/output/api_count_5/report_8003.txt b/benchmarks/output/api_count_5/report_8003.txt new file mode 100644 index 0000000..64a31b5 --- /dev/null +++ b/benchmarks/output/api_count_5/report_8003.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8003 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.456 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 219.07 [#/sec] (mean) +Time per request: 45.647 [ms] (mean) +Time per request: 4.565 [ms] (mean, across all concurrent requests) +Transfer rate: 31.66 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.2 0 1 +Processing: 3 44 34.9 40 140 +Waiting: 3 43 34.8 38 139 +Total: 3 44 35.0 40 140 + +Percentage of the requests served within a certain time (ms) + 50% 40 + 66% 57 + 75% 69 + 80% 75 + 90% 96 + 95% 111 + 98% 133 + 99% 140 + 100% 140 (longest request) diff --git a/benchmarks/output/api_count_5/report_8004.txt b/benchmarks/output/api_count_5/report_8004.txt new file mode 100644 index 0000000..efe33c8 --- /dev/null +++ b/benchmarks/output/api_count_5/report_8004.txt @@ -0,0 +1,42 @@ +This is ApacheBench, Version 2.3 <$Revision: 1903618 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking wiremock (be patient).....done + + +Server Software: +Server Hostname: wiremock +Server Port: 8004 + +Document Path: /HealthCheck +Document Length: 7 bytes + +Concurrency Level: 10 +Time taken for tests: 0.432 seconds +Complete requests: 100 +Failed requests: 0 +Total transferred: 14800 bytes +HTML transferred: 700 bytes +Requests per second: 231.49 [#/sec] (mean) +Time per request: 43.198 [ms] (mean) +Time per request: 4.320 [ms] (mean, across all concurrent requests) +Transfer rate: 33.46 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.2 0 1 +Processing: 4 41 26.8 43 126 +Waiting: 3 39 24.4 40 110 +Total: 4 41 26.8 43 126 + +Percentage of the requests served within a certain time (ms) + 50% 43 + 66% 48 + 75% 59 + 80% 62 + 90% 76 + 95% 94 + 98% 111 + 99% 126 + 100% 126 (longest request) diff --git a/benchmarks/output/comparison.png b/benchmarks/output/comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..e265ba994e0980a04fbc763215a5400f2e449e0a GIT binary patch literal 108081 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%0DF71&08|H$^JwPVe6!@R*&7K~Ccrz9^#v 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

azyA3+7~6uLXkd2z-o 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 Benchmark Wiremock API, port: ${PORT}" + + ab -c 10 -n 100 "http://wiremock:${PORT}/HealthCheck" > \ + "${OUT_DIR}/report_${PORT}.txt" 2>&1 & + + PORT=$((PORT+1)) +done + +sleep infinity \ No newline at end of file diff --git a/benchmarks/scripts/entrypoint.sh b/benchmarks/scripts/entrypoint.sh new file mode 100644 index 0000000..f8b0d1e --- /dev/null +++ b/benchmarks/scripts/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +WIREMOCK_API_COUNT="${WIREMOCK_API_COUNT-1}" + +bash /scripts/run.sh "${WIREMOCK_API_COUNT}" \ No newline at end of file diff --git a/benchmarks/scripts/health.sh b/benchmarks/scripts/health.sh new file mode 100644 index 0000000..bbf5aae --- /dev/null +++ b/benchmarks/scripts/health.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +DEFAULT_PORT=8000 +PORT=${DEFAULT_PORT} + +for i in $(seq 1 "${WIREMOCK_API_COUNT}"); do + echo "===> Check health, port: ${PORT}" + curl --fail http://localhost:${PORT}/HealthCheck || exit 1 + + PORT=$((PORT+1)) +done diff --git a/benchmarks/scripts/report.sh b/benchmarks/scripts/report.sh new file mode 100644 index 0000000..f69e198 --- /dev/null +++ b/benchmarks/scripts/report.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail + +PATH_TO_REPORTS="${1-output}" + +reports='[]' + +for path in $(find "${PATH_TO_REPORTS}" -type d -name "api_*"); do + api_count="$(echo "${path}" | grep -Eo '[0-9]+$')" + + report=$(echo $(find "${path}" -name '*.txt' -exec tail -1 {} \; | awk '{print $2}' | xargs) \ + | jq --arg api_count "${api_count}" -s "{ min_response_time_api_count_$api_count:min, max_response_time_api_count_$api_count:max, avg_response_time_api_count_$api_count: (add/length), median_response_time_api_count_$api_count: (sort|.[(length/2|floor)]) }") + + reports=$(echo "${reports}" | jq ". + [${report}]") +done + +echo "${reports}" | jq > "${PATH_TO_REPORTS}/report.json" diff --git a/benchmarks/scripts/run.sh b/benchmarks/scripts/run.sh new file mode 100644 index 0000000..6eee553 --- /dev/null +++ b/benchmarks/scripts/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euo pipefail + +WIREMOCK_API_COUNT="${1}" +DEFAULT_PORT=8000 +PORT=${DEFAULT_PORT} + +for i in $(seq 1 "${WIREMOCK_API_COUNT}"); do + echo "===> Run Wiremock API, port: ${PORT}" + + java \ + -cp "/var/wiremock/lib/*:/var/wiremock/extensions/*" \ + com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --port ${PORT} & + + PORT=$((PORT+1)) +done + +sleep infinity \ No newline at end of file diff --git a/benchmarks/wiremock/mappings/test_mock_200.json b/benchmarks/wiremock/mappings/test_mock_200.json new file mode 100644 index 0000000..574d7ec --- /dev/null +++ b/benchmarks/wiremock/mappings/test_mock_200.json @@ -0,0 +1,13 @@ +{ + "request" : { + "urlPath" : "/HealthCheck", + "method" : "GET" + }, + "response" : { + "status" : 200, + "body" : "success", + "headers" : { + "Content-Type" : "application/json" + } + } +} \ No newline at end of file diff --git a/cmd/certgen/Makefile b/cmd/certgen/Makefile new file mode 100644 index 0000000..851fe34 --- /dev/null +++ b/cmd/certgen/Makefile @@ -0,0 +1,11 @@ +CLI_NAME=certgen + +.PHONY: build +build: + @go build -o ${CLI_NAME} *.go + +.PHONY: install +install: + go mod tidy + go build -o out *.go + mv out ${GOBIN}/${CLI_NAME} diff --git a/cmd/certgen/commands/certgen.go b/cmd/certgen/commands/certgen.go new file mode 100644 index 0000000..6bddd62 --- /dev/null +++ b/cmd/certgen/commands/certgen.go @@ -0,0 +1,41 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/SberMarket-Tech/grpc-wiremock/internal/usecases/certgen" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" +) + +type CertgenArgs struct { + supervisordPath string + + output string +} + +func certgenCommand(subCommands ...*cobra.Command) *cobra.Command { + var args CertgenArgs + + command := &cobra.Command{ + Use: "certgen", + RunE: func(cmd *cobra.Command, _ []string) error { + generator := certgen.NewCertsGenWithDefaultFs(args.output, args.supervisordPath, os.Stdout) + + if err := generator.Generate(cmd.Context()); err != nil { + return fmt.Errorf("generate certs: %w", err) + } + + return nil + }, + } + + command.Flags().StringVar(&args.supervisordPath, "supervisord-path", environment.SupervisordConfigsDirPath, "Directory with Supervisord config") + command.Flags().StringVar(&args.output, "output", environment.DefaultCertificatesPath, "Directory with certificates") + + command.AddCommand(subCommands...) + + return command +} diff --git a/cmd/certgen/commands/commands.go b/cmd/certgen/commands/commands.go new file mode 100644 index 0000000..d260a30 --- /dev/null +++ b/cmd/certgen/commands/commands.go @@ -0,0 +1,7 @@ +package commands + +import "github.com/spf13/cobra" + +func CreateCommandRoot() *cobra.Command { + return certgenCommand() +} diff --git a/cmd/certgen/main.go b/cmd/certgen/main.go new file mode 100644 index 0000000..830500c --- /dev/null +++ b/cmd/certgen/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + + "github.com/SberMarket-Tech/grpc-wiremock/cmd/certgen/commands" +) + +func main() { + command := commands.CreateCommandRoot() + + if err := command.Execute(); err != nil { + log.Fatalln("execute cli command:", err.Error()) + } +} diff --git a/cmd/confgen/Makefile b/cmd/confgen/Makefile new file mode 100644 index 0000000..f24b244 --- /dev/null +++ b/cmd/confgen/Makefile @@ -0,0 +1,11 @@ +CLI_NAME=confgen + +.PHONY: build +build: + @go build -o ${CLI_NAME} *.go + +.PHONY: install +install: + go mod tidy + go build -o out *.go + mv out ${GOBIN}/${CLI_NAME} diff --git a/cmd/confgen/commands/commands.go b/cmd/confgen/commands/commands.go new file mode 100644 index 0000000..332f843 --- /dev/null +++ b/cmd/confgen/commands/commands.go @@ -0,0 +1,7 @@ +package commands + +import "github.com/spf13/cobra" + +func CreateCommandRoot() *cobra.Command { + return confgenCommand() +} diff --git a/cmd/confgen/commands/confgen.go b/cmd/confgen/commands/confgen.go new file mode 100644 index 0000000..7d87bf7 --- /dev/null +++ b/cmd/confgen/commands/confgen.go @@ -0,0 +1,76 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/utils/exec" + + "github.com/SberMarket-Tech/grpc-wiremock/internal/usecases/confgen" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/runner" +) + +type ConfgenArgs struct { + wiremockPath string + + nginxPath string + supervisordPath string +} + +func confgenCommand(subCommands ...*cobra.Command) *cobra.Command { + var args ConfgenArgs + + command := &cobra.Command{ + Use: "confgen", + RunE: func(cmd *cobra.Command, _ []string) error { + commandRunner := runner.New(exec.New()) + + generator := confgen.NewConfGenWithDefaultFs( + args.wiremockPath, args.supervisordPath, commandRunner, os.Stdout) + + options, err := createOptions(args) + if err != nil { + return fmt.Errorf("create options: %w", err) + } + + if err := generator.Generate(cmd.Context(), options...); err != nil { + return fmt.Errorf("generate configs: %w", err) + } + + return nil + }, + } + + command.Flags().StringVar(&args.nginxPath, "nginx", environment.NginxConfigsPath, "Directory with NGINX config") + command.Flags().StringVar(&args.wiremockPath, "wiremock-path", environment.DefaultWiremockConfigPath, "Directory with Wiremock config") + command.Flags().StringVar(&args.supervisordPath, "supervisord", environment.SupervisordConfigsDirPath, "Directory with Supervisord config") + + command.AddCommand(subCommands...) + + return command +} + +func createOptions(args ConfgenArgs) (confgen.Options, error) { + if args.nginxPath == "" && args.supervisordPath == "" { + return nil, fmt.Errorf("at least one argument must be set") + } + + var options confgen.Options + + if args.nginxPath != "" { + options = append(options, + confgen.WithNGINX(args.nginxPath), + ) + } + + if args.supervisordPath != "" { + options = append(options, + confgen.WithSupervisord(filepath.Join(args.supervisordPath, "mocks")), + ) + } + + return options, nil +} diff --git a/cmd/confgen/main.go b/cmd/confgen/main.go new file mode 100644 index 0000000..de196c8 --- /dev/null +++ b/cmd/confgen/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + + "github.com/SberMarket-Tech/grpc-wiremock/cmd/confgen/commands" +) + +func main() { + command := commands.CreateCommandRoot() + + if err := command.Execute(); err != nil { + log.Fatalln("execute cli command:", err.Error()) + } +} diff --git a/cmd/grpc2http/Makefile b/cmd/grpc2http/Makefile new file mode 100644 index 0000000..a8c2964 --- /dev/null +++ b/cmd/grpc2http/Makefile @@ -0,0 +1,11 @@ +CLI_NAME=grpc2http + +.PHONY: build +build: + @go build -o ${CLI_NAME} *.go + +.PHONY: install +install: + go mod tidy + go build -o out *.go + mv out ${GOBIN}/${CLI_NAME} diff --git a/cmd/grpc2http/main.go b/cmd/grpc2http/main.go new file mode 100644 index 0000000..b87dfdc --- /dev/null +++ b/cmd/grpc2http/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/cobra" + + "github.com/SberMarket-Tech/grpc-wiremock/internal/usecases/grpc2http" +) + +func main() { + command, err := buildGenerateCommand() + if err != nil { + log.Fatalln("create cli command:", err.Error()) + } + if err = command.Execute(); err != nil { + log.Fatalln("execute cli command:", err.Error()) + } +} + +type flags struct { + inputPath string + outputPath string + baseURL string +} + +func buildGenerateCommand() (*cobra.Command, error) { + var ( + args flags + rootCmd = &cobra.Command{ + RunE: func(cmd *cobra.Command, _ []string) error { + gen := grpc2http.NewProxyGen(args.inputPath, args.outputPath, args.baseURL, os.Stdout) + return gen.Generate(cmd.Context()) + }, + } + ) + + rootCmd.Flags().StringVar(&args.inputPath, "input", "", "Directory with contract files") + rootCmd.Flags().StringVarP(&args.outputPath, "output", "o", "generated_proxy", "Directory for generated proxy") + rootCmd.Flags().StringVar(&args.baseURL, "base-url", "http://localhost:8080", "Proxy URL") + + if err := rootCmd.MarkFlagRequired("input"); err != nil { + return nil, fmt.Errorf("make input flag persistent: %w", err) + } + + return rootCmd, nil +} diff --git a/cmd/reload/Makefile b/cmd/reload/Makefile new file mode 100644 index 0000000..82f99f6 --- /dev/null +++ b/cmd/reload/Makefile @@ -0,0 +1,11 @@ +CLI_NAME=reload + +.PHONY: build +build: + @go build -o ${CLI_NAME} *.go + +.PHONY: install +install: + go mod tidy + go build -o out *.go + mv out ${GOBIN}/${CLI_NAME} diff --git a/cmd/reload/commands/commands.go b/cmd/reload/commands/commands.go new file mode 100644 index 0000000..0637cf6 --- /dev/null +++ b/cmd/reload/commands/commands.go @@ -0,0 +1,7 @@ +package commands + +import "github.com/spf13/cobra" + +func CreateCommandRoot() *cobra.Command { + return reloadCommand() +} diff --git a/cmd/reload/commands/reload.go b/cmd/reload/commands/reload.go new file mode 100644 index 0000000..10cd2a7 --- /dev/null +++ b/cmd/reload/commands/reload.go @@ -0,0 +1,31 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" + + certgencmd "github.com/SberMarket-Tech/grpc-wiremock/cmd/certgen/commands" + confgencmd "github.com/SberMarket-Tech/grpc-wiremock/cmd/confgen/commands" +) + +func reloadCommand() *cobra.Command { + command := &cobra.Command{ + Use: "reload", + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + + if err := certgencmd.CreateCommandRoot().ExecuteContext(ctx); err != nil { + return fmt.Errorf("run certgen: %w", err) + } + + if err := confgencmd.CreateCommandRoot().ExecuteContext(ctx); err != nil { + return fmt.Errorf("run confgen: %w", err) + } + + return nil + }, + } + + return command +} diff --git a/cmd/reload/main.go b/cmd/reload/main.go new file mode 100644 index 0000000..ba78bdc --- /dev/null +++ b/cmd/reload/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + + "github.com/SberMarket-Tech/grpc-wiremock/cmd/reload/commands" +) + +func main() { + command := commands.CreateCommandRoot() + + if err := command.Execute(); err != nil { + log.Fatalln("execute cli command:", err.Error()) + } +} diff --git a/cmd/watcher/Makefile b/cmd/watcher/Makefile new file mode 100644 index 0000000..bf95894 --- /dev/null +++ b/cmd/watcher/Makefile @@ -0,0 +1,11 @@ +CLI_NAME=watcher + +.PHONY: build +build: + @go build -o ${CLI_NAME} *.go + +.PHONY: install +install: + go mod tidy + go build -o out *.go + mv out ${GOBIN}/${CLI_NAME} diff --git a/cmd/watcher/commands/commands.go b/cmd/watcher/commands/commands.go new file mode 100644 index 0000000..b6c7500 --- /dev/null +++ b/cmd/watcher/commands/commands.go @@ -0,0 +1,7 @@ +package commands + +import "github.com/spf13/cobra" + +func CreateCommandRoot() *cobra.Command { + return watchCommand() +} diff --git a/cmd/watcher/commands/watch.go b/cmd/watcher/commands/watch.go new file mode 100644 index 0000000..db55e43 --- /dev/null +++ b/cmd/watcher/commands/watch.go @@ -0,0 +1,69 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/SberMarket-Tech/grpc-wiremock/internal/usecases/watcher" +) + +type WatchArgs struct { + mocksPath string + + domainsPath string +} + +func watchCommand(subCommands ...*cobra.Command) *cobra.Command { + var args WatchArgs + + command := &cobra.Command{ + Use: "watch", + RunE: func(cmd *cobra.Command, _ []string) error { + requests, err := createInputs(args) + if err != nil { + return err + } + + runner := watcher.NewRunner(os.Stdout) + + if err = runner.Watch(cmd.Context(), requests...); err != nil { + return err + } + + return nil + }, + } + + command.Flags().StringVar(&args.mocksPath, "mocks", "", "Directory with mocks") + command.Flags().StringVar(&args.domainsPath, "domains", "", "Directory with domain directories") + + command.AddCommand(subCommands...) + + return command +} + +func createInputs(args WatchArgs) ([]watcher.WatchRequest, error) { + if len(args.mocksPath) == 0 && len(args.domainsPath) == 0 { + return nil, fmt.Errorf("watch command: at least one parameter must be set") + } + + var watchers []watcher.WatchRequest + + if len(args.mocksPath) > 0 { + watchers = append(watchers, watcher.WatchRequest{ + Path: args.mocksPath, + Name: watcher.MocksWatcher, + }) + } + + if len(args.domainsPath) > 0 { + watchers = append(watchers, watcher.WatchRequest{ + Path: args.domainsPath, + Name: watcher.DomainsWatcher, + }) + } + + return watchers, nil +} diff --git a/cmd/watcher/main.go b/cmd/watcher/main.go new file mode 100644 index 0000000..32ff7e1 --- /dev/null +++ b/cmd/watcher/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + + "github.com/SberMarket-Tech/grpc-wiremock/cmd/watcher/commands" +) + +func main() { + command := commands.CreateCommandRoot() + + if err := command.Execute(); err != nil { + log.Fatalln("execute cli command:", err.Error()) + } +} diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 0000000..84cab22 --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,50 @@ +## Benchmarks + +Suppose you need to deploy a lot of mock APIs for your service. + +Let's determine how many APIs you can deploy without any noticeable performance degradation. + +I tested on a MacBook Pro 16" with these characteristics: +- 2.6 GHz 6-Core Intel Core i7; +- 16 GB 2667 MHz DDR4; +- Intel UHD Graphics 630 1536 MB. + +For testing we will choose a simple mock, which we will request from each mock API. We will send requests using the ab utility. +An example of such a mock: + +```json +{ + "request" : { + "urlPath" : "/HealthCheck", + "method" : "GET" + }, + "response" : { + "status" : 200, + "body" : "success", + "headers" : { + "Content-Type" : "application/json" + } + } +} +``` + +Command to run tests: +```bash +ab -c 10 -n 100 "http://wiremock:${PORT}/HealthCheck" +``` + +Test parameters: +- the number of requests is 100; +- 10 requests can be executed simultaneously. + +Test Scenario: +- preparing N Wiremock Standalone processes; +- waiting for all processes to be ready to serve clients; +- running the ```ab``` utility for all processes simultaneously. + +### Results: + +In the graph you can see that with the addition of a Wiremock instance, the maximum response time doubles. + +![comparison](images/comparison.png) + diff --git a/docs/comparsion.md b/docs/comparsion.md new file mode 100644 index 0000000..34bfbdd --- /dev/null +++ b/docs/comparsion.md @@ -0,0 +1,18 @@ +## Comparison + +Here is a comparison of existing solutions for deploying mock servers, +as well as a comparison of existing Wiremock extensions with our +implementation. + +### Mock servers + +TODO Критерии сравнения сервисов + +### Existing extensions for Wiremock + +The most notable implementation of gRPC over Wiremock is this [one](https://github.com/Adven27/grpc-wiremock). + +| features | grpc-wiremock | grpc-wiremock (java) | +|---------|---------|----------------------| +| | | | + diff --git a/docs/images/comparison.png b/docs/images/comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..e265ba994e0980a04fbc763215a5400f2e449e0a GIT binary patch literal 108081 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%0DF71&08|H$^JwPVe6!@R*&7K~Ccrz9^#v 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

azyA3+7~6uLXkd2z-o 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 literal 0 HcmV?d00001 diff --git a/docs/mocks.md b/docs/mocks.md new file mode 100644 index 0000000..1206813 --- /dev/null +++ b/docs/mocks.md @@ -0,0 +1 @@ +## Mocks \ No newline at end of file diff --git a/docs/proxy.md b/docs/proxy.md new file mode 100644 index 0000000..4623967 --- /dev/null +++ b/docs/proxy.md @@ -0,0 +1,253 @@ +## Proxy generator + +Inside the container you can find the **grpc2http** CLI utility, +it allows to generate a proxy server (ready to run Golang project) +based on the provided **proto** contracts. + +To generate code from the proto contracts used: +- proto [compiler](https://github.com/protocolbuffers/protobuf); +- go-grpc and go plugins. + +### Install +```bash +make build -C "cmd/grpc2http" +``` + +### Interface + +Help: + +```bash +grpc2http -h +``` + +How to generate a proxy: + +```bash +grpc2http --input "/tmp/my-awesome-contracts-dir" --output "/tmp/ready-to-run-proxy" +``` + +How to run a proxy: + +```bash +make run -C "/tmp/ready-to-run-proxy" +``` + +## How it works? + +gGRP code and stubs are generated for each of your contracts. + +Within the stubs, the gRPC request is converted into HTTP, +then the HTTP request is sent to the Wiremock API, +and its response is converted back to gRPC. + +### Wiremock URL + +URL construction rule. Suppose that your gRPC server is called +```AwesomeService```. And the method is ```CallSomething```. +Base URL is ```http://localhost:8080```. +In this case, the URL would be as follows: + +``` +http://localhost:8080/AwesomeService/CallSomething +``` + +This URL must be present in mocks. + +### Examples + +Examples of stubs for each type of gRPC interactions: +- Unary call + ```Go + func (p *Service) Unary(ctx context.Context, in *example.Request) (*example.Response, error) { + const url = "http://localhost:8080/Example/Unary" + + requestBody, err := protojson.Marshal(in) + if err != nil { + return nil, status.Error(http.StatusBadGateway, fmt.Sprintf("create http request body: %v", err)) + } + + request, err := wiremock.DefaultRequest(ctx, url, bytes.NewReader(requestBody)) + if err != nil { + return nil, status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", err)) + } + + httpResponseBody, err := wiremock.DoRequestDefault(request) + if err != nil { + return nil, err + } + + var protoResponse example.Response + if err = protojson.Unmarshal(httpResponseBody, &protoResponse); err != nil { + return nil, status.Error(http.StatusBadGateway, fmt.Sprintf("marshal json object to proto: %v", err)) + } + + return &protoResponse, nil + } + ``` + +- Client-side streaming + ```Go + func (p *Service) ClientSideStream(stream example.Example_ClientSideStreamServer) error { + const url = "http://localhost:8080/Example/ClientSideStream" + + unmarshalAndSend := func(responseBody []byte) error { + var protoResponse example.Response + if processErr := protojson.Unmarshal(responseBody, &protoResponse); processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("marshal json object to proto: %v", processErr)) + } + if processErr := stream.SendAndClose(&protoResponse); processErr != nil { + return processErr + } + return nil + } + + defaultRequest, err := wiremock.DefaultRequest(stream.Context(), url, bytes.NewReader([]byte{})) + if err != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", err)) + } + + httpResponseBody, streamSize, err := wiremock.DoRequestWithStreamSize(defaultRequest) + if err != nil { + return err + } + + streamCursor := 1 + + for { + req, errReceive := stream.Recv() + if errReceive != nil && errReceive == io.EOF { + return unmarshalAndSend(httpResponseBody) + } + if errReceive != nil { + return errReceive + } + if streamCursor >= streamSize { + return unmarshalAndSend(httpResponseBody) + } + if req == nil { + continue + } + streamCursor++ + } + } + ``` + +- Server-side streaming + ```Go + func (p *Service) ServerSideStream(in *example.Request, stream example.Example_ServerSideStreamServer) error { + const url = "http://localhost:8080/Example/ServerSideStream" + const streamCursor = 1 + + ctx := stream.Context() + + unmarshalAndSend := func(responseBody []byte) error { + var protoResponse example.Response + if processErr := protojson.Unmarshal(responseBody, &protoResponse); processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("marshal json object to proto: %v", processErr)) + } + if processErr := stream.Send(&protoResponse); processErr != nil { + return processErr + } + return nil + } + + processStream := func(cursor int) error { + httpRequest, processErr := wiremock.RequestWithCursor(ctx, url, cursor, bytes.NewReader([]byte{})) + if processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", processErr)) + } + httpResponseBody, processErr := wiremock.DoRequestDefault(httpRequest) + if processErr != nil { + return processErr + } + return unmarshalAndSend(httpResponseBody) + } + + defaultRequest, err := wiremock.RequestWithCursor(ctx, url, streamCursor, bytes.NewReader([]byte{})) + if err != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", err)) + } + + httpResponseBody, streamSize, err := wiremock.DoRequestWithStreamSize(defaultRequest) + if err != nil { + return err + } + + if err = unmarshalAndSend(httpResponseBody); err != nil { + return err + } + + for cursor := streamCursor + 1; cursor <= streamSize; cursor++ { + if err = processStream(cursor); err != nil { + return err + } + } + + return nil + } + ``` + +- Bidirectional streaming + ```Go + func (p *Service) BidirectionalStream(stream example.Example_BidirectionalStreamServer) error { + const url = "http://localhost:8080/Example/BidirectionalStream" + + ctx := stream.Context() + + unmarshalAndSend := func(responseBody []byte) error { + var protoResponse example.Response + if processErr := protojson.Unmarshal(responseBody, &protoResponse); processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("marshal json object to proto: %v", processErr)) + } + if processErr := stream.Send(&protoResponse); processErr != nil { + return processErr + } + return nil + } + + processStream := func(cursor int) error { + httpRequest, processErr := wiremock.RequestWithCursor(ctx, url, cursor, bytes.NewReader([]byte{})) + if processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", processErr)) + } + httpResponseBody, processErr := wiremock.DoRequestDefault(httpRequest) + if processErr != nil { + return processErr + } + return unmarshalAndSend(httpResponseBody) + } + + streamCursor := 1 + + request, err := wiremock.RequestWithCursor(ctx, url, streamCursor, bytes.NewReader([]byte{})) + if err != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", err)) + } + + _, streamSize, err := wiremock.DoRequestWithStreamSize(request) + if err != nil { + return err + } + + for { + req, errReceive := stream.Recv() + if errReceive != nil && errReceive == io.EOF { + return nil + } + if errReceive != nil { + return errReceive + } + if req == nil { + continue + } + if err = processStream(streamCursor); err != nil { + return err + } + if streamCursor >= streamSize { + return nil + } + streamCursor++ + } + } + ``` diff --git a/etc/nginx/http.d/default.conf b/etc/nginx/http.d/default.conf new file mode 100644 index 0000000..368e360 --- /dev/null +++ b/etc/nginx/http.d/default.conf @@ -0,0 +1,34 @@ +# This is a default site configuration which will simply return 404 with explanation, +# preventing chance access to any other virtualhost. + +log_format not_mocked + 'warn: domain "$host" is not mocked' + ' - [$time_local] $remote_addr "$http_user_agent" "$request" '; + +server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl; + + ssl_certificate /etc/ssl/mock/mock.crt; + ssl_certificate_key /etc/ssl/mock/mock.key; + + return 404 'Domain "$host" is not mocked.\nFollow manual to mock it: `TODO`\n'; + + access_log /var/log/nginx/not_mocked.log not_mocked; +} + +server { + listen 3009 ssl http2; + + ssl_certificate /etc/ssl/mock/mock.crt; + ssl_certificate_key /etc/ssl/mock/mock.key; + + location / { + grpc_set_header Host $host; + grpc_set_header X-Real-IP $remote_addr; + grpc_pass grpc://localhost:3010; + } + + access_log /var/log/nginx/grpc_reverse_proxy.log main; +} diff --git a/etc/rsyslog.d/wiremock.conf b/etc/rsyslog.d/wiremock.conf new file mode 100644 index 0000000..f4997a5 --- /dev/null +++ b/etc/rsyslog.d/wiremock.conf @@ -0,0 +1,9 @@ +$RepeatedMsgReduction off +$FileCreateMode 0666 + +template(name="WireMockFileFormat" type="string" + string="%syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n") + +$EscapeControlCharactersOnReceive off + +if $syslogtag startswith 'gw.' then /var/log/wiremock;WireMockFileFormat diff --git a/etc/supervisord/supervisord.conf b/etc/supervisord/supervisord.conf new file mode 100644 index 0000000..d126a2c --- /dev/null +++ b/etc/supervisord/supervisord.conf @@ -0,0 +1,27 @@ +[include] +files = /etc/supervisord/mocks/*.conf + +[supervisord] +loglevel = error +logfile = /var/log/supervisord/supervisord.log +pidfile = /var/log/supervisord/supervisord_pid + +[inet_http_server] +port=0.0.0.0:9000 + +[supervisorctl] +serverurl=http://0.0.0.0:9000 + +# Turn off domains' watcher. + +# [program:watcher-domains] +# command = watcher --domains=/home/mock +# autorestart = true +# redirect_stderr = true +# stdout_logfile = /var/log/supervisord/watcher-domains.log, /dev/stdout + +[program:watcher-mocks] +command = watcher --mocks=/home/mock +autorestart = true +redirect_stderr = true +stdout_logfile = /var/log/supervisord/watcher-mocks.log, /dev/stdout diff --git a/example/Makefile b/example/Makefile new file mode 100644 index 0000000..e9c6d1f --- /dev/null +++ b/example/Makefile @@ -0,0 +1,6 @@ +PROXY_BINARY_NAME=grpc-to-http-proxy + +install: + go mod tidy + go build -o ${PROXY_BINARY_NAME} cmd/*.go + mv ${PROXY_BINARY_NAME} ${GOBIN}/${PROXY_BINARY_NAME} diff --git a/example/cmd/main.go b/example/cmd/main.go new file mode 100644 index 0000000..344470f --- /dev/null +++ b/example/cmd/main.go @@ -0,0 +1,17 @@ +package main + +import ( + _ "context" + _ "fmt" + _ "log" + _ "net" + + _ "github.com/grpc-ecosystem/go-grpc-middleware" + _ "go.uber.org/zap" + _ "google.golang.org/grpc" + _ "google.golang.org/protobuf/encoding/protojson" +) + +func main() { + // Do nothing +} diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 0000000..cdf970b --- /dev/null +++ b/example/go.mod @@ -0,0 +1,22 @@ +module grpc-proxy + +go 1.19 + +require ( + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 + go.uber.org/zap v1.23.0 + google.golang.org/grpc v1.49.0 + google.golang.org/protobuf v1.28.1 +) + +require ( + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.7 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect + golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de // indirect +) diff --git a/example/go.sum b/example/go.sum new file mode 100644 index 0000000..63c608c --- /dev/null +++ b/example/go.sum @@ -0,0 +1,193 @@ +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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +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/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/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.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-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +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/konsorten/go-windows-terminal-sequences v1.0.1/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/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +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/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/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +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/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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/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-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-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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +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.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20220403205710-6acee93ad0eb h1:PVGECzEo9Y3uOidtkHGdd347NjLtITfJFO9BxFpmRoo= +golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb/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/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +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 v0.0.0-20220401170504-314d38edb7de h1:9Ti5SG2U4cAcluryUo/sFay3TQKoxiFMfaT0pbizU7k= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +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.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +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.26.0/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 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +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/go.mod b/go.mod new file mode 100644 index 0000000..334740e --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module github.com/SberMarket-Tech/grpc-wiremock + +go 1.19 + +require ( + github.com/avast/retry-go/v4 v4.3.3 + github.com/farmergreg/rfsnotify v0.0.0-20200716145600-b37be6e4177f + github.com/getkin/kin-openapi v0.113.0 + github.com/golang/protobuf v1.5.2 + github.com/jhump/protoreflect v1.14.0 + github.com/ochinchina/supervisord/config v0.0.0-20221017034009-b1093f890648 + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/afero v1.9.3 + github.com/spf13/cobra v1.7.0 + github.com/stretchr/testify v1.8.1 + golang.org/x/sync v0.1.0 + golang.org/x/time v0.3.0 + google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 + google.golang.org/grpc v1.44.0 + google.golang.org/protobuf v1.27.1 + gopkg.in/fsnotify.v1 v1.4.7 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.19.5 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/yaml v0.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/ochinchina/go-ini v1.0.1 // indirect + github.com/ochinchina/supervisord/util v0.0.0-20221017034009-b1093f890648 // indirect + github.com/perimeterx/marshmallow v1.1.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0fe7b50 --- /dev/null +++ b/go.sum @@ -0,0 +1,564 @@ +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.44.3/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.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +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/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= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/avast/retry-go/v4 v4.3.3 h1:G56Bp6mU0b5HE1SkaoVjscZjlQb0oy4mezwY/cGH19w= +github.com/avast/retry-go/v4 v4.3.3/go.mod h1:rg6XFaiuFYII0Xu3RDbZQkxCofFwruZKW8oEF1jpWiU= +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/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-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/farmergreg/rfsnotify v0.0.0-20200716145600-b37be6e4177f h1:sRiluoTSaqFH+5cK4AsNX5ttFij+chr0Gj/HG/FkNGs= +github.com/farmergreg/rfsnotify v0.0.0-20200716145600-b37be6e4177f/go.mod h1:aRnKN8Geq81irQ1fxWr8sMkv9HY8GXn91Y82yKAfdD4= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/getkin/kin-openapi v0.113.0 h1:t9aNS/q5Agr7a55Jp1AuZ3sR2WzHESv3Dd2ys4UphsM= +github.com/getkin/kin-openapi v0.113.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= +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-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +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/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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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-20201218002935-b9804c9f04c2/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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI= +github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ= +github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E= +github.com/jhump/protoreflect v1.14.0 h1:MBbQK392K3u8NTLbKOCIi3XdI+y+c6yt5oMq0X3xviw= +github.com/jhump/protoreflect v1.14.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/ochinchina/go-ini v1.0.1 h1:qrKGrgxJjY+4H8aV7B2HPohShzHGrymW+/X1Gx933zU= +github.com/ochinchina/go-ini v1.0.1/go.mod h1:Tqs5+JmccLSNMX1KXbbyG/B3ro4J9uXVYC5U5VOeRE8= +github.com/ochinchina/supervisord/config v0.0.0-20221017034009-b1093f890648 h1:bJ3zfxA5ypIhrrO4f7noxNdTbMTfVuD86GEJsKm/DJc= +github.com/ochinchina/supervisord/config v0.0.0-20221017034009-b1093f890648/go.mod h1:jMN/SL0T6GCWWG/dD7Les9iPqjQ2OjEVyBWx8c9RJqI= +github.com/ochinchina/supervisord/util v0.0.0-20221017034009-b1093f890648 h1:y6+2kAXfIGr3jQEEVLNLmaL0+zHmFTAm30A/f5ZQwnA= +github.com/ochinchina/supervisord/util v0.0.0-20221017034009-b1093f890648/go.mod h1:V/yb0hfd2ax3Pzn83yoxBxww4HLJ5AXYH+rQBCieqcU= +github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= +github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +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= +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.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-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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/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/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/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-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-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +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/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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-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-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.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +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/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-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.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.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +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 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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 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.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= +k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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/internal/usecases/certgen/generate.go b/internal/usecases/certgen/generate.go new file mode 100644 index 0000000..a048f4b --- /dev/null +++ b/internal/usecases/certgen/generate.go @@ -0,0 +1,37 @@ +package certgen + +import ( + "context" + "fmt" + "io" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/certificates" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/configopener" +) + +type certsGen struct { + supervisordPath string + + output string + + logger io.Writer + + afero.Fs +} + +func NewCertsGenWithDefaultFs(output, supervisordPath string, logger io.Writer) certsGen { + return certsGen{output: output, supervisordPath: supervisordPath, logger: logger, Fs: afero.NewOsFs()} +} + +func (g *certsGen) Generate(ctx context.Context) error { + opener := configopener.New(g.Fs, g.supervisordPath) + generator := certificates.NewGenerator(g.Fs, opener) + + if err := generator.Generate(ctx, g.output); err != nil { + return fmt.Errorf("generate: %w", err) + } + + return nil +} diff --git a/internal/usecases/confgen/generate.go b/internal/usecases/confgen/generate.go new file mode 100644 index 0000000..99f0d7e --- /dev/null +++ b/internal/usecases/confgen/generate.go @@ -0,0 +1,246 @@ +package confgen + +import ( + "context" + "errors" + "fmt" + "io" + "log" + "os" + "syscall" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/configs" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/configs/nginx" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/configs/supervisord" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/renderer" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/configopener" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/configsync" + "github.com/SberMarket-Tech/grpc-wiremock/static" +) + +//go:generate mockery --exported --name=commandRunner --filename=generate_mock.go +type commandRunner interface { + Run(ctx context.Context, cmd string, args ...string) error +} + +type confGen struct { + WiremockPath string + + SupervisordPath string + + Logger io.Writer + + afero.Fs + commandRunner +} + +var configuerToTemplatePath = map[string]string{ + nginxOption: "proxy-nginx/files", + supervisordOption: "supervisord/files", +} + +var reloaderToCommand = map[string][]string{ + nginxOption: {"sudo", "nginx", "-s", "reload"}, + supervisordOption: {"supervisord", "ctl", "reload", "-c", environment.SupervisordMainConfigPath}, +} + +func NewConfGenWithDefaultFs(wiremockPath, supervisordPath string, commandRunner commandRunner, logger io.Writer) confGen { + return confGen{ + WiremockPath: wiremockPath, + SupervisordPath: supervisordPath, Logger: logger, + commandRunner: commandRunner, Fs: afero.NewOsFs(), + } +} + +func (g *confGen) Generate(ctx context.Context, options ...option) error { + if len(options) == 0 { + return fmt.Errorf("at least one option should be set") + } + + wiremockConfig, err := g.syncWiremockConfig() + if err != nil { + return fmt.Errorf("create wiremock config: %w", err) + } + + if err := g.preparePaths(options...); err != nil { + return fmt.Errorf("prepare paths: %w", err) + } + + configuers, err := g.createConfiguers(static.FromEmbed(), options...) + if err != nil { + return fmt.Errorf("create configures: %w", err) + } + + reloaders, err := g.createReloaders(options...) + if err != nil { + return fmt.Errorf("create configures: %w", err) + } + + runner := configs.NewRunner(g.Fs, wiremockConfig) + + if err = runner.RunConfiguers(configuers...); err != nil { + return fmt.Errorf("configure: %w", err) + } + + if err = runner.RunReloaders(ctx, reloaders...); err != nil { + return fmt.Errorf("reload: %w", err) + } + + return nil +} + +func (g *confGen) syncWiremockConfig() (config.Wiremock, error) { + wiremockConfig, err := configopener.New(g.Fs, g.SupervisordPath).Open() + if err != nil { + log.Printf("open wiremock config: %s", err) + return config.Wiremock{}, handleOpenerErrors(err) + } + + targetWiremockConfig, err := configsync.SyncWiremockConfig(g.Fs, wiremockConfig, g.WiremockPath) + if err != nil { + var e syscall.Errno + if !errors.As(err, &e) { + return config.Wiremock{}, fmt.Errorf("sync wiremock config: %w", err) + } + } + + return targetWiremockConfig, nil +} + +func (g *confGen) applyOptions(options ...option) error { + for _, option := range options { + if option.prepareFunctions == nil { + continue + } + + for _, prepF := range option.prepareFunctions { + if err := prepF(g.Fs, option.outputPath); err != nil { + return fmt.Errorf("prepare func: %w", err) + } + } + } + + return nil +} + +func (g *confGen) createConfiguer(staticFS afero.Fs, option option) (configs.Configuer, error) { + templatePath, exists := configuerToTemplatePath[option.name] + if !exists { + return nil, fmt.Errorf("template path doesn't exist: %s", option.name) + } + + configRenderer, err := renderer.New(staticFS, templatePath) + if err != nil { + return nil, fmt.Errorf("create renderer: %w", err) + } + + switch option.name { + case nginxOption: + return nginx.Configuer{ + Fs: g.Fs, + Renderer: configRenderer, + OutputPath: option.outputPath, + }, nil + case supervisordOption: + return supervisord.Configuer{ + Fs: g.Fs, + Renderer: configRenderer, + OutputPath: option.outputPath, + }, nil + } + + return nil, fmt.Errorf("unknown option name: %s", option.name) +} + +func (g *confGen) createReloader(option option) (configs.Reloader, error) { + command, exists := reloaderToCommand[option.name] + if !exists { + return nil, fmt.Errorf("command doesn't exist: %s", option.name) + } + + switch option.name { + case nginxOption: + return nginx.Reloader{ + Command: command, + Runner: g.commandRunner, + }, nil + case supervisordOption: + return supervisord.Reloader{ + Command: command, + Runner: g.commandRunner, + }, nil + } + + return nil, fmt.Errorf("unknown option name: %s", option.name) +} + +func (g *confGen) createConfiguers(staticFS afero.Fs, options ...option) ([]configs.Configuer, error) { + var configuers []configs.Configuer + + for _, opt := range options { + configuer, err := g.createConfiguer(staticFS, opt) + if err != nil { + return nil, fmt.Errorf("create configuer: %w", err) + } + + configuers = append(configuers, configuer) + } + + return configuers, nil +} + +func (g *confGen) createReloaders(options ...option) ([]configs.Reloader, error) { + var reloaders []configs.Reloader + + for _, opt := range options { + reloader, err := g.createReloader(opt) + if err != nil { + return nil, fmt.Errorf("create reloader: %w", err) + } + + reloaders = append(reloaders, reloader) + } + + return reloaders, nil +} + +func (g *confGen) preparePaths(options ...option) error { + configPaths := Options(options).Paths() + + if err := g.cleanConfigs(configPaths); err != nil { + return fmt.Errorf("clean configs: %w", err) + } + + if err := g.applyOptions(options...); err != nil { + return fmt.Errorf("apply options: %w", err) + } + + return nil +} + +func (g *confGen) cleanConfigs(paths []string) error { + for _, configPath := range paths { + if err := environment.CleanConfigs(g.Fs, configPath); err != nil { + if errors.Is(err, os.ErrNotExist) { + continue + } + + return fmt.Errorf("clean configs: %w", err) + } + } + + return nil +} + +func handleOpenerErrors(err error) error { + if errors.Is(err, os.ErrNotExist) || + errors.Is(err, config.EmptyWiremockConfigErr) { + return nil + } + + return fmt.Errorf("generate: %w", err) +} diff --git a/internal/usecases/confgen/generate_test.go b/internal/usecases/confgen/generate_test.go new file mode 100644 index 0000000..cbbd207 --- /dev/null +++ b/internal/usecases/confgen/generate_test.go @@ -0,0 +1,50 @@ +package confgen + +import ( + "context" + "io" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/SberMarket-Tech/grpc-wiremock/internal/usecases/confgen/mocks" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +var ( + osfs = afero.NewOsFs() + emptyCtx = mock.AnythingOfType("*context.emptyCtx") + projectPath = filepath.Join(fsutils.CurrentDir(), "../../..") + outputPath = filepath.Join(projectPath, "tests") +) + +func Test_confGen_Generate(t *testing.T) { + tests := []struct { + WiremockPath string + Logger io.Writer + Fs afero.Fs + name string + }{ + {Fs: osfs, WiremockPath: filepath.Join(projectPath, "static/tests/data/supervisord/empty-dir"), name: "empty-dir"}, + {Fs: osfs, WiremockPath: filepath.Join(projectPath, "static/tests/data/supervisord/simple"), name: "simple"}, + {Fs: osfs, WiremockPath: filepath.Join(projectPath, "static/tests/data/supervisord/with-includes"), name: "with-includes"}, + {Fs: osfs, WiremockPath: filepath.Join(projectPath, "static/tests/data/supervisord/two-services"), name: "two-services"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runner := mocks.NewCommandRunner(t) + runner.On("Run", emptyCtx, "supervisord", "ctl", "reload", "-c", environment.SupervisordMainConfigPath).Return(nil) + + outputPath := filepath.Join(outputPath, tt.name) + g := &confGen{WiremockPath: tt.WiremockPath, Logger: tt.Logger, Fs: tt.Fs, commandRunner: runner} + + err := g.Generate(context.Background(), WithSupervisord(outputPath, WithDefaultSupervisordConfig)) + require.NoError(t, err) + }) + } +} diff --git a/internal/usecases/confgen/mocks/generate_mock.go b/internal/usecases/confgen/mocks/generate_mock.go new file mode 100644 index 0000000..64da90b --- /dev/null +++ b/internal/usecases/confgen/mocks/generate_mock.go @@ -0,0 +1,50 @@ +// Code generated by mockery v2.23.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// CommandRunner is an autogenerated mock type for the commandRunner type +type CommandRunner struct { + mock.Mock +} + +// Run provides a mock function with given fields: ctx, cmd, args +func (_m *CommandRunner) Run(ctx context.Context, cmd string, args ...string) error { + _va := make([]interface{}, len(args)) + for _i := range args { + _va[_i] = args[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, cmd) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) error); ok { + r0 = rf(ctx, cmd, args...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewCommandRunner interface { + mock.TestingT + Cleanup(func()) +} + +// NewCommandRunner creates a new instance of CommandRunner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewCommandRunner(t mockConstructorTestingTNewCommandRunner) *CommandRunner { + mock := &CommandRunner{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/usecases/confgen/options.go b/internal/usecases/confgen/options.go new file mode 100644 index 0000000..55f3a46 --- /dev/null +++ b/internal/usecases/confgen/options.go @@ -0,0 +1,65 @@ +package confgen + +import ( + "fmt" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/static" +) + +const ( + nginxOption = "nginx" + supervisordOption = "supervisord" +) + +var staticFS = static.FromEmbed() + +type option struct { + name string + enable bool + outputPath string + + prepareFunctions []preparator +} + +type preparator func(afero.Fs, string) error + +func withOption(name, outputPath string) option { + return option{enable: true, name: name, outputPath: outputPath} +} + +func withOptionAndPreparation(name, outputPath string, prepareF ...preparator) option { + return option{enable: true, name: name, outputPath: outputPath, prepareFunctions: prepareF} +} + +func WithNGINX(outputPath string) option { + return withOption(nginxOption, outputPath) +} + +func WithSupervisord(outputPath string, prepareF ...preparator) option { + return withOptionAndPreparation(supervisordOption, outputPath, prepareF...) +} + +func WithDefaultSupervisordConfig(fs afero.Fs, path string) error { + const templatePath = "supervisord/default-config-path" + + if err := fsutils.CopyDir(staticFS, fs, templatePath, path, false); err != nil { + return fmt.Errorf("prepare default supervisord config: %w", err) + } + + return nil +} + +type Options []option + +func (o Options) Paths() []string { + var paths []string + + for _, option := range o { + paths = append(paths, option.outputPath) + } + + return paths +} diff --git a/internal/usecases/grpc2http/generate.go b/internal/usecases/grpc2http/generate.go new file mode 100644 index 0000000..b413a46 --- /dev/null +++ b/internal/usecases/grpc2http/generate.go @@ -0,0 +1,173 @@ +package grpc2http + +import ( + "context" + "fmt" + "io" + "path/filepath" + + "github.com/spf13/afero" + "k8s.io/utils/exec" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/builder" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/builder/updaters" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/compiler" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/proxy" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract/loader" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract/traverser" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/printer" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/renderer" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/runner" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/static" +) + +type GenerateProxyUsecase struct { + Path string + Output string + BaseURL string + + Logs io.Writer + + Fs afero.Fs +} + +func NewProxyGen(path, output, baseURL string, logs io.Writer) GenerateProxyUsecase { + return GenerateProxyUsecase{ + Path: path, + Output: output, + BaseURL: baseURL, + Fs: afero.NewOsFs(), + Logs: logs, + } +} + +func (p GenerateProxyUsecase) Generate(ctx context.Context) error { + if err := environment.CleanTmpDirs(p.Fs); err != nil { + return fmt.Errorf("clean tmp dirs: %w", err) + } + + if err := environment.DumpProtos(p.Fs); err != nil { + return fmt.Errorf("dump: %w", err) + } + + commandRunner := runner.New(exec.New()) + + contractsCompiler, err := compiler.New(p.Fs, commandRunner) + if err != nil { + return fmt.Errorf("create compiler: %w", err) + } + + const pathToTemplates = "proxy/files" + + projectRenderer, err := renderer.New(static.FromEmbed(), pathToTemplates) + if err != nil { + return fmt.Errorf("create renderer: %w", err) + } + + contracts, err := p.overwriteContracts() + if err != nil { + return fmt.Errorf("prepare contracts: %w", err) + } + + generator, err := proxy.NewGenerator( + p.Fs, contractsCompiler, + projectRenderer, p.BaseURL, p.Output, p.Logs, + ) + if err != nil { + return fmt.Errorf("create proxy generator: %w", err) + } + + if err = generator.Generate(ctx, contracts); err != nil { + return fmt.Errorf("generate: %w", err) + } + + if err = p.cleanUp(); err != nil { + return fmt.Errorf("clean up: %w", err) + } + + return nil +} + +func (p GenerateProxyUsecase) overwriteContracts() (protocontract.SetOfContracts, error) { + contractsSourcer, err := sourcer.New(p.Fs, p.Path, types.ProtoType) + if err != nil { + return nil, fmt.Errorf("create sourcer: %w", err) + } + + contracts, err := loader.Load(p.Fs, contractsSourcer) + if err != nil { + return nil, fmt.Errorf("load: %w", err) + } + + goPackageUpdater := updaters.NewGoPackageUpdater() + + descriptors := traverser.Descriptors(contracts) + + updatedDescriptors, err := builder.UpdateContracts(descriptors, goPackageUpdater) + if err != nil { + return nil, fmt.Errorf("overwrite: %w", err) + } + + path := environment.TmpOverwrittenContractsDir + + tmpDirForContract, err := afero.TempDir(p.Fs, path, "") + if err != nil { + return nil, fmt.Errorf("create tmp dir: %w", err) + } + + if err = printer.Print(updatedDescriptors, tmpDirForContract); err != nil { + return nil, fmt.Errorf("print: %w", err) + } + + contractsSourcer, err = sourcer.New(p.Fs, tmpDirForContract, types.ProtoType) + if err != nil { + return nil, fmt.Errorf("create sourcer for overwritten contracts: %w", err) + } + + contracts, err = loader.Load(p.Fs, contractsSourcer) + if err != nil { + return nil, fmt.Errorf("load overwritten contracts: %w", err) + } + + return contracts, nil +} + +func (p GenerateProxyUsecase) cleanUp() error { + if err := p.removeTmpFilesFromProxy(); err != nil { + return fmt.Errorf("remove .keep files: %w", err) + } + + if err := p.renameGoMod(); err != nil { + return fmt.Errorf("rename go.mod: %w", err) + } + + return nil +} + +func (p GenerateProxyUsecase) removeTmpFilesFromProxy() error { + const keepFile = ".keep" + return fsutils.RemoveWithSubdirs(p.Fs, p.Output, keepFile) +} + +func (p GenerateProxyUsecase) renameGoMod() error { + const ( + goModName = "go.mod" + tmpGoModName = "go.mod.rename.me" + ) + + var ( + oldPath = filepath.Join(p.Output, tmpGoModName) + newPath = filepath.Join(p.Output, goModName) + ) + + if err := p.Fs.Rename(oldPath, newPath); err != nil { + return fmt.Errorf("rename: %w", err) + } + + return nil +} diff --git a/internal/usecases/watcher/setup.go b/internal/usecases/watcher/setup.go new file mode 100644 index 0000000..5a5068d --- /dev/null +++ b/internal/usecases/watcher/setup.go @@ -0,0 +1,213 @@ +package watcher + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/spf13/afero" + "k8s.io/utils/exec" + + "github.com/SberMarket-Tech/grpc-wiremock/internal/usecases/certgen" + "github.com/SberMarket-Tech/grpc-wiremock/internal/usecases/confgen" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/runner" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/watcher" + wmclient "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/client" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/configopener" +) + +const ( + MocksWatcher = "mocks" + DomainsWatcher = "domains" +) + +const ( + filesDirRule = `.*/__files$` + mappingsDirRule = `.*/mappings$` + + mockFiles = `^.*\/(mappings|__files)\/.+[.]json$` +) + +type WatchRequest struct { + Name string + Path string +} + +var ( + osfs = afero.NewOsFs() + + opener = configopener.New(osfs, environment.SupervisordConfigsDirPath) + + wiremockClient = wmclient.NewDefaultClient(osfs, opener) +) + +var knownWatchers = map[string]watcher.WatcherDesc{ + MocksWatcher: { + Do: mocksWatcherAction, + + Name: MocksWatcher, + + Recursive: true, + + Behave: watcher.BehaviourDesc{ + Event: watcher.NewEventTypes(). + WithCreate().WithRemove().WithWrite(), + + Throttle: watcher.ThrottlingRules{ + DelayAfterEvent: time.Second * 3, + Interval: time.Millisecond * 500, + }, + + Retry: watcher.RetryRules{Attempts: 10}, + + Entry: watcher.NewEntryRules(). + WithNameRule(mockFiles). + WithNameRule(filesDirRule). + WithNameRule(mappingsDirRule), + }, + }, + + //Turn off domains' watcher. + + //DomainsWatcher: { + // Do: domainsWatcherAction, + // + // Name: DomainsWatcher, + // + // Recursive: false, + // + // Behave: watcher.BehaviourDesc{ + // Event: watcher.NewEventTypes(). + // WithCreate(). + // WithRemove(). + // WithWrite(). + // WithRename(), + // + // Retry: watcher.RetryRules{Attempts: 10}, + // + // Entry: watcher.NewEntryRules().WithNameRule(`.*`), + // }, + //}, +} + +func mocksWatcherAction(ctx context.Context, _ string, path string) error { + // only for '.json' files in '/home/mock/*' directory. + + domain, err := getDomainByPath(path) + if err != nil { + return nil + } + + log.Printf("update mocks for domain '%s'\n", domain) + + if err = wiremockClient.UpdateMocks(ctx, domain); err != nil { + return fmt.Errorf("update mocks for domain '%s': %w", domain, err) + } + + return nil +} + +func domainsWatcherAction(ctx context.Context, root string, path string) error { + /* + Wiremock directories: + /home/mock/ + domain1 + __files + mappings + domain2 + __files + mappings + + Supervisord directories: + /etc/supervisord/ + mocks/ + domain1-mock.conf + domain2-mock.conf + supervisord.conf + + NGINX directories: + /etc/nginx/http.d/ + domain1-mock.conf + domain2-mock.conf + default.conf + */ + + domain, err := getDomainByFolder(root, path) + if err != nil { + log.Printf("get domain by folder: %s\n", err.Error()) + return nil + } + + commandRunner := runner.New(exec.New()) + + configsGenerator := confgen.NewConfGenWithDefaultFs( + environment.DefaultWiremockConfigPath, + environment.SupervisordConfigsDirPath, + commandRunner, os.Stdout) + + opts := confgen.Options{ + confgen.WithNGINX(environment.NginxConfigsPath), + confgen.WithSupervisord(environment.SupervisordConfigsPath), + } + + if err = configsGenerator.Generate(ctx, opts...); err != nil { + return fmt.Errorf("generate configs: %w", err) + } + + log.Println("update supervisord & nginx config", domain) + + certGenerator := certgen.NewCertsGenWithDefaultFs( + environment.DefaultCertificatesPath, + environment.SupervisordConfigsDirPath, + os.Stdout, + ) + + log.Println("update certificates", domain) + + if err := certGenerator.Generate(ctx); err != nil { + return fmt.Errorf("generate certificates: %w", err) + } + + return nil +} + +var domainRE = regexp.MustCompile(`(.*/)?(.*)/(mappings|__files)`) + +func getDomainByPath(path string) (string, error) { + domainSubmatches := domainRE.FindAllStringSubmatch(path, 1) + + if len(domainSubmatches) == 0 || len(domainSubmatches[0]) < 3 { + return "", fmt.Errorf("get domain by path '%s'", path) + } + + domain := domainSubmatches[0][2] + + return domain, nil +} + +func getDomainByFolder(rootPath, eventPath string) (string, error) { + relativePath, err := filepath.Rel(rootPath, eventPath) + if err != nil { + return "", fmt.Errorf("relative path: %w", err) + } + + if !isFirstLevelFolder(relativePath) { + return "", fmt.Errorf("path is not correspond to first level folder: %s", relativePath) + } + + return filepath.Base(relativePath), nil +} + +func isFirstLevelFolder(path string) bool { + if len(filepath.Ext(path)) > 0 { + return false + } + + return len(strings.Split(path, string(os.PathSeparator))) == 1 +} diff --git a/internal/usecases/watcher/setup_test.go b/internal/usecases/watcher/setup_test.go new file mode 100644 index 0000000..e7fe2dd --- /dev/null +++ b/internal/usecases/watcher/setup_test.go @@ -0,0 +1,32 @@ +package watcher + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_getDomainByPath(t *testing.T) { + tests := []struct { + name string + path string + want string + }{ + {path: "/home/mock/awesome/mappings/mock.json", want: "awesome"}, + {path: "/home/mock/awesome-domain/mappings/mock.json", want: "awesome-domain"}, + {path: "/home/mock/awesome-domain/__files/mock.json", want: "awesome-domain"}, + {path: "/Users/test-user1/Desktop/awesome-project/deps/services/dependency/mappings/mock.json", want: "dependency"}, + + {path: "awesome/__files/file.ext", want: "awesome"}, + {path: "/awesome/__files", want: "awesome"}, + {path: "q_123-test-not-so(awesome/mappings", want: "q_123-test-not-so(awesome"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getDomainByPath(tt.path) + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/usecases/watcher/watch.go b/internal/usecases/watcher/watch.go new file mode 100644 index 0000000..b6b611c --- /dev/null +++ b/internal/usecases/watcher/watch.go @@ -0,0 +1,52 @@ +package watcher + +import ( + "context" + "fmt" + "io" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/watcher" +) + +type watchersRunner struct { + logger io.Writer +} + +func NewRunner(logger io.Writer) watchersRunner { + return watchersRunner{logger: logger} +} + +func (r *watchersRunner) Watch(ctx context.Context, requests ...WatchRequest) error { + watchers, err := r.createWatchers(requests) + if err != nil { + return fmt.Errorf("create watchers: %w", err) + } + + if err = watchers.Watch(ctx); err != nil { + return fmt.Errorf("watch: %w", err) + } + + return nil +} + +func (r *watchersRunner) createWatchers(requests []WatchRequest) (watcher.Watchers, error) { + var watchers watcher.Watchers + + for _, request := range requests { + preDefinedWatcher, exists := knownWatchers[request.Name] + if !exists { + return nil, fmt.Errorf("watcher '%s' is not found", request.Name) + } + + preDefinedWatcher.Path = request.Path + + realWatcher, err := watcher.NewRealWatcher(preDefinedWatcher, r.logger) + if err != nil { + return nil, fmt.Errorf("create watcher: %w", err) + } + + watchers = append(watchers, realWatcher) + } + + return watchers, nil +} diff --git a/pkg/blacklist/blacklist.go b/pkg/blacklist/blacklist.go new file mode 100644 index 0000000..4ed7df6 --- /dev/null +++ b/pkg/blacklist/blacklist.go @@ -0,0 +1,45 @@ +package blacklist + +import "strings" + +func IsDeliveredWithProtoc(value string) bool { + var ( + filePathsDoNotTouch = []string{"google/protobuf"} + goPackagesDoNotTouch = []string{"google.golang.org/protobuf"} + ) + + for _, file := range filePathsDoNotTouch { + if strings.Contains(value, file) { + return true + } + } + + for _, goPackage := range goPackagesDoNotTouch { + if strings.Contains(value, goPackage) { + return true + } + } + + return false +} + +func IsGoogleAPIContract(value string) bool { + var ( + filePathsDoNotTouch = []string{"google/api"} + goPackagesDoNotTouch = []string{"google.golang.org/genproto/googleapis/api"} + ) + + for _, file := range filePathsDoNotTouch { + if strings.Contains(value, file) { + return true + } + } + + for _, goPackage := range goPackagesDoNotTouch { + if strings.Contains(value, goPackage) { + return true + } + } + + return false +} diff --git a/pkg/blacklist/blacklist_test.go b/pkg/blacklist/blacklist_test.go new file mode 100644 index 0000000..8126a50 --- /dev/null +++ b/pkg/blacklist/blacklist_test.go @@ -0,0 +1,29 @@ +package blacklist + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsDeliveredWithProtoc(t *testing.T) { + tests := []struct { + name string + + value string + + want bool + }{ + {value: "google.golang.org/protobuf/reflect/protoreflect", want: true}, + {value: "google.golang.org/protobuf/runtime/protoimpl", want: true}, + {value: "google/protobuf/wrapper.proto", want: true}, + {value: "google.golang.org/grpc/codes", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsDeliveredWithProtoc(tt.value) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/builder/update.go b/pkg/builder/update.go new file mode 100644 index 0000000..d905917 --- /dev/null +++ b/pkg/builder/update.go @@ -0,0 +1,61 @@ +package builder + +import ( + "fmt" + + "github.com/jhump/protoreflect/desc" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/blacklist" +) + +type protoUpdater interface { + Name() string + Update(contract *desc.FileDescriptor) (*desc.FileDescriptor, error) +} + +func UpdateContracts(contracts []*desc.FileDescriptor, updaters ...protoUpdater) ([]*desc.FileDescriptor, error) { + var updatedDescriptors []*desc.FileDescriptor + + for _, descriptor := range contracts { + goPackage := protoGoPackage(descriptor) + + if blacklist.IsGoogleAPIContract(goPackage) { + updatedDescriptors = append(updatedDescriptors, descriptor) + continue + } + + if blacklist.IsDeliveredWithProtoc(goPackage) { + updatedDescriptors = append(updatedDescriptors, descriptor) + continue + } + + updatedDescriptor := descriptor + + var err error + for _, updater := range updaters { + updatedDescriptor, err = updater.Update(updatedDescriptor) + if err != nil { + return nil, fmt.Errorf("update proto descriptor with %s: %w", updater.Name(), err) + } + } + + updatedDescriptors = append(updatedDescriptors, updatedDescriptor) + } + + return updatedDescriptors, nil +} + +// TODO separate common functions +func protoGoPackage(descriptor *desc.FileDescriptor) string { + if descriptor.GetFileOptions() == nil { + return "" + } + + goPackage := descriptor.GetFileOptions().GoPackage + + if goPackage == nil { + return "" + } + + return *goPackage +} diff --git a/pkg/builder/updaters/gopackage.go b/pkg/builder/updaters/gopackage.go new file mode 100644 index 0000000..83f5c5a --- /dev/null +++ b/pkg/builder/updaters/gopackage.go @@ -0,0 +1,78 @@ +package updaters + +import ( + "fmt" + "path/filepath" + + protodesc "github.com/golang/protobuf/protoc-gen-go/descriptor" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/desc/builder" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/blacklist" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" +) + +type goPackageUpdater struct{} + +func NewGoPackageUpdater() *goPackageUpdater { + return &goPackageUpdater{} +} + +func (u *goPackageUpdater) Name() string { + return "go package updater" +} + +func (u *goPackageUpdater) Update(contract *desc.FileDescriptor) (*desc.FileDescriptor, error) { + return overwriteGoPackage(contract) +} + +func overwriteGoPackage(descriptor *desc.FileDescriptor) (*desc.FileDescriptor, error) { + if blacklist.IsDeliveredWithProtoc(descriptor.GetName()) { + return descriptor, nil + } + + updatedGopackage, err := updateGopackage(descriptor) + if err != nil { + return descriptor, nil + } + + fileBuilder, err := builder.FromFile(descriptor) + if err != nil { + return nil, fmt.Errorf("load builder: %w", err) + } + + fileBuilder.Options = &protodesc.FileOptions{GoPackage: &updatedGopackage} + + updatedDescriptor, err := fileBuilder.Build() + if err != nil { + return nil, fmt.Errorf("build: %w", err) + } + + return updatedDescriptor, nil +} + +func updateGopackage(descriptor *desc.FileDescriptor) (string, error) { + const generatedGoPackage = "github.com/generated/gopackage" + + packageName := descriptor.GetPackage() + + if descriptor.GetFileOptions() == nil { + return newGoPackage(packageName, generatedGoPackage), nil + } + + goPackage := descriptor.GetFileOptions().GoPackage + if goPackage == nil { + return newGoPackage(packageName, generatedGoPackage), nil + } + + if blacklist.IsDeliveredWithProtoc(*goPackage) { + return "", fmt.Errorf("skip this contract") + } + + return newGoPackage(packageName, *goPackage), nil +} + +func newGoPackage(packageName, goPackageName string) string { + newPackageName := strutils.ResolveNameIfCollides(packageName) + return filepath.Join("grpc-proxy/pkg", filepath.Dir(goPackageName), strutils.ToSnakeCase(newPackageName)) +} diff --git a/pkg/builder/updaters/option.go b/pkg/builder/updaters/option.go new file mode 100644 index 0000000..0ece3e3 --- /dev/null +++ b/pkg/builder/updaters/option.go @@ -0,0 +1,168 @@ +package updaters + +import ( + "fmt" + "path/filepath" + + "github.com/golang/protobuf/proto" + dpb "github.com/golang/protobuf/protoc-gen-go/descriptor" + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/desc/builder" + "github.com/jhump/protoreflect/desc/protoparse" + "google.golang.org/genproto/googleapis/api/annotations" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" +) + +type optionUpdater struct { + annotationsDescriptor *desc.FileDescriptor +} + +func NewOptionUpdater() (*optionUpdater, error) { + descriptor, err := parseAnnotations() + if err != nil { + return nil, fmt.Errorf("get annotations descriptor: %w", err) + } + + return &optionUpdater{annotationsDescriptor: descriptor}, nil +} + +func (u *optionUpdater) Name() string { + return "option updater" +} + +func (u *optionUpdater) Update(contract *desc.FileDescriptor) (*desc.FileDescriptor, error) { + return u.overwriteOptions(contract) +} + +func (u *optionUpdater) overwriteOptions(contract *desc.FileDescriptor) (*desc.FileDescriptor, error) { + withAnnotations, err := includeAnnotations(contract, u.annotationsDescriptor) + if err != nil { + return nil, fmt.Errorf("include annotations: %w", err) + } + + withOptions, err := addOptions(withAnnotations) + if err != nil { + return nil, fmt.Errorf("add options: %w", err) + } + + return withOptions, nil +} + +func addOptions(descriptor *desc.FileDescriptor) (*desc.FileDescriptor, error) { + fd := *descriptor + + for _, service := range fd.GetServices() { + if err := addOptionsForService(service); err != nil { + return nil, fmt.Errorf("for service: %w", err) + } + } + + return &fd, nil +} + +func addOptionsForService(descriptor *desc.ServiceDescriptor) error { + methods := descriptor.GetMethods() + + for idx, method := range methods { + url, err := createRpcUrl(descriptor.GetName(), method.GetName()) + if err != nil { + return fmt.Errorf("create rpc url: %w", err) + } + + updatedDescriptor, err := addOptionForMethod(method, url) + if err != nil { + return fmt.Errorf("update method descriptor: %w", err) + } + + methods[idx] = updatedDescriptor + } + + return nil +} + +func addOptionForMethod(descriptor *desc.MethodDescriptor, url string) (*desc.MethodDescriptor, error) { + methodBuilder, err := builder.FromMethod(descriptor) + if err != nil { + return nil, fmt.Errorf("create method builder: %w", err) + } + + opt, err := createOption(url) + if err != nil { + return nil, fmt.Errorf("create method option: %w", err) + } + + updatedDescriptor, err := methodBuilder.SetOptions(opt).Build() + if err != nil { + return nil, fmt.Errorf("set option: %w", err) + } + + return updatedDescriptor, nil +} + +func parseAnnotations() (*desc.FileDescriptor, error) { + parser := &protoparse.Parser{ + IncludeSourceCodeInfo: true, + + ImportPaths: []string{environment.TmpAnnotationProtosDir}, + } + + annotationFiles := []string{ + environment.AnnotationsPath, + environment.AnnotationsHttpPath, + } + + annotationDesc, err := parser.ParseFiles(annotationFiles...) + if err != nil { + return nil, fmt.Errorf("parse 'annotations.proto': %w", err) + } + + return sliceutils.FirstOf(annotationDesc), nil +} + +func includeAnnotations(contractDescriptor, annotationsDescriptor *desc.FileDescriptor) (*desc.FileDescriptor, error) { + fileBuilder, err := builder.FromFile(contractDescriptor) + if err != nil { + return nil, fmt.Errorf("create file builder: %w", err) + } + + updatedFileDescriptor, err := fileBuilder.AddImportedDependency(annotationsDescriptor).Build() + if err != nil { + return nil, fmt.Errorf("add dependency: %w", err) + } + + return updatedFileDescriptor, nil +} + +func createOption(mockURL string) (*dpb.MethodOptions, error) { + //option (google.api.http) = { + // post: "/v1/example/echo" + // body: "*" + //}; + + options := dpb.MethodOptions{} + httpRule := annotations.HttpRule{ + Body: "*", + Pattern: &annotations.HttpRule_Post{Post: mockURL}, + } + + if err := proto.SetExtension(&options, annotations.E_Http, &httpRule); err != nil { + return nil, fmt.Errorf("set extension: %w", err) + } + + return &options, nil +} + +func createRpcUrl(serviceName string, methodName string) (string, error) { + if serviceName == "" || methodName == "" { + return "", fmt.Errorf("empty name") + } + + return filepath.Join( + "/", + strutils.ToCamelCase(serviceName), + strutils.ToCamelCase(methodName), + ), nil +} diff --git a/pkg/compiler/command/command.go b/pkg/compiler/command/command.go new file mode 100644 index 0000000..4df0623 --- /dev/null +++ b/pkg/compiler/command/command.go @@ -0,0 +1,140 @@ +package command + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/compiler/compilecontract" +) + +type protoCompilerPlugin string + +const ( + goPlugin = "go" + goGRPCPlugin = "go-grpc" + goOpenAPIPlugin = "openapi" +) + +type pluginValues struct { + out string +} + +type flag struct { + key string + value string +} + +func createProtoPath(value string) flag { + const protoPathFlag = "proto_path" + return flag{ + key: protoPathFlag, + value: value, + } +} + +func createPluginOut(plugin protoCompilerPlugin, value string) flag { + const tplFlag = "%s_out" + return flag{ + key: fmt.Sprintf(tplFlag, plugin), + value: value, + } +} + +func (a flag) String() string { + return fmt.Sprintf("--%s=%s", a.key, a.value) +} + +type Command struct { + flags []flag + arguments []string +} + +func BuildArgs( + contract compilecontract.Contract, + extraProtoPaths []string, + pluginOutputs map[protoCompilerPlugin]pluginValues, +) Command { + var flags []flag + + protoPaths := append(extraProtoPaths, getProtoPaths(contract)...) + + for _, path := range protoPaths { + flags = append(flags, createProtoPath(path)) + } + + for plugin, values := range pluginOutputs { + flags = append(flags, createPluginOut(plugin, values.out)) + } + + arguments := contractToArguments(contract) + + return Command{flags: flags, arguments: arguments} +} + +func contractToArguments(contract compilecontract.Contract) []string { + arguments := []string{contract.HeaderPath} + arguments = append(arguments, contract.ImportsPaths...) + + return arguments +} + +func getProtoPaths(contract compilecontract.Contract) (protoPaths []string) { + headerDir := filepath.Dir(contract.HeaderPath) + + uniq := map[string]struct{}{ + headerDir: {}, + } + + protoPaths = append(protoPaths, headerDir) + + for _, path := range contract.ImportsPaths { + dir := filepath.Dir(path) + + if _, exists := uniq[dir]; exists { + continue + } + + uniq[dir] = struct{}{} + protoPaths = append(protoPaths, dir) + } + + return protoPaths +} + +func (c *Command) Args() []string { + return c.toSlice() +} + +func (c *Command) String() string { + return fmt.Sprintf( + "\n\t%s", + strings.Join(c.toSlice(), " \\\n\t"), + ) +} + +func (c *Command) toSlice() []string { + var args []string + + for _, fl := range c.flags { + args = append(args, fl.String()) + } + args = append(args, c.arguments...) + + return args +} + +type CompileCommand map[protoCompilerPlugin]pluginValues + +func CompileGoPackages(path string) CompileCommand { + return map[protoCompilerPlugin]pluginValues{ + goPlugin: {out: path}, + goGRPCPlugin: {out: path}, + } +} + +func CompileOpenAPI(path string) CompileCommand { + return map[protoCompilerPlugin]pluginValues{ + goOpenAPIPlugin: {out: path}, + } +} diff --git a/pkg/compiler/command/command_test.go b/pkg/compiler/command/command_test.go new file mode 100644 index 0000000..b662b93 --- /dev/null +++ b/pkg/compiler/command/command_test.go @@ -0,0 +1,122 @@ +package command + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/compiler/compilecontract" +) + +func Test_buildArgs(t *testing.T) { + tests := []struct { + name string + + extraProtoPaths []string + contract compilecontract.Contract + pluginValues map[protoCompilerPlugin]pluginValues + + want Command + }{ + { + pluginValues: CompileGoPackages("/here/go"), + contract: compilecontract.Contract{ + HeaderPath: "/test/inner/dir/grpc/sample.proto", + ImportsPaths: []string{"/test/inner/dir/grpc/import/sample.proto"}, + }, + want: Command{ + flags: []flag{ + createProtoPath("/test/inner/dir/grpc"), + createProtoPath("/test/inner/dir/grpc/import"), + createPluginOut(goPlugin, "/here/go"), + createPluginOut(goGRPCPlugin, "/here/go"), + }, + arguments: []string{ + "/test/inner/dir/grpc/sample.proto", + "/test/inner/dir/grpc/import/sample.proto", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := BuildArgs(tt.contract, tt.extraProtoPaths, tt.pluginValues) + assert.ElementsMatch(t, tt.want.arguments, got.arguments) + assert.ElementsMatch(t, tt.want.flags, got.flags) + }) + } +} + +func Test_command_Args(t *testing.T) { + tests := []struct { + name string + + flags []flag + arguments []string + + want []string + }{ + { + flags: []flag{ + createPluginOut(goPlugin, "/here/go"), + createPluginOut(goGRPCPlugin, "/here/grpc"), + createProtoPath("/test/inner/dir/grpc"), + createProtoPath("/test/inner/dir/grpc/import"), + }, + arguments: []string{ + "/test/inner/dir/grpc/sample.proto", + "/test/inner/dir/grpc/import/sample.proto", + }, + want: []string{ + "--go_out=/here/go", + "--go-grpc_out=/here/grpc", + "--proto_path=/test/inner/dir/grpc", + "--proto_path=/test/inner/dir/grpc/import", + "/test/inner/dir/grpc/sample.proto", + "/test/inner/dir/grpc/import/sample.proto", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Command{flags: tt.flags, arguments: tt.arguments} + got := c.Args() + assert.ElementsMatch(t, tt.want, got) + }) + } +} + +func Test_command_String(t *testing.T) { + tests := []struct { + name string + + flags []flag + arguments []string + + want string + }{ + { + flags: []flag{ + createPluginOut(goPlugin, "/here/go"), + createPluginOut(goGRPCPlugin, "/here/grpc"), + createProtoPath("/test/inner/dir/grpc"), + createProtoPath("/test/inner/dir/grpc/import"), + }, + arguments: []string{ + "/test/inner/dir/grpc/sample.proto", + "/test/inner/dir/grpc/import/sample.proto", + }, + want: "\n\t--go_out=/here/go \\\n\t--go-grpc_out=/here/grpc \\\n\t--proto_path=/test/inner/dir/grpc \\\n\t--proto_path=/test/inner/dir/grpc/import \\\n\t/test/inner/dir/grpc/sample.proto \\\n\t/test/inner/dir/grpc/import/sample.proto", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Command{flags: tt.flags, arguments: tt.arguments} + got := c.String() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/compiler/compilecontract/contract.go b/pkg/compiler/compilecontract/contract.go new file mode 100644 index 0000000..f6e393d --- /dev/null +++ b/pkg/compiler/compilecontract/contract.go @@ -0,0 +1,7 @@ +package compilecontract + +type Contract struct { + HeaderPath string + + ImportsPaths []string +} diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go new file mode 100644 index 0000000..0b1a81c --- /dev/null +++ b/pkg/compiler/compiler.go @@ -0,0 +1,95 @@ +package compiler + +import ( + "context" + "fmt" + "io" + "os/exec" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/compiler/command" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/compiler/compilecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/executils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +type runner interface { + Run(ctx context.Context, cmd string, args ...string) error +} + +type compiler struct { + compilerPath string + extraProtoPaths []string + + runner +} + +func New(fs afero.Fs, shell runner, protoPaths ...string) (compiler, error) { + protocPath, err := exec.LookPath(ProtoCompiler) + if err != nil { + return compiler{}, fmt.Errorf("look path for '%s': %w", ProtoCompiler, err) + } + + if err = executils.HostHasBinaries(ProtoGoPlugin, ProtoGRPCPlugin, ProtoOpenAPIPlugin); err != nil { + return compiler{}, fmt.Errorf("check binary: %w", err) + } + + if err = fsutils.ValidDirectories(fs, protoPaths...); err != nil { + return compiler{}, fmt.Errorf("check proto path: %w", err) + } + + extraProtoPaths := []string{environment.TmpWellKnownProtosDir} + extraProtoPaths = append(extraProtoPaths, protoPaths...) + + return compiler{ + runner: shell, + compilerPath: protocPath, + extraProtoPaths: extraProtoPaths, + }, nil +} + +func fromProtoContract(contract protocontract.Contract) compilecontract.Contract { + return compilecontract.Contract{ + HeaderPath: contract.HeaderPath, + ImportsPaths: contract.ImportsPaths, + } +} + +func fromBaseContract(contract basecontract.Contract) compilecontract.Contract { + return compilecontract.Contract{ + HeaderPath: contract.HeaderPath, + ImportsPaths: contract.ImportsPaths, + } +} + +func (c compiler) CompileToGo(ctx context.Context, contract protocontract.Contract, output string, logs io.Writer) error { + commandToCompile := command.CompileGoPackages(output) + return c.compile(ctx, fromProtoContract(contract), logs, commandToCompile) +} + +func (c compiler) CompileToOpenAPI(ctx context.Context, contract basecontract.Contract, output string, logs io.Writer) error { + commandToCompile := command.CompileOpenAPI(output) + return c.compile(ctx, fromBaseContract(contract), logs, commandToCompile) +} + +func (c compiler) compile(ctx context.Context, contract compilecontract.Contract, logs io.Writer, compileCmd command.CompileCommand) error { + compile := command.BuildArgs(contract, c.extraProtoPaths, compileCmd) + + if _, err := fmt.Fprintln(logs, logProtocCall(c.compilerPath, compile)); err != nil { + return fmt.Errorf("print: %w", err) + } + + if err := c.Run(ctx, c.compilerPath, compile.Args()...); err != nil { + return fmt.Errorf("run: %w", err) + } + + return nil +} + +func logProtocCall(protoc string, cmd command.Command) string { + return fmt.Sprintf("%s \\%s", protoc, cmd.String()) +} diff --git a/pkg/compiler/const.go b/pkg/compiler/const.go new file mode 100644 index 0000000..7daf84a --- /dev/null +++ b/pkg/compiler/const.go @@ -0,0 +1,8 @@ +package compiler + +const ( + ProtoCompiler = "protoc" + ProtoGoPlugin = "protoc-gen-go" + ProtoGRPCPlugin = "protoc-gen-go-grpc" + ProtoOpenAPIPlugin = "protoc-gen-openapi" +) diff --git a/pkg/environment/environment.go b/pkg/environment/environment.go new file mode 100644 index 0000000..9f1e03f --- /dev/null +++ b/pkg/environment/environment.go @@ -0,0 +1,113 @@ +package environment + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/static" +) + +const ( + TmpUnifiedContractsDir = "/tmp/unified-contracts" + TmpOverwrittenContractsDir = "/tmp/overwritten-contracts" + TmpGeneratedPackagesDir = "/tmp/generated-packages" + + NginxConfigsPath = "/etc/nginx/http.d" + SupervisordConfigsPath = "/etc/supervisord/mocks" + SupervisordConfigsDirPath = "/etc/supervisord" + SupervisordMainConfigPath = "/etc/supervisord/supervisord.conf" + + DefaultWiremockConfigPath = "/home/mock" + + TmpWellKnownProtosDir = "/tmp/proto-includes" + TmpAnnotationProtosDir = "/tmp/proto-annotations" + + AnnotationsPath = "google/api/annotations.proto" + AnnotationsHttpPath = "google/api/http.proto" + + DefaultCertificatesPath = "/etc/ssl" + + TrustedCertificatePath = "/etc/ssl/certs/ca-certificates.crt" + + CAKeyFile = "mock/share/mockCA.key" + CACertFile = "mock/share/mockCA.crt" + + CertKeyFile = "mock/mock.key" + CertCertFile = "mock/mock.crt" +) + +func DumpProtos(fs afero.Fs) error { + protoToCopy := map[string]string{ + "proto-includes": TmpWellKnownProtosDir, + "proto-annotations": TmpAnnotationProtosDir, + } + + staticFS := static.FromEmbed() + + for sourcePath, targetPath := range protoToCopy { + if err := fsutils.CopyDir(staticFS, fs, sourcePath, targetPath, true); err != nil { + return fmt.Errorf("copy protos from embed fs: %w", err) + } + } + + return nil +} + +func CleanTmpDirs(fs afero.Fs) error { + tmpDirs := []string{ + TmpUnifiedContractsDir, + TmpGeneratedPackagesDir, + TmpOverwrittenContractsDir, + } + + if err := fsutils.RemoveTmpDirs(fs, tmpDirs...); err != nil { + return fmt.Errorf("remove tmp dir: %w", err) + } + + return nil +} + +func CleanConfigs(fs afero.Fs, path string) error { + match := func(info os.FileInfo) bool { + if info.IsDir() { + return false + } + + const ( + mockPrefix = "mock-" + mockSuffix = ".conf" + ) + + return strings.HasPrefix(info.Name(), mockPrefix) && + strings.HasSuffix(info.Name(), mockSuffix) + } + + entries, err := fsutils.GatherMatchedEntriesInDir(fs, path, match) + if err != nil { + return fmt.Errorf("gather matched entries: %w", err) + } + + for _, entryPath := range entries { + if err = fs.Remove(entryPath); err != nil { + return fmt.Errorf("remove: %w", err) + } + } + + return nil +} + +func IsCAExists(fs afero.Fs, output string) bool { + certFilePath := filepath.Join(output, CACertFile) + + _, err := fs.Stat(certFilePath) + if err == nil { + return true + } + + return false +} diff --git a/pkg/errgroup/errgroup.go b/pkg/errgroup/errgroup.go new file mode 100644 index 0000000..164060d --- /dev/null +++ b/pkg/errgroup/errgroup.go @@ -0,0 +1,33 @@ +package errgroup + +import ( + "context" + "fmt" + + "golang.org/x/sync/errgroup" +) + +type Group struct { + *errgroup.Group +} + +func WithContext(parent context.Context) (*Group, context.Context) { + g, ctx := errgroup.WithContext(parent) + return &Group{g}, ctx +} + +func (g *Group) Go(originFn func() error) { + fn := func() (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("recovered panic from gorutine: %v", r) + } + }() + + err = originFn() + + return + } + + g.Group.Go(fn) +} diff --git a/pkg/fstesting/comparator.go b/pkg/fstesting/comparator.go new file mode 100644 index 0000000..d98a37e --- /dev/null +++ b/pkg/fstesting/comparator.go @@ -0,0 +1,140 @@ +package fstesting + +import ( + "bytes" + "fmt" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/fstesting/entry" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +type FSDiff struct { + EntriesWithNotEqualBodies []entry.Entry +} + +func (diff *FSDiff) Add(entry entry.Entry) { + diff.EntriesWithNotEqualBodies = + append(diff.EntriesWithNotEqualBodies, entry) +} + +func (diff *FSDiff) Empty() bool { + return len(diff.EntriesWithNotEqualBodies) == 0 +} + +type FSEntries []entry.Entry + +type SetOfFSEntries struct { + Entries []FSEntries + + ActualFS afero.Fs + ExpectedFS afero.Fs +} + +func (sfse *SetOfFSEntries) Expected() FSEntries { + if len(sfse.Entries) < 2 { + panic("set is incompatible") + } + return sfse.Entries[0] +} + +func (sfse *SetOfFSEntries) Actual() FSEntries { + if len(sfse.Entries) < 2 { + panic("set is incompatible") + } + return sfse.Entries[1] +} + +func (sfse *SetOfFSEntries) Size() int { + return len(sfse.Actual()) +} + +func NewSet(expectedFS, actualFS afero.Fs) (SetOfFSEntries, error) { + expectedEntries, err := fsutils.GatherEntries(expectedFS, "/") + if err != nil { + return SetOfFSEntries{}, fmt.Errorf("walk throw expected fs: %w", err) + } + + actualEntries, err := fsutils.GatherEntries(actualFS, "/") + if err != nil { + return SetOfFSEntries{}, fmt.Errorf("walk throw actual fs: %w", err) + } + + if len(expectedEntries) != len(actualEntries) { + return SetOfFSEntries{}, fmt.Errorf( + "incorrect size of fs entries: len(expected) = %d, len(actual) = %d", + len(expectedEntries), len(actualEntries), + ) + } + + return SetOfFSEntries{ + ActualFS: actualFS, + ExpectedFS: expectedFS, + Entries: []FSEntries{expectedEntries, actualEntries}, + }, nil +} + +func (sfse *SetOfFSEntries) compareFiles(expected, actual entry.Entry) (bool, error) { + entryIsDir := expected.FileInfo.IsDir() && + expected.FileInfo.IsDir() == actual.FileInfo.IsDir() + + if entryIsDir { + return true, nil + } + + expectedBody, err := fsutils.ReadFile(sfse.ExpectedFS, expected.Path) + if err != nil { + return false, fmt.Errorf("read expected file: %w", err) + } + + actualBody, err := fsutils.ReadFile(sfse.ActualFS, actual.Path) + if err != nil { + return false, fmt.Errorf("read actual file: %w", err) + } + + equal := bytes.Equal(expectedBody, actualBody) + + return equal, nil +} + +func (sfse *SetOfFSEntries) Compare() (FSDiff, error) { + var ( + size = sfse.Size() + actual = sfse.Actual() + expected = sfse.Expected() + ) + + var diff FSDiff + for i := 0; i < size; i++ { + var ( + actualEntry = actual[i] + expectedEntry = expected[i] + ) + + equal, err := sfse.compareFiles(expectedEntry, actualEntry) + if err != nil { + return FSDiff{}, fmt.Errorf("compare files: %w", err) + } + + if !equal { + diff.Add(actualEntry) + } + } + + return diff, nil +} + +func CompareFS(expectedFS, actualFS afero.Fs) (FSDiff, error) { + sfse, err := NewSet(expectedFS, actualFS) + if err != nil { + return FSDiff{}, fmt.Errorf("create set of fs: %w", err) + } + + fsDiff, err := sfse.Compare() + if err != nil { + return FSDiff{}, fmt.Errorf("compare fs: %w", err) + } + + return fsDiff, nil +} diff --git a/pkg/fstesting/entry/entry.go b/pkg/fstesting/entry/entry.go new file mode 100644 index 0000000..0583d58 --- /dev/null +++ b/pkg/fstesting/entry/entry.go @@ -0,0 +1,13 @@ +package entry + +import "io/fs" + +type Entry struct { + Path string + + fs.FileInfo +} + +func NewEntry(path string, info fs.FileInfo) Entry { + return Entry{FileInfo: info, Path: path} +} diff --git a/pkg/fstesting/entrymock/entry_mock.go b/pkg/fstesting/entrymock/entry_mock.go new file mode 100644 index 0000000..fa67ba1 --- /dev/null +++ b/pkg/fstesting/entrymock/entry_mock.go @@ -0,0 +1,53 @@ +package entrymock + +import ( + "io/fs" + "path/filepath" + "time" +) + +type Entry struct { + isDir bool + name string + path string +} + +func Dir(path string) Entry { + return NewEntryMock(path, true) +} + +func File(path string) Entry { + return NewEntryMock(path, false) +} + +func NewEntryMock(path string, isDir bool) Entry { + return Entry{name: filepath.Base(path), isDir: isDir, path: path} +} + +func (e Entry) Name() string { + return e.name +} + +func (e Entry) IsDir() bool { + return e.isDir +} + +func (e Entry) Path() string { + return e.path +} + +func (e Entry) Size() int64 { + return 0 +} + +func (e Entry) Mode() fs.FileMode { + return fs.ModePerm +} + +func (e Entry) ModTime() time.Time { + return time.Time{} +} + +func (e Entry) Sys() any { + return nil +} diff --git a/pkg/fstesting/setup.go b/pkg/fstesting/setup.go new file mode 100644 index 0000000..bce2ac2 --- /dev/null +++ b/pkg/fstesting/setup.go @@ -0,0 +1,42 @@ +package fstesting + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/fstesting/entrymock" +) + +func CreateMockFS(entries ...entrymock.Entry) afero.Fs { + fs := afero.NewMemMapFs() + + // the MemMapFS write error is deliberately ignored + fs, _ = WriteMockEntries(fs, entries...) + + return fs +} + +func WriteMockEntries(fs afero.Fs, entries ...entrymock.Entry) (afero.Fs, error) { + for _, entry := range entries { + path := entry.Path() + + if entry.IsDir() { + if err := fs.MkdirAll(path, os.ModePerm); err != nil { + return nil, fmt.Errorf("create dir: %w", err) + } + } else { + if err := fs.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { + return nil, fmt.Errorf("create dir: %w", err) + } + _, err := fs.Create(path) + if err != nil { + return nil, fmt.Errorf("create file: %w", err) + } + } + } + + return fs, nil +} diff --git a/pkg/generators/certificates/cert.go b/pkg/generators/certificates/cert.go new file mode 100644 index 0000000..99c458e --- /dev/null +++ b/pkg/generators/certificates/cert.go @@ -0,0 +1,74 @@ +package certificates + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "fmt" + "path/filepath" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" +) + +func (g *certsGen) getCertificateAuthority(caCertificate *x509.Certificate, caPrivateKey *rsa.PrivateKey, output string) (pair, error) { + var caContent []byte + var err error + + convertedPrivateKey := x509.MarshalPKCS1PrivateKey(caPrivateKey) + + caPrivateKeyPEM, err := marshalToPEMBlocks(privateKeyPEMBlockName, convertedPrivateKey) + if err != nil { + return pair{}, fmt.Errorf("marshal ca private key: %w", err) + } + + if environment.IsCAExists(g.fs, output) { + caCertFilePath := filepath.Join(output, environment.CACertFile) + + caPEM, err := afero.ReadFile(g.fs, caCertFilePath) + if err != nil { + return pair{}, fmt.Errorf("read ca content: %w", err) + } + + return pair{privateKey: caPrivateKeyPEM, certificate: caPEM}, nil + } + + caContent, err = x509.CreateCertificate(rand.Reader, caCertificate, caCertificate, &caPrivateKey.PublicKey, caPrivateKey) + if err != nil { + return pair{}, fmt.Errorf("create ca certificate: %w", err) + } + + caPEM, err := marshalToPEMBlocks(certificatePEMBlockName, caContent) + if err != nil { + return pair{}, fmt.Errorf("marshal ca: %w", err) + } + + return pair{privateKey: caPrivateKeyPEM, certificate: caPEM}, nil +} + +func (g *certsGen) createCertificate(caCertificateDesc, certificateDesc *x509.Certificate, caPrivateKey *rsa.PrivateKey) (pair, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, defaultBitsCount) + if err != nil { + return pair{}, fmt.Errorf("generate private key: %w", err) + } + + certificateContent, err := x509.CreateCertificate(rand.Reader, certificateDesc, caCertificateDesc, &privateKey.PublicKey, caPrivateKey) + if err != nil { + return pair{}, fmt.Errorf("create certificate: %w", err) + } + + convertedPrivateKey := x509.MarshalPKCS1PrivateKey(privateKey) + + privateKeyPEM, err := marshalToPEMBlocks(privateKeyPEMBlockName, convertedPrivateKey) + if err != nil { + return pair{}, fmt.Errorf("marshal private key: %w", err) + } + + certificatePEM, err := marshalToPEMBlocks(certificatePEMBlockName, certificateContent) + if err != nil { + return pair{}, fmt.Errorf("marshal certificate: %w", err) + } + + return pair{privateKey: privateKeyPEM, certificate: certificatePEM}, nil +} diff --git a/pkg/generators/certificates/cert_test.go b/pkg/generators/certificates/cert_test.go new file mode 100644 index 0000000..7f1a519 --- /dev/null +++ b/pkg/generators/certificates/cert_test.go @@ -0,0 +1,58 @@ +package certificates + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/certificates/test" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/configopener" +) + +func Test_testCertificates(t *testing.T) { + t.Run("with correct certificates", func(t *testing.T) { + opener := configopener.New(staticFS, "tests/data/wiremock/configs/one-service") + g := NewGenerator(staticFS, opener) + + caCertPair, certPair, err := g.generate([]string{"awesome"}, "") + require.NoError(t, err) + + serverConfig, clientConfig, err := createTLSConfig(caCertPair, certPair) + require.NoError(t, err) + + err = runTest(test.NewCertsTester(serverConfig, clientConfig)) + require.NoError(t, err) + }) +} + +func runTest(t test.CertsTester) error { + t.Server.StartTLS() + defer t.Server.Close() + + if err := t.DoTestRequest(); err != nil { + return err + } + + return nil +} + +func createTLSConfig(caPair, certificatePair pair) (*tls.Config, *tls.Config, error) { + tlsCertificate, err := tls.X509KeyPair(certificatePair.certificate, certificatePair.privateKey) + if err != nil { + return nil, nil, fmt.Errorf("tls pair: %w", err) + } + + pool := x509.NewCertPool() + + if !pool.AppendCertsFromPEM(caPair.certificate) { + return nil, nil, fmt.Errorf("pool doesn't contain certificate authority") + } + + clientTLSConf := tls.Config{RootCAs: pool} + serverTLSConf := tls.Config{Certificates: []tls.Certificate{tlsCertificate}} + + return &serverTLSConf, &clientTLSConf, nil +} diff --git a/pkg/generators/certificates/domains.go b/pkg/generators/certificates/domains.go new file mode 100644 index 0000000..a49901f --- /dev/null +++ b/pkg/generators/certificates/domains.go @@ -0,0 +1,62 @@ +package certificates + +import ( + "errors" + "fmt" + "log" + "os" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +var domains = []string{ + "com", "net", "gov", "mil", "io", + "ru", "org", "local", "tech", "dev", + "online", "internal", "team", +} + +var commonDomains = []string{ + "localhost", "mock", + "grpc-wiremock", "*.local.sbermarket.tech", +} + +func (g *certsGen) collectDomains(commonDomains, domains []string) ([]string, error) { + wiremockConfig, err := g.opener.Open() + if err != nil { + log.Printf("certgen: open wiremock config: %s", err) + + if err = handleOpenerErrors(err); err != nil { + return nil, err + } + } + + var targetDomains []string + + for _, service := range wiremockConfig.Services { + targetDomains = append(targetDomains, service.Name) + } + + var certDomains []string + + for _, domain := range targetDomains { + for _, ext := range domains { + certDomains = append(certDomains, fmt.Sprintf("%s.%s", domain, ext)) + } + } + + if len(certDomains) == 0 { + return strutils.UniqueAndSorted(commonDomains...), nil + } + + return strutils.UniqueAndSorted(append(certDomains, commonDomains...)...), nil +} + +func handleOpenerErrors(err error) error { + if errors.Is(err, os.ErrNotExist) || + errors.Is(err, config.EmptyWiremockConfigErr) { + return nil + } + + return fmt.Errorf("read wiremock config: %w", err) +} diff --git a/pkg/generators/certificates/generate.go b/pkg/generators/certificates/generate.go new file mode 100644 index 0000000..bb334d9 --- /dev/null +++ b/pkg/generators/certificates/generate.go @@ -0,0 +1,201 @@ +package certificates + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "path/filepath" + "time" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +const ( + defaultBitsCount = 2048 + + serialNumber = 1126 + + privateKeyPEMBlockName = "RSA PRIVATE KEY" + certificatePEMBlockName = "CERTIFICATE" +) + +type configOpener interface { + Open() (config.Wiremock, error) +} + +var caCertificateDesc = &x509.Certificate{ + IsCA: true, + + Subject: defaultSubject, + + SerialNumber: big.NewInt(serialNumber), + + NotBefore: time.Now(), + NotAfter: todayPlusOneYear(), + + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + + BasicConstraintsValid: true, +} + +var defaultSubject = pkix.Name{ + CommonName: "grpc-wiremock", + + Country: []string{"RU"}, + Locality: []string{"Moscow"}, + + Organization: []string{"Sbermarket LLC"}, +} + +type pair struct { + privateKey []byte + certificate []byte +} + +type certsGen struct { + opener configOpener + + fs afero.Fs +} + +func NewGenerator(fs afero.Fs, opener configOpener) certsGen { + return certsGen{opener: opener, fs: fs} +} + +func (g *certsGen) Generate(_ context.Context, output string) error { + collectedDomains, err := g.collectDomains(commonDomains, domains) + if err != nil { + return fmt.Errorf("generate domains: %w", err) + } + + caCertPair, certPair, err := g.generate(collectedDomains, output) + if err != nil { + return fmt.Errorf("generate cert pairs: %w", err) + } + + filesToSave := map[string][]byte{ + environment.CertKeyFile: certPair.privateKey, + environment.CertCertFile: certPair.certificate, + + environment.CAKeyFile: caCertPair.privateKey, + environment.CACertFile: caCertPair.certificate, + } + + for filePath, content := range filesToSave { + saveToPath := filepath.Join(output, filePath) + + if err = fsutils.WriteFile(g.fs, saveToPath, string(content)); err != nil { + return fmt.Errorf("save '%s': %w", saveToPath, err) + } + } + + return nil +} + +func (g *certsGen) generate(collectedDomains []string, output string) (pair, pair, error) { + caPrivateKey, err := g.getCAKey(output) + if err != nil { + return pair{}, pair{}, fmt.Errorf("get key: %w", err) + } + + caPair, err := g.getCertificateAuthority(caCertificateDesc, caPrivateKey, output) + if err != nil { + return pair{}, pair{}, fmt.Errorf("generate ca: %w", err) + } + + certificateDesc := createCertificateDesc(collectedDomains) + + certificatePair, err := g.createCertificate(caCertificateDesc, certificateDesc, caPrivateKey) + if err != nil { + return pair{}, pair{}, fmt.Errorf("generate certificate: %w", err) + } + + return caPair, certificatePair, nil +} + +func (g *certsGen) getCAKey(output string) (*rsa.PrivateKey, error) { + var caPrivateKey *rsa.PrivateKey + var err error + + if !environment.IsCAExists(g.fs, output) { + caPrivateKey, err = rsa.GenerateKey(rand.Reader, defaultBitsCount) + if err != nil { + return nil, fmt.Errorf("create ca private key: %w", err) + } + + return caPrivateKey, nil + } + + keyFilePath := filepath.Join(output, environment.CAKeyFile) + + keyContent, err := afero.ReadFile(g.fs, keyFilePath) + if err != nil { + return nil, fmt.Errorf("read key file: %w", err) + } + + keyPEM, err := unmarshalFromPEMBlock(keyContent) + if err != nil { + return nil, fmt.Errorf("unmarshal key file: %w", err) + } + + privateKey, err := x509.ParsePKCS1PrivateKey(keyPEM.Bytes) + if err != nil { + return nil, fmt.Errorf("parse key file: %w", err) + } + + return privateKey, nil +} + +func createCertificateDesc(dnsNames []string) *x509.Certificate { + return &x509.Certificate{ + Subject: defaultSubject, + + DNSNames: dnsNames, + KeyUsage: x509.KeyUsageDigitalSignature, + + SerialNumber: big.NewInt(serialNumber), + + NotBefore: time.Now(), + NotAfter: todayPlusOneYear(), + + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + } +} + +func marshalToPEMBlocks(contentType string, content []byte) ([]byte, error) { + var buffer bytes.Buffer + + block := pem.Block{Type: contentType, Bytes: content} + + if err := pem.Encode(&buffer, &block); err != nil { + return nil, fmt.Errorf("encode: %w", err) + } + + return buffer.Bytes(), nil +} + +func unmarshalFromPEMBlock(content []byte) (*pem.Block, error) { + block, _ := pem.Decode(content) + if block == nil { + return nil, fmt.Errorf("empty block") + } + + return block, nil +} + +func todayPlusOneYear() time.Time { + return time.Now().AddDate(1, 0, 0) +} diff --git a/pkg/generators/certificates/generate_test.go b/pkg/generators/certificates/generate_test.go new file mode 100644 index 0000000..56eea5f --- /dev/null +++ b/pkg/generators/certificates/generate_test.go @@ -0,0 +1,80 @@ +package certificates + +import ( + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/configopener" + "github.com/SberMarket-Tech/grpc-wiremock/static" +) + +var ( + osfs = afero.NewOsFs() + + staticFS = static.FromEmbed() + + projectDir = filepath.Join(fsutils.CurrentDir(), "../../..") +) + +func Test_certsGen_Generate(t *testing.T) { + tests := []struct { + name string + + fs afero.Fs + + domains []string + commonDomains []string + wiremockPath string + want []string + }{ + { + fs: osfs, + + domains: []string{"com", "ru"}, + commonDomains: []string{"localhost", "grpc-wiremock", "google.com"}, + want: []string{"localhost", "grpc-wiremock", "google.com", "awesome.ru", "awesome.com", "push-sender.ru", "push-sender.com"}, + + wiremockPath: filepath.Join(projectDir, "static/tests/data/supervisord/two-services"), + }, + { + fs: staticFS, + + domains: []string{"com", "ru"}, + commonDomains: []string{"localhost", "grpc-wiremock", "google.com"}, + want: []string{"localhost", "grpc-wiremock", "google.com", "awesome.ru", "awesome.com"}, + + wiremockPath: filepath.Join(projectDir, "static/tests/data/supervisord/one-service"), + }, + { + fs: staticFS, + + domains: []string{"com", "ru"}, + commonDomains: []string{"localhost", "grpc-wiremock"}, + want: []string{"localhost", "grpc-wiremock"}, + + wiremockPath: filepath.Join(projectDir, "static/tests/data/supervisord/empty"), + }, + { + fs: staticFS, + + domains: []string{"com", "ru"}, + commonDomains: []string{"localhost", "grpc-wiremock", "google.com"}, + want: []string{"localhost", "grpc-wiremock", "google.com"}, + + wiremockPath: filepath.Join(projectDir, "static/tests/data/supervisord/empty-dir"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &certsGen{opener: configopener.New(tt.fs, tt.wiremockPath), fs: tt.fs} + got, err := g.collectDomains(tt.commonDomains, tt.domains) + require.NoError(t, err) + require.ElementsMatch(t, got, tt.want) + }) + } +} diff --git a/pkg/generators/certificates/test/tester.go b/pkg/generators/certificates/test/tester.go new file mode 100644 index 0000000..cfdc687 --- /dev/null +++ b/pkg/generators/certificates/test/tester.go @@ -0,0 +1,58 @@ +package test + +import ( + "crypto/tls" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" +) + +const successMessage = "success" + +type CertsTester struct { + Client *http.Client + Server *httptest.Server +} + +func NewCertsTester(serverCert, clientCert *tls.Config) CertsTester { + server := httptest.NewUnstartedServer(createTestHandler()) + server.TLS = serverCert + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: clientCert, + }, + } + + return CertsTester{Server: server, Client: client} +} + +func (t *CertsTester) DoTestRequest() error { + response, err := t.Client.Get(t.Server.URL) + if err != nil { + return fmt.Errorf("do get: %w", err) + } + + responseContent, err := io.ReadAll(response.Body) + if err != nil { + return fmt.Errorf("read body: %w", err) + } + + return requireEqualBody(string(responseContent), successMessage) +} + +func createTestHandler() http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + fmt.Fprintln(writer, successMessage) + } +} + +func requireEqualBody(got, target string) error { + if strings.TrimSpace(got) != target { + return fmt.Errorf("not equal got: %s, target: %s", got, target) + } + + return nil +} diff --git a/pkg/generators/configs/generate.go b/pkg/generators/configs/generate.go new file mode 100644 index 0000000..a542870 --- /dev/null +++ b/pkg/generators/configs/generate.go @@ -0,0 +1,61 @@ +package configs + +import ( + "context" + "fmt" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +type Configuer interface { + GenerateConfig(values Values) error +} + +type Reloader interface { + ReloadConfig(ctx context.Context) error +} + +type Values struct { + Domain string + Port string + Root string +} + +type runner struct { + fs afero.Fs + config config.Wiremock +} + +func NewRunner(fs afero.Fs, config config.Wiremock) *runner { + return &runner{fs: fs, config: config} +} + +func (g *runner) RunConfiguers(configuers ...Configuer) error { + for _, service := range g.config.Services { + values := Values{ + Domain: service.Name, + Root: service.RootDir, + Port: fmt.Sprint(service.Port), + } + + for _, configuer := range configuers { + if err := configuer.GenerateConfig(values); err != nil { + return fmt.Errorf("generate config: %w", err) + } + } + } + + return nil +} + +func (g *runner) RunReloaders(ctx context.Context, reloaders ...Reloader) error { + for _, reloader := range reloaders { + if err := reloader.ReloadConfig(ctx); err != nil { + return fmt.Errorf("reload config: %w", err) + } + } + + return nil +} diff --git a/pkg/generators/configs/nginx/configure.go b/pkg/generators/configs/nginx/configure.go new file mode 100644 index 0000000..f88e39e --- /dev/null +++ b/pkg/generators/configs/nginx/configure.go @@ -0,0 +1,51 @@ +package nginx + +import ( + "fmt" + "path/filepath" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/configs" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +// renderer abstracts how exactly project should be rendered. +type renderer interface { + Substitute(string, interface{}) (string, error) +} + +type nginxDomainConfView struct { + Domain string + Port string +} + +type Configuer struct { + Renderer renderer + OutputPath string + + afero.Fs +} + +func (c Configuer) GenerateConfig(values configs.Values) error { + const templatePath = "proxy-nginx/files/nginx.conf.tpl" + + confView := nginxDomainConfView{Domain: values.Domain, Port: values.Port} + + content, err := c.Renderer.Substitute(templatePath, &confView) + if err != nil { + return fmt.Errorf("substitute: %w", err) + } + + pathToSave := c.createConfigPath(values.Domain) + + if err = fsutils.WriteFile(c.Fs, pathToSave, content); err != nil { + return fmt.Errorf("write: %w", err) + } + + return nil +} + +func (c Configuer) createConfigPath(domain string) string { + return filepath.Join(c.OutputPath, fmt.Sprintf("mock-%s.conf", domain)) +} diff --git a/pkg/generators/configs/nginx/reload.go b/pkg/generators/configs/nginx/reload.go new file mode 100644 index 0000000..68bfcc2 --- /dev/null +++ b/pkg/generators/configs/nginx/reload.go @@ -0,0 +1,25 @@ +package nginx + +import ( + "context" + "fmt" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" +) + +type commandRunner interface { + Run(ctx context.Context, cmd string, args ...string) error +} + +type Reloader struct { + Command []string + Runner commandRunner +} + +func (r Reloader) ReloadConfig(ctx context.Context) error { + if err := r.Runner.Run(ctx, sliceutils.FirstOf(r.Command), r.Command[1:]...); err != nil { + return fmt.Errorf("reload: %w", err) + } + + return nil +} diff --git a/pkg/generators/configs/supervisord/configure.go b/pkg/generators/configs/supervisord/configure.go new file mode 100644 index 0000000..f426486 --- /dev/null +++ b/pkg/generators/configs/supervisord/configure.go @@ -0,0 +1,52 @@ +package supervisord + +import ( + "fmt" + "path/filepath" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/configs" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +// renderer abstracts how exactly project should be rendered. +type renderer interface { + Substitute(string, interface{}) (string, error) +} + +type supervisordDomainConfView struct { + Domain string + Port string + Root string +} + +type Configuer struct { + Renderer renderer + OutputPath string + + afero.Fs +} + +func (c Configuer) GenerateConfig(values configs.Values) error { + const templatePath = "supervisord/files/supervisord.conf.tpl" + + confView := supervisordDomainConfView{Domain: values.Domain, Port: values.Port, Root: values.Root} + + content, err := c.Renderer.Substitute(templatePath, &confView) + if err != nil { + return fmt.Errorf("substitute: %w", err) + } + + pathToSave := c.createConfigPath(values.Domain) + + if err = fsutils.WriteFile(c.Fs, pathToSave, content); err != nil { + return fmt.Errorf("write: %w", err) + } + + return nil +} + +func (c Configuer) createConfigPath(domain string) string { + return filepath.Join(c.OutputPath, fmt.Sprintf("mock-%s.conf", domain)) +} diff --git a/pkg/generators/configs/supervisord/reload.go b/pkg/generators/configs/supervisord/reload.go new file mode 100644 index 0000000..950d205 --- /dev/null +++ b/pkg/generators/configs/supervisord/reload.go @@ -0,0 +1,25 @@ +package supervisord + +import ( + "context" + "fmt" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" +) + +type commandRunner interface { + Run(ctx context.Context, cmd string, args ...string) error +} + +type Reloader struct { + Command []string + Runner commandRunner +} + +func (r Reloader) ReloadConfig(ctx context.Context) error { + if err := r.Runner.Run(ctx, sliceutils.FirstOf(r.Command), r.Command[1:]...); err != nil { + return fmt.Errorf("reload: %w", err) + } + + return nil +} diff --git a/pkg/generators/proxy/const.go b/pkg/generators/proxy/const.go new file mode 100644 index 0000000..0dae1a0 --- /dev/null +++ b/pkg/generators/proxy/const.go @@ -0,0 +1,14 @@ +package proxy + +import ( + "fmt" + "strings" +) + +const pkgDir = "grpc-proxy/pkg" + +// toOriginalPackageName helps to convert modified for +// generation purposes `go_package` to original one. +func toOriginalPackageName(goPackage string) string { + return strings.TrimPrefix(goPackage, fmt.Sprintf("%s/", pkgDir)) +} diff --git a/pkg/generators/proxy/packages.go b/pkg/generators/proxy/packages.go new file mode 100644 index 0000000..c5aa326 --- /dev/null +++ b/pkg/generators/proxy/packages.go @@ -0,0 +1,56 @@ +package proxy + +import ( + "context" + "fmt" + "io" + "path/filepath" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/errgroup" + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +// GeneratePackages runs proto compiler with 'go' and 'go-grpc' +// plugins and places generated golang packages to the proxy template. +func (g proxyGenerator) GeneratePackages(ctx context.Context, contracts contract.SetOfContracts, logs io.Writer) error { + packagesPath := filepath.Join(g.output, "pkg") + return g.generatePackages(ctx, contracts, packagesPath, logs) +} + +func (g proxyGenerator) generatePackages(ctx context.Context, contracts contract.SetOfContracts, output string, logs io.Writer) error { + errG, errCtx := errgroup.WithContext(ctx) + errG.SetLimit(len(contracts)) + + for _, contractToGenerate := range contracts { + contractToGenerate := contractToGenerate + + errG.Go(func() error { + tmpDirForContract, err := afero.TempDir(g.fs, environment.TmpGeneratedPackagesDir, "") + if err != nil { + return fmt.Errorf("create tmp dir for contract '%s': %w", contractToGenerate.HeaderPath, err) + } + + if err = g.compiler.CompileToGo(errCtx, contractToGenerate, tmpDirForContract, logs); err != nil { + return fmt.Errorf("compile '%s': %w", contractToGenerate.HeaderPath, err) + } + + targetPkgDir := filepath.Join(tmpDirForContract, pkgDir) + + if err = fsutils.CopyDir(g.fs, g.fs, targetPkgDir, output, true); err != nil { + return fmt.Errorf("move generated contracts into proxy folder: %w", err) + } + + return nil + }) + } + + if err := errG.Wait(); err != nil { + return fmt.Errorf("compile: %w", err) + } + + return nil +} diff --git a/pkg/generators/proxy/project.go b/pkg/generators/proxy/project.go new file mode 100644 index 0000000..4d84de4 --- /dev/null +++ b/pkg/generators/proxy/project.go @@ -0,0 +1,54 @@ +package proxy + +import ( + "fmt" + "path/filepath" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/static" +) + +const ( + mainGoTemplatePath = "proxy/files/main.go.tpl" + projectTemplateDir = "proxy/template/layout" +) + +// GenerateProject generates golang project based on the template. +// And calls proto compiler to compile packages based on provided proto contracts. +func (g proxyGenerator) GenerateProject(contracts protocontract.SetOfContracts) error { + return g.generateProject(contracts, projectTemplateDir) +} + +func (g proxyGenerator) generateProject(contracts protocontract.SetOfContracts, templatePath string) error { + staticFS := static.FromEmbed() + + if err := fsutils.CopyDir(staticFS, g.fs, templatePath, g.output, false); err != nil { + return fmt.Errorf("copy template: %w", err) + } + + fileToPath := map[string]struct { + path string + substitution interface{} + }{ + "cmd/main.go": { + path: mainGoTemplatePath, + substitution: substitutionForProject(contracts), + }, + } + + for outputFilePath, template := range fileToPath { + content, err := g.renderer.Substitute(template.path, &template.substitution) + if err != nil { + return fmt.Errorf("substitute file: %s, err: %w", template.path, err) + } + + pathInProject := filepath.Join(g.output, outputFilePath) + + if err = fsutils.WriteFile(g.fs, pathInProject, content); err != nil { + return fmt.Errorf("write main.go file: %w", err) + } + } + + return nil +} diff --git a/pkg/generators/proxy/proxy.go b/pkg/generators/proxy/proxy.go new file mode 100644 index 0000000..7f1160e --- /dev/null +++ b/pkg/generators/proxy/proxy.go @@ -0,0 +1,78 @@ +package proxy + +import ( + "context" + "fmt" + "io" + "net/url" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" +) + +// errHasNoContractsWithMethods indicates that provided set of contracts has no methods to mock. +var errHasNoContractsWithMethods = fmt.Errorf("provided contracts has no methods") + +// errHasNoProtoContracts indicates that provided set of contracts is not supported by proxy generator. +var errHasNoProtoContracts = fmt.Errorf("provided contracts are not supported") + +// compiler abstracts how exactly project should be compiled. +type compiler interface { + CompileToGo(context.Context, protocontract.Contract, string, io.Writer) error +} + +// renderer abstracts how exactly project should be rendered. +type renderer interface { + Substitute(string, interface{}) (string, error) +} + +type proxyGenerator struct { + port string + host string + output string + + fs afero.Fs + + renderer + compiler + io.Writer +} + +func NewGenerator(fs afero.Fs, compiler compiler, renderer renderer, baseURL, output string, logs io.Writer) (proxyGenerator, error) { + parsed, err := url.Parse(baseURL) + if err != nil { + return proxyGenerator{}, fmt.Errorf("invalid base url: %w", err) + } + + return proxyGenerator{ + fs: fs, port: parsed.Port(), host: fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host), + output: output, renderer: renderer, compiler: compiler, Writer: logs, + }, nil +} + +// Generate generates project template, grpc packages and stubs. +// The result of generation is ready to 'go run' proxy project. +func (g proxyGenerator) Generate(ctx context.Context, contracts protocontract.SetOfContracts) error { + if !contracts.HasContractsWithMethods() { + return errHasNoContractsWithMethods + } + + if !contracts.HasContractsWithProto() { + return errHasNoProtoContracts + } + + if err := g.GenerateProject(contracts); err != nil { + return fmt.Errorf("generate project: %w", err) + } + + if err := g.GeneratePackages(ctx, contracts, g.Writer); err != nil { + return fmt.Errorf("generate packages: %w", err) + } + + if err := g.GenerateStubs(contracts); err != nil { + return fmt.Errorf("generate stubs: %w", err) + } + + return nil +} diff --git a/pkg/generators/proxy/stubs.go b/pkg/generators/proxy/stubs.go new file mode 100644 index 0000000..9653002 --- /dev/null +++ b/pkg/generators/proxy/stubs.go @@ -0,0 +1,120 @@ +package proxy + +import ( + "fmt" + "path/filepath" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" +) + +const ( + serviceTemplateName = "proxy/files/service.go.tpl" + + unaryMethodTemplateName = "proxy/files/method-unary.go.tpl" + + clientStreamingMethodTemplateName = "proxy/files/method-client-streaming.go.tpl" + + serverStreamingMethodTemplateName = "proxy/files/method-server-streaming.go.tpl" + + bidirectionalStreamingMethodTemplateName = "proxy/files/method-bidirectional-streaming.go.tpl" +) + +// GenerateStubs generates golang grpc stubs based on the template and the provided contracts. +func (g proxyGenerator) GenerateStubs(contracts protocontract.SetOfContracts) error { + return g.generateStubs(contracts) +} + +func (g proxyGenerator) generateStubs(contracts protocontract.SetOfContracts) error { + for _, contractToGenerate := range contracts { + if err := g.generateForContract(contractToGenerate); err != nil { + return fmt.Errorf("generate stubs for contract '%s', err: %w", contractToGenerate.HeaderPath, err) + } + } + + return nil +} + +func (g proxyGenerator) generateForContract(contractToGenerate protocontract.Contract) error { + for _, service := range contractToGenerate.Services { + if err := g.generateForService(contractToGenerate, service); err != nil { + return fmt.Errorf("generate for service '%s', err: %w", service.Name, err) + } + } + + return nil +} + +func (g proxyGenerator) generateForService(contractToGenerate protocontract.Contract, service protocontract.Service) error { + for _, method := range service.Methods { + if err := g.generateForMethod(service, method); err != nil { + return fmt.Errorf("generate for method '%s', err: %w", method.Name, err) + } + } + + templatePath := serviceTemplateName + substitution := substitutionServiceForStubs(contractToGenerate.GoPackage, service.Name) + + content, err := g.renderer.Substitute(templatePath, &substitution) + if err != nil { + return fmt.Errorf("substitute file: %s, err: %w", templatePath, err) + } + + pathInProject := filepath.Join(g.output, serviceFileName(service.GoPackage, service.Name)) + + if err = fsutils.WriteFile(g.fs, pathInProject, content); err != nil { + return fmt.Errorf("write service file: %w", err) + } + + return nil +} + +func (g proxyGenerator) generateForMethod(service protocontract.Service, method protocontract.Method) error { + templateType, err := getMethodTemplate(method.MethodType) + if err != nil { + return fmt.Errorf("get template: %w", err) + } + + substitution, err := substitutionMethodForStubs(service, method, g.host) + if err != nil { + return fmt.Errorf("get substitution: %w", err) + } + + content, err := g.renderer.Substitute(templateType, &substitution) + if err != nil { + return fmt.Errorf("substitute file: %s, err: %w", templateType, err) + } + + pathInProject := filepath.Join(g.output, methodFileName(service.GoPackage, service.Name, method.Name)) + + if err = fsutils.WriteFile(g.fs, pathInProject, content); err != nil { + return fmt.Errorf("write method file: %w", err) + } + + return nil +} + +func getMethodTemplate(methodType types.ProtoMethodType) (string, error) { + switch methodType { + case types.UnaryType: + return unaryMethodTemplateName, nil + case types.ClientSideStreamingType: + return clientStreamingMethodTemplateName, nil + case types.ServerSideStreamingType: + return serverStreamingMethodTemplateName, nil + case types.BidirectionalStreamingType: + return bidirectionalStreamingMethodTemplateName, nil + } + + return "", fmt.Errorf("incorrect method type %s", methodType) +} + +func serviceFileName(packageName, serviceName string) string { + return filepath.Join("internal", toOriginalPackageName(packageName), strutils.ToSnakeCase(serviceName), "service.go") +} + +func methodFileName(packageName, serviceName, methodName string) string { + return filepath.Join("internal", toOriginalPackageName(packageName), strutils.ToSnakeCase(serviceName), strutils.ToSnakeCase(methodName)+".go") +} diff --git a/pkg/generators/proxy/views.go b/pkg/generators/proxy/views.go new file mode 100644 index 0000000..5338ee3 --- /dev/null +++ b/pkg/generators/proxy/views.go @@ -0,0 +1,136 @@ +package proxy + +import ( + "fmt" + "strings" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" +) + +type PackageToService struct { + Service string + + ProtoPackage string + ServicePackage string +} + +// SubstitutionForMainGoView data view for generating 'main.go'. +type SubstitutionForMainGoView struct { + GoPackages []string + OriginalGoPackagesWithService []string + PackageToServices []PackageToService +} + +// SubstitutionMethodForStubsView data view for generating file with grpc method. +type SubstitutionMethodForStubsView struct { + URL string + Method string + Service string + PackageHeader string + Package string + MethodInName string + MethodInPackage string + MethodOutName string + MethodOutPackage string + MethodPackage string + + GoPackages []string +} + +// SubstitutionServiceForStubsView data view for generating file with service. +type SubstitutionServiceForStubsView struct { + Service string + Package string + GoPackage string + + PackageHeader string +} + +func substitutionForProject(contracts contract.SetOfContracts) SubstitutionForMainGoView { + data := SubstitutionForMainGoView{} + + gatherPackageToServices := func(contractDesc contract.Contract) { + for _, service := range contractDesc.Services { + packageName := contractDesc.Package + + data.PackageToServices = append( + data.PackageToServices, + PackageToService{ + Service: strings.Title(service.Name), + ProtoPackage: strutils.ToSnakeCase(packageName), + ServicePackage: strutils.ToSnakeCase(strutils.ToPackageName(contractDesc.GoPackage) + service.Name), + }, + ) + } + } + + gatherOriginalGoPackagesWithService := func(contractDesc contract.Contract) { + for _, service := range contractDesc.Services { + data.OriginalGoPackagesWithService = append( + data.OriginalGoPackagesWithService, + fmt.Sprintf( + "%s/%s", + toOriginalPackageName(contractDesc.GoPackage), + strutils.ToSnakeCase(service.Name), + ), + ) + } + } + + for _, contractDesc := range contracts.WithMethodsOnly() { + gatherPackageToServices(contractDesc) + gatherOriginalGoPackagesWithService(contractDesc) + data.GoPackages = append(data.GoPackages, contractDesc.GoPackage) + } + + data.GoPackages = strutils.UniqueAndSorted(data.GoPackages...) + data.OriginalGoPackagesWithService = strutils.UniqueAndSorted(data.OriginalGoPackagesWithService...) + + return data +} + +func substitutionMethodForStubs(service contract.Service, method contract.Method, baseURL string) (SubstitutionMethodForStubsView, error) { + var goPackages []string + + switch method.MethodType { + case types.UnaryType: + goPackages = strutils.UniqueAndSorted(method.InType.GoPackage, method.OutType.GoPackage) + case types.ServerSideStreamingType: + goPackages = strutils.UniqueAndSorted(method.InType.GoPackage, method.OutType.GoPackage, service.GoPackage) + case types.ClientSideStreamingType, types.BidirectionalStreamingType: + goPackages = strutils.UniqueAndSorted(method.OutType.GoPackage, service.GoPackage) + } + + data := SubstitutionMethodForStubsView{ + GoPackages: goPackages, + Method: method.Name, + Service: service.Name, + MethodInName: method.InType.Name, + MethodOutName: method.OutType.Name, + MethodPackage: strutils.ToSnakeCase(method.Package), + PackageHeader: headerPackage(service.GoPackage, service.Name), + Package: strutils.ToPackageName(service.GoPackage), + MethodInPackage: strutils.ToPackageName(method.InType.GoPackage), + MethodOutPackage: strutils.ToPackageName(method.OutType.GoPackage), + URL: fmt.Sprintf("%s/%s/%s", baseURL, service.Name, method.Name), + } + + return data, nil +} + +func substitutionServiceForStubs(goPackage, serviceName string) SubstitutionServiceForStubsView { + data := SubstitutionServiceForStubsView{ + GoPackage: goPackage, + Service: serviceName, + Package: strutils.ToPackageName(goPackage), + PackageHeader: headerPackage(goPackage, serviceName), + } + + return data +} + +func headerPackage(goPackage, serviceName string) string { + return strutils.ToSnakeCase(strutils.ToPackageName(goPackage) + serviceName) +} diff --git a/pkg/generators/proxy/views_test.go b/pkg/generators/proxy/views_test.go new file mode 100644 index 0000000..f784495 --- /dev/null +++ b/pkg/generators/proxy/views_test.go @@ -0,0 +1,187 @@ +package proxy + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" +) + +func Test_createDataToSubstituteMainGo(t *testing.T) { + tests := []struct { + name string + + contracts contract.SetOfContracts + + wantData SubstitutionForMainGoView + }{ + { + contracts: contract.SetOfContracts{ + contract.Contract{ + Base: contract.Base{ + Package: "example", + GoPackage: "gitlab.com/paas/example", + Services: []contract.Service{ + { + Name: "Example", + Methods: []contract.Method{ + { + Name: "Unary", + Package: "example", + MethodType: types.UnaryType, + }, + }, + }, + }, + }, + }, + contract.Contract{ + Base: contract.Base{ + Package: "example", + GoPackage: "gitlab.com/paas/example", + Services: []contract.Service{ + { + Name: "AnotherExample", + Methods: []contract.Method{ + { + Name: "AnotherUnary", + Package: "example", + MethodType: types.UnaryType, + }, + }, + }, + }, + }, + }, + }, + wantData: SubstitutionForMainGoView{ + OriginalGoPackagesWithService: []string{ + "gitlab.com/paas/example/another_example", + "gitlab.com/paas/example/example", + }, + GoPackages: []string{ + "gitlab.com/paas/example", + }, + PackageToServices: []PackageToService{ + {ProtoPackage: "example", Service: "Example", ServicePackage: "example_example"}, + {ProtoPackage: "example", Service: "AnotherExample", ServicePackage: "example_another_example"}, + }, + }, + }, + { + contracts: contract.SetOfContracts{ + contract.Contract{ + Base: contract.Base{ + Package: "example", + GoPackage: "gitlab.com/paas/example", + Services: []contract.Service{ + { + Name: "Example", + }, + }, + }, + }, + contract.Contract{ + Base: contract.Base{ + Package: "example", + GoPackage: "gitlab.com/paas/example", + Services: []contract.Service{ + { + Name: "AnotherExample", + }, + }, + }, + }, + }, + wantData: SubstitutionForMainGoView{ + OriginalGoPackagesWithService: nil, + GoPackages: nil, + PackageToServices: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := substitutionForProject(tt.contracts) + assert.Equal(t, tt.wantData, got) + }) + } +} + +func Test_substitutionMethodForStubs(t *testing.T) { + tests := []struct { + name string + + method contract.Method + service contract.Service + baseURL string + + want SubstitutionMethodForStubsView + }{ + { + service: contract.Service{ + Name: "Example", + GoPackage: "gitlab.io/paas/example", + }, + baseURL: "http://localhost:8000", + method: contract.Method{ + Name: "Unary", + Package: "example", + InType: contract.MessageType{Name: "CustomEmpty", Package: "import", GoPackage: "gitlab.io/paas/example/import", IsExternal: true}, + OutType: contract.MessageType{Name: "Response", Package: "example", GoPackage: "gitlab.io/paas/example", IsExternal: false}, + MethodType: types.UnaryType, + }, + want: SubstitutionMethodForStubsView{ + Method: "Unary", + MethodInPackage: "import", + Package: "example", + Service: "Example", + MethodPackage: "example", + MethodOutPackage: "example", + MethodOutName: "Response", + MethodInName: "CustomEmpty", + PackageHeader: "example_example", + URL: "http://localhost:8000/Example/Unary", + GoPackages: []string{"gitlab.io/paas/example", "gitlab.io/paas/example/import"}, + }, + }, + { + service: contract.Service{ + Name: "Fancy", + GoPackage: "gitlab.io/paas/example", + }, + baseURL: "http://localhost:8001", + method: contract.Method{ + Name: "SomeFancyName", + Package: "fancy", + InType: contract.MessageType{Name: "CustomEmpty", Package: "another", GoPackage: "gitlab.io/paas/another", IsExternal: true}, + OutType: contract.MessageType{Name: "ResponseFromExternalPackage", Package: "external", GoPackage: "gitlab.io/paas/external", IsExternal: true}, + MethodType: types.UnaryType, + }, + want: SubstitutionMethodForStubsView{ + Service: "Fancy", + MethodPackage: "fancy", + Package: "example", + MethodInPackage: "another", + MethodOutPackage: "external", + MethodInName: "CustomEmpty", + Method: "SomeFancyName", + PackageHeader: "example_fancy", + MethodOutName: "ResponseFromExternalPackage", + URL: "http://localhost:8001/Fancy/SomeFancyName", + GoPackages: []string{"gitlab.io/paas/another", "gitlab.io/paas/external"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := substitutionMethodForStubs(tt.service, tt.method, tt.baseURL) + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/models/basecontract/contract.go b/pkg/models/basecontract/contract.go new file mode 100644 index 0000000..7c8c3c4 --- /dev/null +++ b/pkg/models/basecontract/contract.go @@ -0,0 +1,93 @@ +package basecontract + +import ( + "fmt" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/jhump/protoreflect/desc" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" +) + +type Contract struct { + HeaderPath string + + ImportsPaths []string + + ContractHasMethods bool + + descriptor any +} + +func (c Contract) Descriptor() any { + return c.descriptor +} + +func ProtoFromAny(contract Contract) (*desc.FileDescriptor, error) { + descriptor, ok := contract.Descriptor().(*desc.FileDescriptor) + if !ok { + return nil, fmt.Errorf("no valid descriptor") + } + + return descriptor, nil +} + +func OpenAPIFromAny(contract Contract) (*openapi3.T, error) { + descriptor, ok := contract.Descriptor().(*openapi3.T) + if !ok { + return nil, fmt.Errorf("no valid descriptor") + } + + return descriptor, nil +} + +func (c Contract) HasMethods() bool { + return c.ContractHasMethods +} + +func (c Contract) IsProtoContract() bool { + return fsutils.GetFileExt(c.HeaderPath) == "proto" +} + +func (c Contract) IsOpenAPIContract() bool { + return fsutils.GetFileExt(c.HeaderPath) == "yaml" +} + +// SetOfContracts is just alias for several contracts. +type SetOfContracts []Contract + +// WithMethodsOnly returns only contracts with methods. +func (set SetOfContracts) WithMethodsOnly() SetOfContracts { + var filtered SetOfContracts + + for _, contract := range set { + if contract.HasMethods() { + filtered = append(filtered, contract) + } + } + + return filtered +} + +func (set SetOfContracts) HasContractsWithMethods() bool { + for _, contract := range set { + if contract.HasMethods() { + return true + } + } + + return false +} + +func (set SetOfContracts) FileType() types.SourceFileType { + switch { + case sliceutils.FirstOf(set).IsProtoContract(): + return types.ProtoType + case sliceutils.FirstOf(set).IsOpenAPIContract(): + return types.OpenAPIType + default: + return types.UnknownFileType + } +} diff --git a/pkg/models/basecontract/from_openapi.go b/pkg/models/basecontract/from_openapi.go new file mode 100644 index 0000000..88906b8 --- /dev/null +++ b/pkg/models/basecontract/from_openapi.go @@ -0,0 +1,23 @@ +package basecontract + +import ( + "fmt" + + "github.com/getkin/kin-openapi/openapi3" +) + +func FromOpenAPIDescriptor(descriptor *openapi3.T, path string) (Contract, error) { + if descriptor == nil { + return Contract{}, fmt.Errorf("descriptor is nil") + } + + contract := Contract{ + HeaderPath: path, + + ContractHasMethods: len(descriptor.Paths) != 0, + + descriptor: descriptor, + } + + return contract, nil +} diff --git a/pkg/models/basecontract/from_proto.go b/pkg/models/basecontract/from_proto.go new file mode 100644 index 0000000..698181d --- /dev/null +++ b/pkg/models/basecontract/from_proto.go @@ -0,0 +1,73 @@ +package basecontract + +import ( + "fmt" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/blacklist" + "path/filepath" + + "github.com/jhump/protoreflect/desc" +) + +// FromProtoDescriptor converts proto descriptor into Contract model. +func FromProtoDescriptor(descriptor *desc.FileDescriptor, protoPath string) (Contract, error) { + if descriptor == nil { + return Contract{}, fmt.Errorf("descriptor is nil") + } + + contract := Contract{ + descriptor: descriptor, + ContractHasMethods: hasMethods(descriptor), + HeaderPath: filepath.Join(protoPath, descriptor.GetName()), + + ImportsPaths: protoImports(descriptor, protoPath), + } + + return contract, nil +} + +func protoImports(descriptor *desc.FileDescriptor, protoPath string) []string { + var imports []string + + for _, path := range getImports(descriptor) { + if blacklist.IsDeliveredWithProtoc(path) { + continue + } + + if blacklist.IsGoogleAPIContract(path) { + continue + } + + imports = append(imports, filepath.Join(protoPath, path)) + } + + return imports +} + +func getImports(descriptor *desc.FileDescriptor) []string { + var all []string + traverse(descriptor, &all) + + var paths []string + for _, path := range all { + paths = append(paths, path) + } + + return paths +} + +func traverse(desc *desc.FileDescriptor, all *[]string) { + for _, descriptor := range desc.GetDependencies() { + *all = append(*all, descriptor.GetName()) + traverse(descriptor, all) + } +} + +func hasMethods(descriptor *desc.FileDescriptor) bool { + for _, service := range descriptor.GetServices() { + if len(service.GetMethods()) > 0 { + return true + } + } + + return false +} diff --git a/pkg/models/basecontract/loader/load.go b/pkg/models/basecontract/loader/load.go new file mode 100644 index 0000000..35680d0 --- /dev/null +++ b/pkg/models/basecontract/loader/load.go @@ -0,0 +1,64 @@ +package loader + +import ( + "fmt" + + "github.com/spf13/afero" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract/loader/openapi" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract/loader/proto" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" +) + +// contractLoader provides specific loader. For proto and OpenAPI contracts. +type contractLoader interface { + LoadDir() (contract.SetOfContracts, error) + LoadFile() (contract.SetOfContracts, error) +} + +// contractSourcer provides validated source for requested contract/dir with contracts. +type contractSourcer interface { + SourceInputs() []string + SourceLoadType() types.SourceLoadType + SourceFileType() types.SourceFileType +} + +// Load allows to load contracts from provided source as `SetOfContracts`. +func Load(fs afero.Fs, source contractSourcer) (contracts contract.SetOfContracts, err error) { + var ( + specificLoader = getLoader(fs, source) + sourceType = source.SourceLoadType() + ) + + switch sourceType { + case types.SourceDirType: + contracts, err = specificLoader.LoadDir() + if err != nil { + return nil, fmt.Errorf("load contracts from dir: %w", err) + } + + case types.SourceSingleType: + contracts, err = specificLoader.LoadFile() + if err != nil { + return nil, fmt.Errorf("load contracts from file: %w", err) + } + + default: + return nil, fmt.Errorf("incorrect source type: %v", sourceType) + } + + return contracts, nil +} + +func getLoader(fs afero.Fs, s contractSourcer) contractLoader { + switch s.SourceFileType() { + case types.ProtoType: + return proto.NewLoader(fs, s) + + case types.OpenAPIType: + return openapi.NewLoader(fs, s) + } + + return nil +} diff --git a/pkg/models/basecontract/loader/openapi/openapi.go b/pkg/models/basecontract/loader/openapi/openapi.go new file mode 100644 index 0000000..0fe79e4 --- /dev/null +++ b/pkg/models/basecontract/loader/openapi/openapi.go @@ -0,0 +1,49 @@ +package openapi + +import ( + "fmt" + + "github.com/spf13/afero" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract/parser/openapi" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" +) + +type contractSourcer interface { + SourceInputs() []string + SourceLoadType() types.SourceLoadType + SourceFileType() types.SourceFileType +} + +type loader struct { + fs afero.Fs + source contractSourcer +} + +func NewLoader(fs afero.Fs, source contractSourcer) loader { + return loader{fs: fs, source: source} +} + +func (l loader) LoadFile() (contract.SetOfContracts, error) { + oaFiles := l.source.SourceInputs() + + contracts, err := openapi.ParseOpenAPIFiles(oaFiles) + if err != nil { + return nil, fmt.Errorf("parse openapi file: %w", err) + } + + return contracts, nil +} + +func (l loader) LoadDir() (contract.SetOfContracts, error) { + oaFiles := l.source.SourceInputs() + + contracts, err := openapi.ParseOpenAPIDir(l.fs, sliceutils.FirstOf(oaFiles)) + if err != nil { + return nil, fmt.Errorf("parse openapi dir: %w", err) + } + + return contracts, nil +} diff --git a/pkg/models/basecontract/loader/proto/proto.go b/pkg/models/basecontract/loader/proto/proto.go new file mode 100644 index 0000000..0d02313 --- /dev/null +++ b/pkg/models/basecontract/loader/proto/proto.go @@ -0,0 +1,48 @@ +package proto + +import ( + "fmt" + + "github.com/spf13/afero" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract/parser/proto" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" +) + +type contractSourcer interface { + SourceInputs() []string + SourceLoadType() types.SourceLoadType + SourceFileType() types.SourceFileType +} + +type loader struct { + fs afero.Fs + source contractSourcer +} + +func NewLoader(fs afero.Fs, source contractSourcer) loader { + return loader{fs: fs, source: source} +} + +func (l loader) LoadFile() (contract.SetOfContracts, error) { + protoFiles := l.source.SourceInputs() + + contracts, err := proto.ParseProtoFiles(protoFiles) + if err != nil { + return nil, fmt.Errorf("parse proto file: %w", err) + } + + return contracts, nil +} + +func (l loader) LoadDir() (contract.SetOfContracts, error) { + protoPaths := l.source.SourceInputs() + + contracts, err := proto.ParseProtoDir(l.fs, protoPaths) + if err != nil { + return nil, fmt.Errorf("parse proto dir: %w", err) + } + + return contracts, nil +} diff --git a/pkg/models/basecontract/parser/openapi/openapi.go b/pkg/models/basecontract/parser/openapi/openapi.go new file mode 100644 index 0000000..68d28d7 --- /dev/null +++ b/pkg/models/basecontract/parser/openapi/openapi.go @@ -0,0 +1,111 @@ +package openapi + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/spf13/afero" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" +) + +func ParseOpenAPIFiles(openapiFile []string) (contract.SetOfContracts, error) { + var contracts contract.SetOfContracts + + contractPath := sliceutils.FirstOf(openapiFile) + + descriptor, err := parse(contractPath) + if err != nil { + return nil, fmt.Errorf("parse openapi: %w", err) + } + + convertedContract, errConvert := contract.FromOpenAPIDescriptor(descriptor, contractPath) + if errConvert != nil { + return nil, fmt.Errorf("convert openapi: %w", errConvert) + } + + contracts = append(contracts, convertedContract) + + return contracts, nil +} + +func ParseOpenAPIDir(fs afero.Fs, openapiPath string) (contract.SetOfContracts, error) { + var contracts contract.SetOfContracts + + files, err := fsutils.GatherMatchedEntriesInDir(fs, openapiPath, onlyOpenAPIFiles) + if err != nil { + return nil, fmt.Errorf("get valid files: %w", err) + } + + for _, openapiFile := range files { + descriptor, err := parse(openapiFile) + if err != nil { + return nil, fmt.Errorf("parse openapi: %w", err) + } + + convertedContract, errConvert := contract.FromOpenAPIDescriptor(descriptor, openapiFile) + if errConvert != nil { + return nil, fmt.Errorf("convert openapi: %w", errConvert) + } + + contracts = append(contracts, convertedContract) + } + + return contracts, nil +} + +func parse(openapiFile string) (*openapi3.T, error) { + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + + descriptor, err := loader.LoadFromFile(openapiFile) + if err != nil { + return nil, fmt.Errorf("load from data '%s': %w", openapiFile, err) + } + + ctx := context.Background() + contractPath := filepath.Dir(openapiFile) + internalizeF := createInternalizeF(contractPath) + + descriptor.InternalizeRefs(ctx, internalizeF) + + return descriptor, nil +} + +func createInternalizeF(contractPath string) func(string) string { + return func(ref string) string { + if ref == "" { + return "" + } + + split := strings.SplitN(ref, "#", 2) + if len(split) == 2 { + return filepath.Base(split[1]) + } + + ref = sliceutils.FirstOf(split) + + for ext := filepath.Ext(ref); ext != ""; ext = filepath.Ext(ref) { + ref = strings.TrimSuffix(ref, ext) + } + + ref = strings.TrimPrefix(strings.TrimPrefix(ref, contractPath), "/") + + return strutils.ToCamelCase(ref) + } +} + +func onlyOpenAPIFiles(info os.FileInfo) bool { + if !info.IsDir() && + strings.Contains(info.Name(), ".yaml") { + return true + } + return false +} diff --git a/pkg/models/basecontract/parser/proto/proto.go b/pkg/models/basecontract/parser/proto/proto.go new file mode 100644 index 0000000..1508986 --- /dev/null +++ b/pkg/models/basecontract/parser/proto/proto.go @@ -0,0 +1,103 @@ +package proto + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/desc/protoparse" + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" +) + +var ProtoPaths = []string{ + environment.TmpWellKnownProtosDir, + environment.TmpAnnotationProtosDir, +} + +func ParseProtoFiles(protoFiles []string) (basecontract.SetOfContracts, error) { + contractProtoPath := filepath.Dir(sliceutils.FirstOf(protoFiles)) + + protoPaths := []string{contractProtoPath} + protoPaths = append(protoPaths, ProtoPaths...) + + descriptors, err := parse(protoPaths, protoFiles...) + if err != nil { + return nil, fmt.Errorf("parse proto: %w", err) + } + + var contracts basecontract.SetOfContracts + + for _, descriptor := range descriptors { + convertedContract, errConvert := basecontract.FromProtoDescriptor(descriptor, contractProtoPath) + if errConvert != nil { + return nil, fmt.Errorf("convert proto: %w", errConvert) + } + + contracts = append(contracts, convertedContract) + } + + return contracts, nil +} + +func ParseProtoDir(fs afero.Fs, protoPaths []string) (basecontract.SetOfContracts, error) { + var contracts basecontract.SetOfContracts + for _, protoPath := range protoPaths { + files, err := fsutils.GatherMatchedEntriesInDir(fs, protoPath, onlyProtoFiles) + if err != nil { + return nil, fmt.Errorf("get valid files: %w", err) + } + + protoPaths := []string{protoPath} + protoPaths = append(protoPaths, ProtoPaths...) + + descriptors, err := parse(protoPaths, files...) + if err != nil { + return nil, fmt.Errorf("parse proto: %w", err) + } + + for _, descriptor := range descriptors { + convertedContract, errConvert := basecontract.FromProtoDescriptor(descriptor, protoPath) + if errConvert != nil { + return nil, fmt.Errorf("convert proto: %w", errConvert) + } + + contracts = append(contracts, convertedContract) + } + } + + return contracts, nil +} + +func parse(protoPaths []string, protoFiles ...string) ([]*desc.FileDescriptor, error) { + resolvedHeaders, err := protoparse.ResolveFilenames(protoPaths, protoFiles...) + if err != nil { + return nil, fmt.Errorf("resolve names: %w", err) + } + + parser := &protoparse.Parser{ + IncludeSourceCodeInfo: true, + ImportPaths: protoPaths, + } + + descriptors, err := parser.ParseFiles(resolvedHeaders...) + if err != nil { + return nil, fmt.Errorf("parse file: %w", err) + } + + return descriptors, nil +} + +func onlyProtoFiles(info os.FileInfo) bool { + if !info.IsDir() && + strings.Contains(info.Name(), ".proto") { + return true + } + return false +} diff --git a/pkg/models/basecontract/traverser/traverse.go b/pkg/models/basecontract/traverser/traverse.go new file mode 100644 index 0000000..c0426b1 --- /dev/null +++ b/pkg/models/basecontract/traverser/traverse.go @@ -0,0 +1,32 @@ +package traverser + +import ( + "fmt" + + "github.com/jhump/protoreflect/desc" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" +) + +func Descriptors(contracts contract.SetOfContracts) ([]*desc.FileDescriptor, error) { + var all []*desc.FileDescriptor + + for _, cont := range contracts { + descriptor, err := contract.ProtoFromAny(cont) + if err != nil { + return nil, fmt.Errorf("from any: %w", err) + } + + all = append(all, descriptor) + rec(descriptor, &all) + } + + return all, nil +} + +func rec(desc *desc.FileDescriptor, all *[]*desc.FileDescriptor) { + *all = append(*all, desc.GetDependencies()...) + for _, descriptor := range desc.GetDependencies() { + rec(descriptor, all) + } +} diff --git a/pkg/models/basecontract/unifier/openapi/openapi.go b/pkg/models/basecontract/unifier/openapi/openapi.go new file mode 100644 index 0000000..b4d2f35 --- /dev/null +++ b/pkg/models/basecontract/unifier/openapi/openapi.go @@ -0,0 +1,51 @@ +package openapi + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/spf13/afero" + "gopkg.in/yaml.v3" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +type openapiUnifier struct { + fs afero.Fs +} + +func NewUnifier(fs afero.Fs) *openapiUnifier { + return &openapiUnifier{fs: fs} +} + +func (o *openapiUnifier) Unify(ctx context.Context, contracts contract.SetOfContracts, path string) error { + for _, contractToUnify := range contracts { + if err := o.saveFromDescriptor(contractToUnify, path); err != nil { + return fmt.Errorf("save from descriptor: %w", err) + } + } + + return nil +} + +func (o *openapiUnifier) saveFromDescriptor(contractToUnify contract.Contract, path string) error { + descriptor, err := contract.OpenAPIFromAny(contractToUnify) + if err != nil { + return fmt.Errorf("get openapi descriptor: %w", err) + } + + content, err := yaml.Marshal(descriptor) + if err != nil { + return fmt.Errorf("marshal contract %s: %w", contractToUnify.HeaderPath, err) + } + + pathToSave := filepath.Join(path, filepath.Base(contractToUnify.HeaderPath)) + + if err = fsutils.WriteFile(o.fs, pathToSave, string(content)); err != nil { + return fmt.Errorf("write unified contract %s: %w", contractToUnify.HeaderPath, err) + } + + return nil +} diff --git a/pkg/models/basecontract/unifier/proto/proto.go b/pkg/models/basecontract/unifier/proto/proto.go new file mode 100644 index 0000000..d2478b9 --- /dev/null +++ b/pkg/models/basecontract/unifier/proto/proto.go @@ -0,0 +1,140 @@ +package proto + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/builder" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/builder/updaters" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/errgroup" + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract/loader" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract/traverser" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/printer" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +type compiler interface { + CompileToOpenAPI(context.Context, contract.Contract, string, io.Writer) error +} + +type protoUnifier struct { + fs afero.Fs + + logs io.Writer + + compiler +} + +func NewUnifier(fs afero.Fs, compiler compiler, logs io.Writer) *protoUnifier { + return &protoUnifier{fs: fs, compiler: compiler, logs: logs} +} + +func (o *protoUnifier) Unify(ctx context.Context, contracts contract.SetOfContracts, output string) error { + tmpPath := environment.TmpOverwrittenContractsDir + + tmpDirForContracts, err := afero.TempDir(o.fs, tmpPath, "") + if err != nil { + return fmt.Errorf("create tmp dir: %w", err) + } + + goPackageUpdater := updaters.NewGoPackageUpdater() + + optionUpdater, err := updaters.NewOptionUpdater() + if err != nil { + return fmt.Errorf("create option updater: %w", err) + } + + descriptors, err := traverser.Descriptors(contracts) + if err != nil { + return fmt.Errorf("get descriptors: %w", err) + } + + updatedDescriptors, err := builder.UpdateContracts(descriptors, goPackageUpdater, optionUpdater) + if err != nil { + return fmt.Errorf("overwrite: %w", err) + } + + if err = printer.Print(updatedDescriptors, tmpDirForContracts); err != nil { + return fmt.Errorf("print: %w", err) + } + + sourcerInstance, err := sourcer.New(o.fs, tmpDirForContracts, types.ProtoType) + if err != nil { + return fmt.Errorf("create sourcer for overwritten proto contracts: %w", err) + } + + updatedContracts, err := loader.Load(o.fs, sourcerInstance) + if err != nil { + return fmt.Errorf("load overwritten proto contracts: %w", err) + } + + errG, errCtx := errgroup.WithContext(ctx) + errG.SetLimit(len(updatedContracts)) + + for _, contractToCompile := range updatedContracts { + contractToCompile := contractToCompile + + errG.Go(func() error { + saveTo, err := o.createTmpDir(contractToCompile, output) + if err != nil { + return fmt.Errorf("create tmp dir: %w", err) + } + + if err = o.CompileToOpenAPI(errCtx, contractToCompile, saveTo, o.logs); err != nil { + return fmt.Errorf("compile: %w", err) + } + + return nil + }) + } + + if err = errG.Wait(); err != nil { + return fmt.Errorf("wait: %w", err) + } + + if err = o.replaceFromTmpDirs(output); err != nil { + return fmt.Errorf("replace: %w", err) + } + + return nil +} + +func (o *protoUnifier) createTmpDir(contract contract.Contract, output string) (string, error) { + saveTo := filepath.Join(output, strings.TrimSuffix(filepath.Base(contract.HeaderPath), ".proto")) + + if err := o.fs.MkdirAll(saveTo, os.ModePerm); err != nil { + return "", fmt.Errorf("mkdirall: %w", err) + } + + return saveTo, nil +} + +func (o *protoUnifier) replaceFromTmpDirs(output string) error { + entries, err := fsutils.GatherEntries(o.fs, output) + if err != nil { + return fmt.Errorf("gather entries: %w", err) + } + + var idx int + for _, entry := range entries { + if !entry.IsDir() && entry.Name() == "openapi.yaml" { + newPath := filepath.Join(output, fmt.Sprintf("openapi%d.yaml", idx)) + if err := o.fs.Rename(entry.Path, newPath); err != nil { + return fmt.Errorf("rename: %w", err) + } + idx++ + } + } + + return nil +} diff --git a/pkg/models/basecontract/unifier/unify.go b/pkg/models/basecontract/unifier/unify.go new file mode 100644 index 0000000..acb85df --- /dev/null +++ b/pkg/models/basecontract/unifier/unify.go @@ -0,0 +1,59 @@ +package unifier + +import ( + "context" + "fmt" + "io" + + "github.com/spf13/afero" + + contract "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract/unifier/openapi" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/basecontract/unifier/proto" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" +) + +type unifier interface { + Unify(context.Context, contract.SetOfContracts, string) error +} + +type compiler interface { + CompileToOpenAPI(context.Context, contract.Contract, string, io.Writer) error +} + +type baseUnifier struct { + fs afero.Fs + + compiler compiler + + logs io.Writer +} + +func NewUnifier(fs afero.Fs, compiler compiler, logs io.Writer) *baseUnifier { + return &baseUnifier{fs: fs, logs: logs, compiler: compiler} +} + +func (u *baseUnifier) Unify(ctx context.Context, contracts contract.SetOfContracts, path string) error { + specificUnifier, err := u.getUnifier(u.fs, contracts, u.compiler) + if err != nil { + return fmt.Errorf("get unifier: %w", err) + } + + if err := specificUnifier.Unify(ctx, contracts, path); err != nil { + return fmt.Errorf("unify: %w", err) + } + + return nil +} + +func (u *baseUnifier) getUnifier(fs afero.Fs, contracts contract.SetOfContracts, compiler compiler) (unifier, error) { + switch contracts.FileType() { + case types.ProtoType: + return proto.NewUnifier(fs, compiler, u.logs), nil + + case types.OpenAPIType: + return openapi.NewUnifier(fs), nil + } + + return nil, fmt.Errorf("no available unifier") +} diff --git a/pkg/models/protocontract/contract.go b/pkg/models/protocontract/contract.go new file mode 100644 index 0000000..d631d70 --- /dev/null +++ b/pkg/models/protocontract/contract.go @@ -0,0 +1,104 @@ +package protocontract + +import ( + "github.com/jhump/protoreflect/desc" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +type Base struct { + Package string + GoPackage string + HeaderPath string + + Messages []string + ImportsPaths []string + + Services []Service +} + +// Contract represents contract files with scope. +// +// `HeaderPath` stores main contract path. +// `[]ImportsPaths` stores paths of contracts which was imported in the main contract file. +type Contract struct { + Base + + Desc *desc.FileDescriptor +} + +type MessageType struct { + Name string + Package string + GoPackage string + + IsExternal bool +} + +type Method struct { + Name string + Package string + + InType MessageType + OutType MessageType + + MethodType types.ProtoMethodType +} + +type Service struct { + Name string + GoPackage string + + Methods []Method +} + +func (c Contract) HasMethods() bool { + for _, service := range c.Services { + if len(service.Methods) > 0 { + return true + } + } + + return false +} + +func (c Contract) IsProtoContract() bool { + return fsutils.GetFileExt(c.HeaderPath) == "proto" +} + +// SetOfContracts is just alias for several contracts. +type SetOfContracts []Contract + +// WithMethodsOnly returns only contracts with methods. +func (set SetOfContracts) WithMethodsOnly() SetOfContracts { + var filtered SetOfContracts + + for _, contract := range set { + if contract.HasMethods() { + filtered = append(filtered, contract) + } + } + + return filtered +} + +func (set SetOfContracts) HasContractsWithMethods() bool { + for _, contract := range set { + if contract.HasMethods() { + return true + } + } + + return false +} + +func (set SetOfContracts) HasContractsWithProto() bool { + for _, contract := range set { + if contract.IsProtoContract() { + return true + } + } + + return false +} diff --git a/pkg/models/protocontract/contract_test.go b/pkg/models/protocontract/contract_test.go new file mode 100644 index 0000000..e0fb8ff --- /dev/null +++ b/pkg/models/protocontract/contract_test.go @@ -0,0 +1,48 @@ +package protocontract + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" +) + +func TestSourceFileTypeFromPath(t *testing.T) { + tests := []struct { + name string + path string + want types.SourceFileType + }{ + {path: "/some/test/path/file.proto", want: types.ProtoType}, + {path: "/some/test/path/file.yaml", want: types.OpenAPIType}, + {path: "/some/test/path/file.yml", want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := types.SourceFileTypeFromPath(tt.path) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSourceFileType_Is(t *testing.T) { + tests := []struct { + name string + s types.SourceFileType + that string + want bool + }{ + {s: types.OpenAPIType, that: "yaml", want: true}, + {s: types.ProtoType, that: "proto", want: true}, + {s: types.OpenAPIType, that: "yml", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.s.Is(tt.that) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/models/protocontract/from_proto.go b/pkg/models/protocontract/from_proto.go new file mode 100644 index 0000000..4036bf4 --- /dev/null +++ b/pkg/models/protocontract/from_proto.go @@ -0,0 +1,143 @@ +package protocontract + +import ( + "fmt" + "path/filepath" + + "github.com/jhump/protoreflect/desc" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/blacklist" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" +) + +// FromProtoDescriptor converts proto descriptor into Contract model. +func FromProtoDescriptor(descriptor *desc.FileDescriptor, protoPath string) (Contract, error) { + goPackage := protoGoPackage(descriptor) + + services, err := protoServices(descriptor) + if err != nil { + return Contract{}, fmt.Errorf("get services: %w", err) + } + + base := Base{ + Services: services, + GoPackage: goPackage, + Package: descriptor.GetPackage(), + Messages: protoMessages(descriptor), + ImportsPaths: protoImports(descriptor, protoPath), + HeaderPath: filepath.Join(protoPath, descriptor.GetName()), + } + + contract := Contract{ + Base: base, + Desc: descriptor, + } + + return contract, nil +} + +func protoGoPackage(descriptor *desc.FileDescriptor) string { + if descriptor.GetFileOptions() == nil { + return "" + } + + goPackage := descriptor.GetFileOptions().GoPackage + + if goPackage == nil { + return "" + } + + return *goPackage +} + +func protoImports(descriptor *desc.FileDescriptor, protoPath string) []string { + var imports []string + + for _, path := range getImports(descriptor) { + if blacklist.IsDeliveredWithProtoc(path) { + continue + } + + imports = append(imports, filepath.Join(protoPath, path)) + } + + return imports +} + +func getImports(descriptor *desc.FileDescriptor) []string { + var all []string + traverse(descriptor, &all) + + var paths []string + for _, path := range all { + paths = append(paths, path) + } + + return paths +} + +func traverse(desc *desc.FileDescriptor, all *[]string) { + for _, descriptor := range desc.GetDependencies() { + *all = append(*all, descriptor.GetName()) + traverse(descriptor, all) + } +} + +func protoServices(descriptor *desc.FileDescriptor) ([]Service, error) { + var services []Service + + for _, service := range descriptor.GetServices() { + var methods []Method + + for _, method := range service.GetMethods() { + in, err := protoMessageType(method, method.GetInputType()) + if err != nil { + return nil, fmt.Errorf("message-in type: %w", err) + } + + out, err := protoMessageType(method, method.GetOutputType()) + if err != nil { + return nil, fmt.Errorf("message-out type: %w", err) + } + + methods = append(methods, Method{ + InType: in, + OutType: out, + Name: method.GetName(), + Package: method.GetFile().GetPackage(), + MethodType: types.MethodType(method.IsClientStreaming(), method.IsServerStreaming()), + }) + } + + services = append(services, Service{ + Methods: methods, + Name: service.GetName(), + GoPackage: protoGoPackage(descriptor), + }) + } + + return services, nil +} + +func protoMessages(descriptor *desc.FileDescriptor) []string { + var messages []string + + for _, message := range descriptor.GetMessageTypes() { + messages = append(messages, message.GetName()) + } + + return messages +} + +func protoMessageType(methodDesc *desc.MethodDescriptor, messageDesc *desc.MessageDescriptor) (MessageType, error) { + goPackage := protoGoPackage(messageDesc.GetFile()) + + message := MessageType{ + GoPackage: goPackage, + Name: messageDesc.GetName(), + Package: messageDesc.GetFile().GetPackage(), + IsExternal: messageDesc.GetFile().GetPackage() != methodDesc.GetFile().GetPackage(), + } + + return message, nil +} diff --git a/pkg/models/protocontract/loader/load.go b/pkg/models/protocontract/loader/load.go new file mode 100644 index 0000000..66096df --- /dev/null +++ b/pkg/models/protocontract/loader/load.go @@ -0,0 +1,63 @@ +package loader + +import ( + "fmt" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" +) + +// contractLoader provides specific loader. For proto and OpenAPI contracts. +type contractLoader interface { + LoadDir() (protocontract.SetOfContracts, error) + LoadFile() (protocontract.SetOfContracts, error) +} + +// contractSourcer provides validated source for requested contract/dir with contracts. +type contractSourcer interface { + SourceInputs() []string + SourceLoadType() types.SourceLoadType + SourceFileType() types.SourceFileType +} + +var unsupportedLoaderErr = fmt.Errorf("loader is not supported") + +// Load allows to load contracts from provided source as `SetOfContracts`. +func Load(fs afero.Fs, source contractSourcer) (contracts protocontract.SetOfContracts, err error) { + sourceType := source.SourceLoadType() + + specificLoader, err := getLoader(fs, source) + if err != nil { + return nil, fmt.Errorf("get loader: %w", err) + } + + switch sourceType { + case types.SourceDirType: + contracts, err = specificLoader.LoadDir() + if err != nil { + return nil, fmt.Errorf("load contracts from dir: %w", err) + } + + case types.SourceSingleType: + contracts, err = specificLoader.LoadFile() + if err != nil { + return nil, fmt.Errorf("load contracts from file: %w", err) + } + + default: + return nil, fmt.Errorf("incorrect source type: %v", sourceType) + } + + return contracts, nil +} + +func getLoader(fs afero.Fs, s contractSourcer) (contractLoader, error) { + switch s.SourceFileType() { + case types.ProtoType: + return NewLoader(fs, s), nil + default: + return nil, unsupportedLoaderErr + } +} diff --git a/pkg/models/protocontract/loader/load_test.go b/pkg/models/protocontract/loader/load_test.go new file mode 100644 index 0000000..7b54313 --- /dev/null +++ b/pkg/models/protocontract/loader/load_test.go @@ -0,0 +1,116 @@ +package loader + +import ( + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +var projectPath = filepath.Join(fsutils.CurrentDir(), "../../../..") + +var ( + osfs = afero.NewOsFs() +) + +func Test_loader_loadFromDir(t *testing.T) { + tests := []struct { + name string + + path string + fs afero.Fs + contractType types.SourceFileType + + want protocontract.SetOfContracts + wantErr error + }{ + { + fs: osfs, contractType: types.ProtoType, + path: filepath.Join(projectPath, "static/tests/data/static-examples/with-local-imports-as-reciever"), + want: protocontract.SetOfContracts{ + protocontract.Contract{ + Base: struct { + Package string + GoPackage string + HeaderPath string + Messages []string + ImportsPaths []string + Services []protocontract.Service + }{ + Package: "example", + GoPackage: "gitlab.io/paas/example", + HeaderPath: filepath.Join(projectPath, "static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/example.proto"), + ImportsPaths: []string{filepath.Join(projectPath, "static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/local/custom.proto")}, + Messages: []string{"Request", "Response"}, + Services: []protocontract.Service{ + { + Name: "Example", + GoPackage: "gitlab.io/paas/example", + Methods: []protocontract.Method{ + { + Name: "Unary", + Package: "example", + InType: protocontract.MessageType{Name: "CustomEmpty", Package: "local", GoPackage: "gitlab.io/paas/example/local", IsExternal: true}, + OutType: protocontract.MessageType{Name: "Response", Package: "example", GoPackage: "gitlab.io/paas/example", IsExternal: false}, + MethodType: types.UnaryType, + }, + { + Name: "ClientSideStream", + Package: "example", + InType: protocontract.MessageType{Name: "Request", Package: "example", GoPackage: "gitlab.io/paas/example", IsExternal: false}, + OutType: protocontract.MessageType{Name: "Response", Package: "example", GoPackage: "gitlab.io/paas/example", IsExternal: false}, + MethodType: types.ClientSideStreamingType, + }, + { + Name: "ServerSideStream", + Package: "example", + InType: protocontract.MessageType{Name: "Request", Package: "example", GoPackage: "gitlab.io/paas/example", IsExternal: false}, + OutType: protocontract.MessageType{Name: "Response", Package: "example", GoPackage: "gitlab.io/paas/example", IsExternal: false}, + MethodType: types.ServerSideStreamingType, + }, + { + Name: "BidirectionalStream", + Package: "example", + InType: protocontract.MessageType{Name: "Request", Package: "example", GoPackage: "gitlab.io/paas/example", IsExternal: false}, + OutType: protocontract.MessageType{Name: "Response", Package: "example", GoPackage: "gitlab.io/paas/example", IsExternal: false}, + MethodType: types.BidirectionalStreamingType, + }, + }, + }, + }, + }, + }, + }, + wantErr: nil, + }, + { + fs: osfs, contractType: types.OpenAPIType, + path: filepath.Join(projectPath, "static/tests/data/openapi/petstore"), + want: nil, + wantErr: unsupportedLoaderErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sourcerInstance, err := sourcer.New(tt.fs, tt.path, tt.contractType) + assert.NoError(t, err) + + contracts, err := Load(tt.fs, sourcerInstance) + assert.ErrorIs(t, err, tt.wantErr) + + var contractsToCompare protocontract.SetOfContracts + for _, con := range contracts { + contractsToCompare = append(contractsToCompare, protocontract.Contract{Base: con.Base}) + } + + assert.Equal(t, tt.want, contractsToCompare) + }) + } +} diff --git a/pkg/models/protocontract/loader/proto.go b/pkg/models/protocontract/loader/proto.go new file mode 100644 index 0000000..856d316 --- /dev/null +++ b/pkg/models/protocontract/loader/proto.go @@ -0,0 +1,42 @@ +package loader + +import ( + "fmt" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract/parser" +) + +type loader struct { + fs afero.Fs + + source contractSourcer +} + +func NewLoader(fs afero.Fs, source contractSourcer) loader { + return loader{fs: fs, source: source} +} + +func (l loader) LoadFile() (protocontract.SetOfContracts, error) { + protoFiles := l.source.SourceInputs() + + contracts, err := parser.ParseProtoFiles(protoFiles) + if err != nil { + return nil, fmt.Errorf("parse proto file: %w", err) + } + + return contracts, nil +} + +func (l loader) LoadDir() (protocontract.SetOfContracts, error) { + protoPaths := l.source.SourceInputs() + + contracts, err := parser.ParseProtoDir(l.fs, protoPaths) + if err != nil { + return nil, fmt.Errorf("parse proto dir: %w", err) + } + + return contracts, nil +} diff --git a/pkg/models/protocontract/parser/proto.go b/pkg/models/protocontract/parser/proto.go new file mode 100644 index 0000000..28696ba --- /dev/null +++ b/pkg/models/protocontract/parser/proto.go @@ -0,0 +1,93 @@ +package parser + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/desc/protoparse" + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/environment" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" +) + +func ParseProtoFiles(protoFiles []string) (protocontract.SetOfContracts, error) { + var contracts protocontract.SetOfContracts + + protoPath := filepath.Dir(sliceutils.FirstOf(protoFiles)) + + descriptors, err := parse([]string{protoPath, environment.TmpWellKnownProtosDir}, protoFiles...) + if err != nil { + return nil, fmt.Errorf("parse proto: %w", err) + } + + for _, descriptor := range descriptors { + convertedContract, errConvert := protocontract.FromProtoDescriptor(descriptor, protoPath) + if errConvert != nil { + return nil, fmt.Errorf("convert proto: %w", errConvert) + } + + contracts = append(contracts, convertedContract) + } + + return contracts, nil +} + +func ParseProtoDir(fs afero.Fs, protoPaths []string) (protocontract.SetOfContracts, error) { + var contracts protocontract.SetOfContracts + + for _, protoPath := range protoPaths { + files, err := fsutils.GatherMatchedEntriesInDir(fs, protoPath, onlyProtoFiles) + if err != nil { + return nil, fmt.Errorf("get valid files: %w", err) + } + + descriptors, err := parse([]string{protoPath, environment.TmpWellKnownProtosDir}, files...) + if err != nil { + return nil, fmt.Errorf("parse proto: %w", err) + } + + for _, descriptor := range descriptors { + convertedContract, errConvert := protocontract.FromProtoDescriptor(descriptor, protoPath) + if errConvert != nil { + return nil, fmt.Errorf("convert proto: %w", errConvert) + } + + contracts = append(contracts, convertedContract) + } + } + + return contracts, nil +} + +func parse(protoPaths []string, protoFiles ...string) ([]*desc.FileDescriptor, error) { + resolvedHeaders, err := protoparse.ResolveFilenames(protoPaths, protoFiles...) + if err != nil { + return nil, fmt.Errorf("resolve names: %w", err) + } + + parser := &protoparse.Parser{ + IncludeSourceCodeInfo: true, + ImportPaths: protoPaths, + } + + descriptors, err := parser.ParseFiles(resolvedHeaders...) + if err != nil { + return nil, fmt.Errorf("parse file: %w", err) + } + + return descriptors, nil +} + +func onlyProtoFiles(info os.FileInfo) bool { + if !info.IsDir() && + strings.Contains(info.Name(), ".proto") { + return true + } + return false +} diff --git a/pkg/models/protocontract/traverser/traverse.go b/pkg/models/protocontract/traverser/traverse.go new file mode 100644 index 0000000..cd97060 --- /dev/null +++ b/pkg/models/protocontract/traverser/traverse.go @@ -0,0 +1,25 @@ +package traverser + +import ( + "github.com/jhump/protoreflect/desc" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract" +) + +func Descriptors(contracts protocontract.SetOfContracts) []*desc.FileDescriptor { + var all []*desc.FileDescriptor + + for _, cont := range contracts { + all = append(all, cont.Desc) + rec(cont.Desc, &all) + } + + return all +} + +func rec(desc *desc.FileDescriptor, all *[]*desc.FileDescriptor) { + *all = append(*all, desc.GetDependencies()...) + for _, descriptor := range desc.GetDependencies() { + rec(descriptor, all) + } +} diff --git a/pkg/models/protocontract/traverser/traverse_test.go b/pkg/models/protocontract/traverser/traverse_test.go new file mode 100644 index 0000000..4eb178d --- /dev/null +++ b/pkg/models/protocontract/traverser/traverse_test.go @@ -0,0 +1,56 @@ +package traverser + +import ( + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/models/protocontract/loader" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" +) + +var projectDir = filepath.Join(fsutils.CurrentDir(), "../../../..") + +func TestSetOfContracts_Descriptors(t *testing.T) { + tests := []struct { + name string + + fs afero.Fs + path string + + wantFiles []string + }{ + { + fs: afero.NewOsFs(), + path: filepath.Join(projectDir, "static/tests/data/static-examples/with-common-imports"), + wantFiles: []string{ + "example.proto", + "google/protobuf/empty.proto", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sourcerInstance, err := sourcer.New(tt.fs, tt.path, types.ProtoType) + assert.NoError(t, err) + + contracts, err := loader.Load(tt.fs, sourcerInstance) + assert.NoError(t, err) + + descriptors := Descriptors(contracts) + assert.NotNil(t, descriptors) + + var files []string + for _, desc := range descriptors { + files = append(files, desc.GetName()) + } + + assert.ElementsMatch(t, tt.wantFiles, strutils.UniqueAndSorted(files...)) + }) + } +} diff --git a/pkg/printer/print.go b/pkg/printer/print.go new file mode 100644 index 0000000..ba57a11 --- /dev/null +++ b/pkg/printer/print.go @@ -0,0 +1,18 @@ +package printer + +import ( + "fmt" + + "github.com/jhump/protoreflect/desc" + "github.com/jhump/protoreflect/desc/protoprint" +) + +func Print(descriptors []*desc.FileDescriptor, outputPath string) error { + printer := protoprint.Printer{} + + if err := printer.PrintProtosToFileSystem(descriptors, outputPath); err != nil { + return fmt.Errorf("print: %w", err) + } + + return nil +} diff --git a/pkg/renderer/funcs.go b/pkg/renderer/funcs.go new file mode 100644 index 0000000..171db51 --- /dev/null +++ b/pkg/renderer/funcs.go @@ -0,0 +1,17 @@ +package renderer + +import ( + "path" + "strings" + "text/template" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" +) + +var funcMap = template.FuncMap{ + "Base": path.Base, + "Capitalize": strings.Title, + "ToLower": strings.ToLower, + "ToSnakeCase": strutils.ToSnakeCase, + "ToPackageName": strutils.ToPackageName, +} diff --git a/pkg/renderer/renderer.go b/pkg/renderer/renderer.go new file mode 100644 index 0000000..e45d3e5 --- /dev/null +++ b/pkg/renderer/renderer.go @@ -0,0 +1,68 @@ +package renderer + +import ( + "fmt" + "path/filepath" + "strings" + "text/template" + + "github.com/spf13/afero" +) + +type renderer struct { + fs afero.Fs + + tpls map[string]*template.Template + + pathToTemplates string +} + +func New(fs afero.Fs, path string) (renderer, error) { + newRenderer := renderer{ + fs: fs, + pathToTemplates: path, + + tpls: map[string]*template.Template{}, + } + + templatesGlob := filepath.Join(path, "*") + + files, err := afero.Glob(fs, templatesGlob) + if err != nil { + return renderer{}, fmt.Errorf("glob: %w", err) + } + + for _, name := range files { + data, err := afero.ReadFile(fs, name) + if err != nil { + return renderer{}, fmt.Errorf("ReadFile: %w", err) + } + + tpl, err := template.New(name).Funcs(funcMap).Parse(string(data)) + if err != nil { + return renderer{}, fmt.Errorf("parse: %w", err) + } + + newRenderer.tpls[name] = tpl + } + + return newRenderer, nil +} + +// Substitute tries to correct collisions with go keywords. The data should be passed as a pointer. +// The data will then be substituted into the template from static FS. +func (s renderer) Substitute(name string, v interface{}) (string, error) { + resolveCollisions(v) + + var buff strings.Builder + tpl, ok := s.tpls[name] + if !ok { + return "", fmt.Errorf("template not found: %s", name) + } + + if err := tpl.Execute(&buff, v); err != nil { + return "", fmt.Errorf("execute: %w", err) + } + + return buff.String(), nil +} diff --git a/pkg/renderer/resolver.go b/pkg/renderer/resolver.go new file mode 100644 index 0000000..769ecc4 --- /dev/null +++ b/pkg/renderer/resolver.go @@ -0,0 +1,65 @@ +package renderer + +import ( + "reflect" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/strutils" +) + +func resolveCollisions(value interface{}) { + resolve(reflect.ValueOf(value)) +} + +func changeSlice(rv reflect.Value) { + if !rv.CanAddr() { + return + } + + for i := 0; i < rv.Len(); i++ { + forEachSliceElem(rv.Index(i)) + } +} + +func resolve(rv reflect.Value) { + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + + switch rv.Kind() { + case reflect.Struct: + changeStruct(rv) + case reflect.Slice: + changeSlice(rv) + } +} + +func changeStruct(rv reflect.Value) { + if !rv.CanAddr() { + return + } + + for i := 0; i < rv.NumField(); i++ { + field := rv.Field(i) + forEachField(field) + } +} + +func forEachField(field reflect.Value) { + switch field.Kind() { + case reflect.String: + newValue := strutils.ResolveNameIfCollides(field.String()) + field.SetString(newValue) + case reflect.Slice: + changeSlice(field) + } +} + +func forEachSliceElem(item reflect.Value) { + switch item.Kind() { + case reflect.Struct: + resolve(item) + case reflect.String: + newValue := strutils.ResolveNameIfCollides(item.String()) + item.SetString(newValue) + } +} diff --git a/pkg/renderer/resolver_test.go b/pkg/renderer/resolver_test.go new file mode 100644 index 0000000..f044d96 --- /dev/null +++ b/pkg/renderer/resolver_test.go @@ -0,0 +1,67 @@ +package renderer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/generators/proxy" +) + +func Test_replaceValue(t *testing.T) { + tests := []struct { + name string + + obj proxy.SubstitutionForMainGoView + + want interface{} + }{ + { + obj: proxy.SubstitutionForMainGoView{ + GoPackages: []string{"github.com/service"}, + + OriginalGoPackagesWithService: []string{"service1", "service2"}, + PackageToServices: []proxy.PackageToService{ + {ProtoPackage: "service", Service: "service1"}, + {ProtoPackage: "service", Service: "service2"}, + }, + }, + want: proxy.SubstitutionForMainGoView{ + GoPackages: []string{"github.com/service"}, + + OriginalGoPackagesWithService: []string{"service1", "service2"}, + PackageToServices: []proxy.PackageToService{ + {ProtoPackage: "service", Service: "service1"}, + {ProtoPackage: "service", Service: "service2"}, + }, + }, + }, + { + obj: proxy.SubstitutionForMainGoView{ + GoPackages: []string{"github.com/service"}, + + OriginalGoPackagesWithService: []string{"go_resolved", "service2"}, + PackageToServices: []proxy.PackageToService{ + {ProtoPackage: "type", Service: "service1"}, + {ProtoPackage: "service", Service: "service2"}, + }, + }, + want: proxy.SubstitutionForMainGoView{ + GoPackages: []string{"github.com/service"}, + + OriginalGoPackagesWithService: []string{"go_resolved", "service2"}, + PackageToServices: []proxy.PackageToService{ + {ProtoPackage: "type_resolved", Service: "service1"}, + {ProtoPackage: "service", Service: "service2"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolveCollisions(&tt.obj) + assert.Equal(t, tt.want, tt.obj) + }) + } +} diff --git a/pkg/runner/errors.go b/pkg/runner/errors.go new file mode 100644 index 0000000..dd3000d --- /dev/null +++ b/pkg/runner/errors.go @@ -0,0 +1,29 @@ +package runner + +import ( + "fmt" + "strings" +) + +type errUnknownCommandExecution struct { + Err error + + Command string + Output string + Args []string +} + +func (e errUnknownCommandExecution) Error() string { + var builder strings.Builder + + builder.WriteString("command execution unknown error:\n") + builder.WriteString(fmt.Sprintf("\t command: '%s'\n", e.Command)) + builder.WriteString(fmt.Sprintf("\t args: '%s'\n", strings.Join(e.Args, " "))) + builder.WriteString(fmt.Sprintf("\t output: '%s'\n", e.Output)) + + return builder.String() +} + +func (e errUnknownCommandExecution) Unwrap() error { + return e.Err +} diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go new file mode 100644 index 0000000..7d83941 --- /dev/null +++ b/pkg/runner/runner.go @@ -0,0 +1,33 @@ +package runner + +import ( + "context" + "errors" + "fmt" + "strings" + + "k8s.io/utils/exec" +) + +type shell struct { + exec exec.Interface +} + +func New(ex exec.Interface) shell { + return shell{exec: ex} +} + +func (s shell) Run(ctx context.Context, cmd string, args ...string) error { + rawOut, err := s.exec.CommandContext(ctx, cmd, args...).CombinedOutput() + if err == nil { + return nil + } + + out := strings.TrimSpace(string(rawOut)) + switch { + case errors.Is(err, exec.ErrExecutableNotFound): + return fmt.Errorf("'%s' not found on host: %w", cmd, err) + default: + return errUnknownCommandExecution{Command: cmd, Output: out, Args: args, Err: err} + } +} diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go new file mode 100644 index 0000000..5d2bf06 --- /dev/null +++ b/pkg/runner/runner_test.go @@ -0,0 +1,58 @@ +package runner + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/utils/exec" + testingexec "k8s.io/utils/exec/testing" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/executils" +) + +type execArgs struct { + command string + args []string + output string + err error +} + +func Test_shell_Run(t *testing.T) { + tests := []struct { + name string + + execScript execArgs + + want string + wantErr error + }{ + { + execScript: execArgs{"protoc", []string{"-I", "/tmp/protos", "baugi.proto"}, "", exec.ErrExecutableNotFound}, + wantErr: exec.ErrExecutableNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeExec := &testingexec.FakeExec{ExactOrder: true} + + fakeCmd := &testingexec.FakeCmd{} + + cmdAction := executils.MakeFakeCmd(fakeCmd, tt.execScript.command, tt.execScript.args...) + outputAction := executils.MakeFakeOutput(tt.execScript.output, tt.execScript.err) + + fakeCmd.CombinedOutputScript = append(fakeCmd.CombinedOutputScript, outputAction) + fakeExec.CommandScript = append(fakeExec.CommandScript, cmdAction) + + ctx := context.Background() + err := New(fakeExec).Run(ctx, tt.execScript.command, tt.execScript.args...) + + if tt.wantErr == nil { + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, tt.wantErr) + } + }) + } +} diff --git a/pkg/sourcer/sourcer.go b/pkg/sourcer/sourcer.go new file mode 100644 index 0000000..cbeae4e --- /dev/null +++ b/pkg/sourcer/sourcer.go @@ -0,0 +1,118 @@ +package sourcer + +import ( + "fmt" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +var ErrContractTypeIsNotCorrespondToSourceLoadType = fmt.Errorf("expected another contract type") + +// ErrProvidedDirectoryHasNoContracts indicates that provided directory input is invalid. +var ErrProvidedDirectoryHasNoContracts = fmt.Errorf("provided directory has no valid contracts") + +// Sourcer hides how to work with contract sources. +// +// Contract source implements all details about opening, reading, validating +// files with specification. Such as .proto, .yaml (openapi) files. +type sourcer struct { + contractPaths []string + + afero.Fs + + loadType types.SourceLoadType + fileType types.SourceFileType +} + +// New creates instance of sourcer. +func New(fs afero.Fs, path string, sourceFileType types.SourceFileType) (sourcer, error) { + sourceLoadType, err := getContractSourceLoadType(fs, path) + if err != nil { + return sourcer{}, fmt.Errorf("get contract source load type: %w", err) + } + + contractPaths, err := getContractPaths(fs, path, sourceFileType, sourceLoadType) + if err != nil { + return sourcer{}, fmt.Errorf("get contract paths: %w", err) + } + + source := sourcer{ + Fs: fs, + + fileType: sourceFileType, + loadType: sourceLoadType, + + contractPaths: contractPaths, + } + + return source, nil +} + +// SourceLoadType returns selected source load type. +func (c sourcer) SourceLoadType() types.SourceLoadType { + return c.loadType +} + +// SourceFileType returns selected source file type. +func (c sourcer) SourceFileType() types.SourceFileType { + return c.fileType +} + +// SourceInputs returns path of input entry. +// +// Input to the directory with the contracts or to the single contract file. +func (c sourcer) SourceInputs() []string { + return c.contractPaths +} + +func getContractSourceLoadType(fs afero.Fs, path string) (types.SourceLoadType, error) { + entryInfo, err := fs.Stat(path) + if err != nil { + return types.UnknownLoadType, fmt.Errorf("stat entry %s: %w", path, err) + } + + if entryInfo.IsDir() { + return types.SourceDirType, nil + } + + return types.SourceSingleType, nil +} + +func getContractPaths(fs afero.Fs, path string, sourceFileType types.SourceFileType, loadType types.SourceLoadType) ([]string, error) { + switch loadType { + case types.SourceSingleType: + if !sourceFileType.Is(fsutils.GetFileExt(path)) { + return nil, ErrContractTypeIsNotCorrespondToSourceLoadType + } + + return []string{path}, nil + + case types.SourceDirType: + dirsWithContracts, err := findDirsWithContracts(fs, path, sourceFileType) + if err != nil { + return nil, fmt.Errorf("find dirs with contracts: %w", err) + } + + if len(dirsWithContracts) == 0 { + return nil, ErrProvidedDirectoryHasNoContracts + } + + return dirsWithContracts, nil + } + + return nil, fmt.Errorf("incorrect load type") +} + +func findDirsWithContracts(fs afero.Fs, path string, sourceFileType types.SourceFileType) ([]string, error) { + dirsWithContracts, err := fsutils.FindDirsWithContracts(fs, path, func(s string) bool { + return sourceFileType.Is(fsutils.GetFileExt(s)) + }) + if err != nil { + return nil, fmt.Errorf("find dirs: %w", err) + } + + return dirsWithContracts, nil +} diff --git a/pkg/sourcer/sourcer_test.go b/pkg/sourcer/sourcer_test.go new file mode 100644 index 0000000..095dee9 --- /dev/null +++ b/pkg/sourcer/sourcer_test.go @@ -0,0 +1,218 @@ +package sourcer + +import ( + "os" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/fstesting" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/fstesting/entrymock" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/sourcer/types" +) + +func TestSource(t *testing.T) { + testCases := []struct { + name string + input string + + fileType types.SourceFileType + + fs afero.Fs + + expectedLoadType types.SourceLoadType + expectedFileType types.SourceFileType + expectedInputs []string + }{ + { + input: "/test/inner/dir_with_contracts", + fileType: types.ProtoType, + expectedInputs: []string{"/test/inner/dir_with_contracts"}, + + expectedLoadType: types.SourceDirType, + expectedFileType: types.ProtoType, + fs: fstesting.CreateMockFS( + entrymock.File("/test/inner/dir_with_contracts/sample.proto"), + ), + }, + { + input: "/test/contract.proto", + fileType: types.ProtoType, + expectedInputs: []string{"/test/contract.proto"}, + + expectedLoadType: types.SourceSingleType, + expectedFileType: types.ProtoType, + fs: fstesting.CreateMockFS( + entrymock.File("/test/contract.proto"), + entrymock.File("/test/inner/contract.proto"), + ), + }, + { + input: "/test/openapi.yaml", + fileType: types.OpenAPIType, + expectedInputs: []string{"/test/openapi.yaml"}, + + expectedLoadType: types.SourceSingleType, + expectedFileType: types.OpenAPIType, + fs: fstesting.CreateMockFS( + entrymock.File("/test/openapi.yaml"), + ), + }, + { + input: "/deps", + fileType: types.ProtoType, + expectedInputs: []string{"/deps/services/ph/grpc"}, + expectedLoadType: types.SourceDirType, + expectedFileType: types.ProtoType, + fs: fstesting.CreateMockFS( + entrymock.File("/deps/services/ab/openapi/ab.yaml"), + entrymock.File("/deps/services/ph/grpc/phfd.proto"), + ), + }, + { + input: "/deps", + fileType: types.OpenAPIType, + expectedInputs: []string{"/deps/services"}, + + expectedLoadType: types.SourceDirType, + expectedFileType: types.OpenAPIType, + fs: fstesting.CreateMockFS( + entrymock.File("/deps/services/ab.yaml"), + entrymock.File("/deps/services/phfd.proto"), + ), + }, + { + input: "/deps", + fileType: types.OpenAPIType, + expectedInputs: []string{"/deps/services"}, + + expectedLoadType: types.SourceDirType, + expectedFileType: types.OpenAPIType, + fs: fstesting.CreateMockFS( + entrymock.File("/deps/services/ab.yaml"), + entrymock.File("/deps/services/phfd.proto"), + ), + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + source, err := New(tt.fs, tt.input, tt.fileType) + assert.NoError(t, err) + + assert.ElementsMatch(t, tt.expectedInputs, source.SourceInputs()) + assert.Equal(t, source.SourceLoadType(), tt.expectedLoadType) + assert.Equal(t, source.SourceFileType(), tt.expectedFileType) + }) + } +} + +func TestSourceWithError(t *testing.T) { + testCases := []struct { + name string + input string + + fileType types.SourceFileType + + fs afero.Fs + wantErr error + }{ + { + name: "dir doesn't exist", + input: "/test/inner/dir_with_contracts", + wantErr: os.ErrNotExist, + fs: fstesting.CreateMockFS( + entrymock.Dir("/test/inner/dir"), + ), + }, + { + name: "proto file doesn't exist", + input: "/test/contract_test.proto", + wantErr: os.ErrNotExist, + fs: fstesting.CreateMockFS( + entrymock.File("/test/contract.proto"), + entrymock.File("/test/inner/contract.proto"), + ), + }, + { + name: "incorrect extension", + input: "/test/openapi.txt", + wantErr: ErrContractTypeIsNotCorrespondToSourceLoadType, + fs: fstesting.CreateMockFS( + entrymock.File("/test/openapi.txt"), + ), + }, + { + name: "empty dir", + input: "/test", + wantErr: ErrProvidedDirectoryHasNoContracts, + fs: fstesting.CreateMockFS( + entrymock.Dir("/test"), + ), + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + _, err := New(tt.fs, tt.input, tt.fileType) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func Test_findDirsWithContracts(t *testing.T) { + tests := []struct { + name string + fs afero.Fs + path string + sourceFileType types.SourceFileType + want []string + }{ + { + path: "/deps", + sourceFileType: types.OpenAPIType, + fs: fstesting.CreateMockFS( + entrymock.Dir("/deps/services/awesome/openapi/contract.yaml"), + entrymock.Dir("/deps/services/awesome/grpc/contract.proto"), + entrymock.Dir("/deps/services/disgusting/openapi/contract.yaml"), + ), + want: []string{ + "/deps/services/awesome/openapi", + "/deps/services/disgusting/openapi", + }, + }, + { + path: "/deps", + sourceFileType: types.OpenAPIType, + fs: fstesting.CreateMockFS( + entrymock.Dir("/deps/services/awesome/grpc/contract.yaml"), + entrymock.Dir("/deps/services/awesome/grpc/contract.proto"), + entrymock.Dir("/deps/services/disgusting/grpc/contract.yaml"), + ), + want: []string{ + "/deps/services/awesome/grpc", + "/deps/services/disgusting/grpc", + }, + }, + { + path: "/deps", + sourceFileType: types.OpenAPIType, + fs: fstesting.CreateMockFS( + entrymock.Dir("/deps/services/awesome/grpc/contract.proto"), + entrymock.Dir("/deps/services/awesome/grpc/contract.proto"), + entrymock.Dir("/deps/services/disgusting/grpc/contract.proto"), + ), + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := findDirsWithContracts(tt.fs, tt.path, tt.sourceFileType) + require.NoError(t, err) + require.ElementsMatch(t, got, tt.want) + }) + } +} diff --git a/pkg/sourcer/types/types.go b/pkg/sourcer/types/types.go new file mode 100644 index 0000000..e234a70 --- /dev/null +++ b/pkg/sourcer/types/types.go @@ -0,0 +1,88 @@ +package types + +import ( + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +// SourceLoadType represents how exactly contracts must be loaded. +type SourceLoadType int + +const ( + // UnknownLoadType represents default value of SourceLoadType. + UnknownLoadType SourceLoadType = iota + + // SourceDirType represents that provided input is directory with the contracts. + SourceDirType SourceLoadType = iota + + // SourceSingleType represents that provided input is single contract file. + SourceSingleType +) + +type SourceFileType string + +const ( + // UnknownFileType represents unknown contracts type. + UnknownFileType = "unknown" + + // ProtoType represents contracts described in .proto files. + ProtoType SourceFileType = "proto" + + // OpenAPIType represents contracts described in .yaml files. + OpenAPIType SourceFileType = "yaml" +) + +func (s SourceFileType) Is(that string) bool { + return string(s) == that +} + +func SourceFileTypeFromPath(path string) SourceFileType { + extension := fsutils.GetFileExt(path) + + switch { + case ProtoType.Is(extension): + return ProtoType + case OpenAPIType.Is(extension): + return OpenAPIType + } + + return "" +} + +type ProtoMethodType int + +const ( + UnaryType ProtoMethodType = iota + ClientSideStreamingType + ServerSideStreamingType + BidirectionalStreamingType +) + +func MethodType(isClientSideStream, isServerSideStream bool) ProtoMethodType { + if isServerSideStream { + if isClientSideStream { + return BidirectionalStreamingType + } + return ServerSideStreamingType + } + + if isClientSideStream { + return ClientSideStreamingType + } + + return UnaryType +} + +func (t ProtoMethodType) String() string { + switch t { + case UnaryType: + return "unary" + case ClientSideStreamingType: + return "client side streaming" + case ServerSideStreamingType: + return "server side streaming" + case BidirectionalStreamingType: + return "bidirectional streaming" + } + + return "" +} diff --git a/pkg/svctesting/client/health.pb.go b/pkg/svctesting/client/health.pb.go new file mode 100644 index 0000000..d611aa9 --- /dev/null +++ b/pkg/svctesting/client/health.pb.go @@ -0,0 +1,284 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.18.1 +// source: static/health/health.proto + +package healther + +import ( + 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) +) + +type HealthCheckResponse_ServingStatus int32 + +const ( + HealthCheckResponse_UNKNOWN HealthCheckResponse_ServingStatus = 0 + HealthCheckResponse_SERVING HealthCheckResponse_ServingStatus = 1 + HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2 +) + +// Enum value maps for HealthCheckResponse_ServingStatus. +var ( + HealthCheckResponse_ServingStatus_name = map[int32]string{ + 0: "UNKNOWN", + 1: "SERVING", + 2: "NOT_SERVING", + } + HealthCheckResponse_ServingStatus_value = map[string]int32{ + "UNKNOWN": 0, + "SERVING": 1, + "NOT_SERVING": 2, + } +) + +func (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse_ServingStatus { + p := new(HealthCheckResponse_ServingStatus) + *p = x + return p +} + +func (x HealthCheckResponse_ServingStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor { + return file_static_health_health_proto_enumTypes[0].Descriptor() +} + +func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType { + return &file_static_health_health_proto_enumTypes[0] +} + +func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. +func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { + return file_static_health_health_proto_rawDescGZIP(), []int{1, 0} +} + +type HealthCheckRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` +} + +func (x *HealthCheckRequest) Reset() { + *x = HealthCheckRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_static_health_health_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckRequest) ProtoMessage() {} + +func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_static_health_health_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 HealthCheckRequest.ProtoReflect.Descriptor instead. +func (*HealthCheckRequest) Descriptor() ([]byte, []int) { + return file_static_health_health_proto_rawDescGZIP(), []int{0} +} + +func (x *HealthCheckRequest) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +type HealthCheckResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status HealthCheckResponse_ServingStatus `protobuf:"varint,1,opt,name=status,proto3,enum=grpc.health.v1.HealthCheckResponse_ServingStatus" json:"status,omitempty"` +} + +func (x *HealthCheckResponse) Reset() { + *x = HealthCheckResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_static_health_health_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckResponse) ProtoMessage() {} + +func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_static_health_health_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 HealthCheckResponse.ProtoReflect.Descriptor instead. +func (*HealthCheckResponse) Descriptor() ([]byte, []int) { + return file_static_health_health_proto_rawDescGZIP(), []int{1} +} + +func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { + if x != nil { + return x.Status + } + return HealthCheckResponse_UNKNOWN +} + +var File_static_health_health_proto protoreflect.FileDescriptor + +var file_static_health_health_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x2f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2f, + 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x22, 0x2e, 0x0a, 0x12, + 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x9c, 0x01, 0x0a, + 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x3a, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, + 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, + 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x32, 0xae, 0x01, 0x0a, 0x06, + 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x50, 0x0a, 0x05, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, + 0x22, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, + 0x68, 0x12, 0x22, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, + 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x2b, 0x5a, 0x29, + 0x67, 0x72, 0x70, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, + 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x73, 0x62, 0x6d, 0x74, 0x2e, 0x69, 0x6f, 0x2f, 0x70, 0x61, + 0x61, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_static_health_health_proto_rawDescOnce sync.Once + file_static_health_health_proto_rawDescData = file_static_health_health_proto_rawDesc +) + +func file_static_health_health_proto_rawDescGZIP() []byte { + file_static_health_health_proto_rawDescOnce.Do(func() { + file_static_health_health_proto_rawDescData = protoimpl.X.CompressGZIP(file_static_health_health_proto_rawDescData) + }) + return file_static_health_health_proto_rawDescData +} + +var file_static_health_health_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_static_health_health_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_static_health_health_proto_goTypes = []interface{}{ + (HealthCheckResponse_ServingStatus)(0), // 0: grpc.health.v1.HealthCheckResponse.ServingStatus + (*HealthCheckRequest)(nil), // 1: grpc.health.v1.HealthCheckRequest + (*HealthCheckResponse)(nil), // 2: grpc.health.v1.HealthCheckResponse +} +var file_static_health_health_proto_depIdxs = []int32{ + 0, // 0: grpc.health.v1.HealthCheckResponse.status:type_name -> grpc.health.v1.HealthCheckResponse.ServingStatus + 1, // 1: grpc.health.v1.Health.Check:input_type -> grpc.health.v1.HealthCheckRequest + 1, // 2: grpc.health.v1.Health.Watch:input_type -> grpc.health.v1.HealthCheckRequest + 2, // 3: grpc.health.v1.Health.Check:output_type -> grpc.health.v1.HealthCheckResponse + 2, // 4: grpc.health.v1.Health.Watch:output_type -> grpc.health.v1.HealthCheckResponse + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_static_health_health_proto_init() } +func file_static_health_health_proto_init() { + if File_static_health_health_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_static_health_health_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HealthCheckRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_static_health_health_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HealthCheckResponse); 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_static_health_health_proto_rawDesc, + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_static_health_health_proto_goTypes, + DependencyIndexes: file_static_health_health_proto_depIdxs, + EnumInfos: file_static_health_health_proto_enumTypes, + MessageInfos: file_static_health_health_proto_msgTypes, + }.Build() + File_static_health_health_proto = out.File + file_static_health_health_proto_rawDesc = nil + file_static_health_health_proto_goTypes = nil + file_static_health_health_proto_depIdxs = nil +} diff --git a/pkg/svctesting/client/health_grpc.pb.go b/pkg/svctesting/client/health_grpc.pb.go new file mode 100644 index 0000000..1a8d44b --- /dev/null +++ b/pkg/svctesting/client/health_grpc.pb.go @@ -0,0 +1,169 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.18.1 +// source: static/health/health.proto + +package healther + +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 + +// HealthClient is the client API for Health 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 HealthClient interface { + Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) + Watch(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (Health_WatchClient, error) +} + +type healthClient struct { + cc grpc.ClientConnInterface +} + +func NewHealthClient(cc grpc.ClientConnInterface) HealthClient { + return &healthClient{cc} +} + +func (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { + out := new(HealthCheckResponse) + err := c.cc.Invoke(ctx, "/grpc.health.v1.Health/Check", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *healthClient) Watch(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (Health_WatchClient, error) { + stream, err := c.cc.NewStream(ctx, &Health_ServiceDesc.Streams[0], "/grpc.health.v1.Health/Watch", opts...) + if err != nil { + return nil, err + } + x := &healthWatchClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Health_WatchClient interface { + Recv() (*HealthCheckResponse, error) + grpc.ClientStream +} + +type healthWatchClient struct { + grpc.ClientStream +} + +func (x *healthWatchClient) Recv() (*HealthCheckResponse, error) { + m := new(HealthCheckResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// HealthServer is the server API for Health service. +// All implementations must embed UnimplementedHealthServer +// for forward compatibility +type HealthServer interface { + Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) + Watch(*HealthCheckRequest, Health_WatchServer) error + mustEmbedUnimplementedHealthServer() +} + +// UnimplementedHealthServer must be embedded to have forward compatible implementations. +type UnimplementedHealthServer struct { +} + +func (UnimplementedHealthServer) Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Check not implemented") +} +func (UnimplementedHealthServer) Watch(*HealthCheckRequest, Health_WatchServer) error { + return status.Errorf(codes.Unimplemented, "method Watch not implemented") +} +func (UnimplementedHealthServer) mustEmbedUnimplementedHealthServer() {} + +// UnsafeHealthServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HealthServer will +// result in compilation errors. +type UnsafeHealthServer interface { + mustEmbedUnimplementedHealthServer() +} + +func RegisterHealthServer(s grpc.ServiceRegistrar, srv HealthServer) { + s.RegisterService(&Health_ServiceDesc, srv) +} + +func _Health_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HealthServer).Check(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/grpc.health.v1.Health/Check", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HealthServer).Check(ctx, req.(*HealthCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Health_Watch_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(HealthCheckRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(HealthServer).Watch(m, &healthWatchServer{stream}) +} + +type Health_WatchServer interface { + Send(*HealthCheckResponse) error + grpc.ServerStream +} + +type healthWatchServer struct { + grpc.ServerStream +} + +func (x *healthWatchServer) Send(m *HealthCheckResponse) error { + return x.ServerStream.SendMsg(m) +} + +// Health_ServiceDesc is the grpc.ServiceDesc for Health service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Health_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "grpc.health.v1.Health", + HandlerType: (*HealthServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Check", + Handler: _Health_Check_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Watch", + Handler: _Health_Watch_Handler, + ServerStreams: true, + }, + }, + Metadata: "static/health/health.proto", +} diff --git a/pkg/svctesting/svcrunner/runner.go b/pkg/svctesting/svcrunner/runner.go new file mode 100644 index 0000000..e74a1c7 --- /dev/null +++ b/pkg/svctesting/svcrunner/runner.go @@ -0,0 +1,67 @@ +package svcrunner + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "syscall" +) + +func Run(ctx context.Context, path, port string) (func(), error) { + process, err := start(ctx, path, port) + if err != nil { + return nil, fmt.Errorf("process start is failed: %w", err) + } + + turnOffF := func() { + log.Printf("Process %d and his whole family "+ + "are on their way to Valhalla", process.Pid) + + killGroup(process) + } + + log.Printf("Process %d is started\n", process.Pid) + + return turnOffF, nil +} + +func killGroup(process *os.Process) { + if process == nil { + log.Println("Process is nil. Can't stop the process") + return + } + + negativePIDToKillEntireGroup := -process.Pid + + // kill the entire group with child processes + if err := syscall.Kill(negativePIDToKillEntireGroup, syscall.SIGKILL); err != nil { + log.Printf("failed to kill child process %d\n", process.Pid) + } +} + +func start(ctx context.Context, path, port string) (*os.Process, error) { + command := exec.CommandContext(ctx, "make", "run", "-C", path) + command.Env = createEnvs(port) + + command.Stdout = os.Stdout + command.Stderr = os.Stderr + + command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + if err := command.Start(); err != nil { + return nil, fmt.Errorf("run cmd: %w", err) + } + + return command.Process, nil +} + +func createEnvs(port string) []string { + const portEnv = "GRPC_TO_HTTP_PROXY_PORT" + + env := fmt.Sprintf("%s=%s", portEnv, port) + envs := os.Environ() + + return append(envs, env) +} diff --git a/pkg/svctesting/testing.go b/pkg/svctesting/testing.go new file mode 100644 index 0000000..cdd777d --- /dev/null +++ b/pkg/svctesting/testing.go @@ -0,0 +1,79 @@ +package svctesting + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + healther "github.com/SberMarket-Tech/grpc-wiremock/pkg/svctesting/client" +) + +var ( + healthRequestTimeout = time.Second * 50 + healthRequestInterval = time.Second +) + +type tester struct { + interval time.Duration + client healther.HealthClient +} + +func createClient(port string) (healther.HealthClient, error) { + addr := fmt.Sprintf(":%s", port) + + conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("create health connect: %w", err) + } + + return healther.NewHealthClient(conn), nil +} + +func Check(ctx context.Context, port string) error { + ctx, cancel := context.WithTimeout(ctx, healthRequestTimeout) + defer cancel() + + healthClient, err := createClient(port) + if err != nil { + return fmt.Errorf("create health client: %w", err) + } + + t := tester{ + interval: healthRequestInterval, + client: healthClient, + } + + if err = t.isHealthy(ctx); err != nil { + return fmt.Errorf("is service healthy: %w", err) + } + + return nil +} + +func (t *tester) isHealthy(ctx context.Context) error { + ticker := time.NewTicker(t.interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := t.isHealthyRequest(ctx); err == nil { + return nil + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func (t *tester) isHealthyRequest(ctx context.Context) error { + _, err := t.client.Check(ctx, &healther.HealthCheckRequest{}) + if err != nil { + return fmt.Errorf("do request: %w", err) + } + + return nil +} diff --git a/pkg/timespec/time_darwin.go b/pkg/timespec/time_darwin.go new file mode 100644 index 0000000..8001032 --- /dev/null +++ b/pkg/timespec/time_darwin.go @@ -0,0 +1,7 @@ +package timespec + +import "syscall" + +func CreationTime(info *syscall.Stat_t) syscall.Timespec { + return info.Ctimespec +} diff --git a/pkg/timespec/time_linux.go b/pkg/timespec/time_linux.go new file mode 100644 index 0000000..eb00d7f --- /dev/null +++ b/pkg/timespec/time_linux.go @@ -0,0 +1,7 @@ +package timespec + +import "syscall" + +func CreationTime(info *syscall.Stat_t) syscall.Timespec { + return info.Ctim +} diff --git a/pkg/utils/executils/fakecmd.go b/pkg/utils/executils/fakecmd.go new file mode 100644 index 0000000..1e82ef3 --- /dev/null +++ b/pkg/utils/executils/fakecmd.go @@ -0,0 +1,21 @@ +package executils + +import ( + "k8s.io/utils/exec" + testingexec "k8s.io/utils/exec/testing" +) + +func MakeFakeCmd(fakeCmd *testingexec.FakeCmd, cmd string, args ...string) testingexec.FakeCommandAction { + c := cmd + a := args + return func(cmd string, args ...string) exec.Cmd { + return testingexec.InitFakeCmd(fakeCmd, c, a...) + } +} + +func MakeFakeOutput(output string, err error) testingexec.FakeAction { + o := output + return func() ([]byte, []byte, error) { + return []byte(o), nil, err + } +} diff --git a/pkg/utils/executils/look.go b/pkg/utils/executils/look.go new file mode 100644 index 0000000..e7b519e --- /dev/null +++ b/pkg/utils/executils/look.go @@ -0,0 +1,17 @@ +package executils + +import ( + "fmt" + "os/exec" +) + +func HostHasBinaries(binaries ...string) error { + for _, binary := range binaries { + _, err := exec.LookPath(binary) + if err != nil { + return fmt.Errorf("look path for '%s': %w", binary, err) + } + } + + return nil +} diff --git a/pkg/utils/fsutils/compress.go b/pkg/utils/fsutils/compress.go new file mode 100644 index 0000000..807e1ab --- /dev/null +++ b/pkg/utils/fsutils/compress.go @@ -0,0 +1,85 @@ +package fsutils + +import ( + "archive/zip" + "fmt" + "io" + "io/fs" + "path/filepath" + + "github.com/spf13/afero" +) + +func MakeZipArchive(sourceFS, targetFS afero.Fs, sourcePath, targetPath string) error { + f, err := targetFS.Create(targetPath) + if err != nil { + return fmt.Errorf("create file: %w", err) + } + defer f.Close() + + writer := zip.NewWriter(f) + defer writer.Close() + + waklF := createWalkF(sourceFS, sourcePath, writer) + + if err := afero.Walk(sourceFS, sourcePath, waklF); err != nil { + return fmt.Errorf("walk: %w", err) + } + + return nil +} + +func createWalkF(sourceFS afero.Fs, sourcePath string, writer *zip.Writer) filepath.WalkFunc { + return func(path string, info fs.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("walk func: %w", err) + } + + if info.IsDir() { + return nil + } + + header, err := createHeader(info, sourcePath, path) + if err != nil { + return fmt.Errorf("create header: %w", err) + } + + headerWriter, err := writer.CreateHeader(header) + if err != nil { + return fmt.Errorf("write header: %w", err) + } + + f, err := sourceFS.Open(path) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer f.Close() + + _, err = io.Copy(headerWriter, f) + if err != nil { + return fmt.Errorf("copy: %w", err) + } + + return nil + } +} + +func createHeader(info fs.FileInfo, sourcePath, path string) (*zip.FileHeader, error) { + header, err := zip.FileInfoHeader(info) + if err != nil { + return nil, fmt.Errorf("header: %w", err) + } + + header.Method = zip.Deflate + + header.Name, err = filepath.Rel(filepath.Dir(sourcePath), path) + if err != nil { + return nil, fmt.Errorf("get rel path: %w", err) + } + + if info.IsDir() { + header.Name += "/" + } + + return header, nil +} diff --git a/pkg/utils/fsutils/copydir.go b/pkg/utils/fsutils/copydir.go new file mode 100644 index 0000000..9b3e3b6 --- /dev/null +++ b/pkg/utils/fsutils/copydir.go @@ -0,0 +1,104 @@ +package fsutils + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/afero" +) + +func CopyDir(sourceFS, targetFS afero.Fs, sourcePath, targetPath string, skipIfExists bool) error { + info, err := sourceFS.Stat(sourcePath) + if err != nil { + return fmt.Errorf("stat dir: %w", err) + } + return copy(sourceFS, targetFS, sourcePath, targetPath, info, skipIfExists) +} + +func copy(sourceFS, targetFS afero.Fs, src, dest string, info os.FileInfo, skipIfExists bool) error { + if info.Mode()&os.ModeDevice != 0 { + return nil + } + + switch { + case info.IsDir(): + if err := dcopy(sourceFS, targetFS, src, dest, skipIfExists); err != nil { + return fmt.Errorf("copy dir: %w", err) + } + default: + if err := fcopy(sourceFS, targetFS, src, dest); err != nil { + return fmt.Errorf("copy file: %w", err) + } + } + + return nil +} + +func fcopy(sourceFS, targetFS afero.Fs, src, dest string) error { + if err := targetFS.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { + return fmt.Errorf("make dirs: %w", err) + } + + f, err := targetFS.Create(dest) + if err != nil { + return fmt.Errorf("create: %w", err) + } + + s, err := sourceFS.Open(src) + if err != nil { + return fmt.Errorf("open: %w", err) + } + + var buf []byte = nil + var ( + w io.Writer = f + r io.Reader = s + ) + + if _, err = io.CopyBuffer(w, r, buf); err != nil { + return fmt.Errorf("copy buffer: %w", err) + } + + return nil +} + +func dcopy(sourceFS, targetFS afero.Fs, sourcePath, targetPath string, skipIfExists bool) (err error) { + if skip, err := onDirExists(targetFS, targetPath, skipIfExists); err != nil { + return err + } else if skip { + return nil + } + + contents, err := afero.ReadDir(sourceFS, sourcePath) + if err != nil { + return + } + + for _, content := range contents { + cs, cd := filepath.Join(sourcePath, content.Name()), filepath.Join(targetPath, content.Name()) + + if err = copy(sourceFS, targetFS, cs, cd, content, skipIfExists); err != nil { + // If any error, exit immediately + return + } + } + + return +} + +func onDirExists(targetFS afero.Fs, targetPath string, skipIfExists bool) (bool, error) { + _, err := targetFS.Stat(targetPath) + if err == nil { + if skipIfExists { + return false, nil + } + if err = targetFS.RemoveAll(targetPath); err != nil { + return false, err + } + } else if err != nil && !os.IsNotExist(err) { + return true, err // Unwelcome error type...! + } + return false, nil +} diff --git a/pkg/utils/fsutils/fsutils-tests/fsutils_test.go b/pkg/utils/fsutils/fsutils-tests/fsutils_test.go new file mode 100644 index 0000000..8b4816b --- /dev/null +++ b/pkg/utils/fsutils/fsutils-tests/fsutils_test.go @@ -0,0 +1,278 @@ +package fsutils_tests + +import ( + "os" + "strings" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/fstesting" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/fstesting/entrymock" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +func TestFindDirsWithContracts(t *testing.T) { + tests := []struct { + name string + + fs afero.Fs + path string + + want []string + }{ + { + path: "/searcher", + fs: fstesting.CreateMockFS( + entrymock.File("/searcher/services/navigation/grpc/navigation.proto"), + entrymock.File("/searcher/services/product-hub/grpc/ph.proto"), + ), + want: []string{ + "/searcher/services/navigation/grpc", + "/searcher/services/product-hub/grpc", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := fsutils.FindDirsWithContracts(tt.fs, tt.path, func(entryName string) bool { + return strings.Contains(entryName, ".proto") || strings.Contains(entryName, ".yaml") + }) + assert.NoError(t, err) + assert.ElementsMatch(t, tt.want, got) + }) + } +} + +func TestGatherEntries(t *testing.T) { + tests := []struct { + name string + + fs afero.Fs + path string + + want []string + wantErr error + }{ + { + path: "/", + fs: fstesting.CreateMockFS( + entrymock.File("/searcher/services/navigation/grpc/navigation.proto"), + entrymock.File("/searcher/services/product-hub/grpc/ph.proto"), + ), + want: []string{ + "/", + "/searcher", + "/searcher/services", + "/searcher/services/navigation", + "/searcher/services/navigation/grpc", + "/searcher/services/navigation/grpc/navigation.proto", + "/searcher/services/product-hub", + "/searcher/services/product-hub/grpc", + "/searcher/services/product-hub/grpc/ph.proto", + }, + }, + { + path: "/searcher/services/navigation/grpc", + fs: fstesting.CreateMockFS( + entrymock.File("/searcher/services/navigation/grpc/navigation.proto"), + entrymock.File("/searcher/services/product-hub/grpc/ph.proto"), + ), + want: []string{ + "/searcher/services/navigation/grpc", + "/searcher/services/navigation/grpc/navigation.proto", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := fsutils.GatherEntries(tt.fs, tt.path) + assert.NoError(t, err) + + var paths []string + for _, entry := range got { + paths = append(paths, entry.Path) + } + assert.Equal(t, tt.want, paths) + }) + } +} + +func TestGetFileExt(t *testing.T) { + tests := []struct { + name string + + path string + + want string + }{ + {path: "", want: ""}, + {path: "/foo/bar/file.txt", want: "txt"}, + {path: "/foo/bar/file.proto", want: "proto"}, + {path: "/foo/bar/file", want: ""}, + {path: "file.go", want: "go"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := fsutils.GetFileExt(tt.path) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRemoveTmpDirs(t *testing.T) { + tests := []struct { + name string + + fs afero.Fs + paths []string + + wantFs afero.Fs + }{ + { + paths: []string{"/tmp/protos", "/tmp/foo"}, + fs: fstesting.CreateMockFS( + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + entrymock.File("/searcher/services/navigation/grpc/navigation.proto"), + entrymock.File("/searcher/services/product-hub/grpc/ph.proto"), + ), + wantFs: fstesting.CreateMockFS( + entrymock.Dir("/tmp/protos"), + entrymock.Dir("/tmp/foo"), + entrymock.File("/searcher/services/navigation/grpc/navigation.proto"), + entrymock.File("/searcher/services/product-hub/grpc/ph.proto"), + ), + }, + { + paths: []string{"/tmp/path_does_not_exist"}, + fs: fstesting.CreateMockFS( + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + ), + wantFs: fstesting.CreateMockFS( + entrymock.Dir("/tmp/path_does_not_exist"), + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := fsutils.RemoveTmpDirs(tt.fs, tt.paths...) + assert.NoError(t, err) + + diff, err := fstesting.CompareFS(tt.wantFs, tt.fs) + assert.NoError(t, err) + assert.True(t, diff.Empty()) + }) + } +} + +func TestRemoveWithSubdirs(t *testing.T) { + tests := []struct { + name string + + fs afero.Fs + path string + nameToRemove string + + wantFs afero.Fs + }{ + { + nameToRemove: ".keep", + path: "/", + fs: fstesting.CreateMockFS( + entrymock.File("/tmp/.keep"), + entrymock.File("/tmp/foo/bar/.keep"), + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + ), + wantFs: fstesting.CreateMockFS( + entrymock.Dir("/tmp"), + entrymock.Dir("/tmp/foo"), + entrymock.Dir("/tmp/foo/bar"), + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + ), + }, + { + nameToRemove: ".keep", + path: "/tmp/foo/bar", + fs: fstesting.CreateMockFS( + entrymock.File("/tmp/.keep"), + entrymock.File("/tmp/foo/bar/.keep"), + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + ), + wantFs: fstesting.CreateMockFS( + entrymock.File("/tmp/.keep"), + entrymock.Dir("/tmp/foo"), + entrymock.Dir("/tmp/foo/bar"), + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := fsutils.RemoveWithSubdirs(tt.fs, tt.path, tt.nameToRemove) + assert.NoError(t, err) + + diff, err := fstesting.CompareFS(tt.wantFs, tt.fs) + assert.NoError(t, err) + assert.True(t, diff.Empty()) + }) + } +} + +func TestValidDirectories(t *testing.T) { + tests := []struct { + name string + + fs afero.Fs + paths []string + + wantErr error + }{ + { + fs: fstesting.CreateMockFS( + entrymock.File("/tmp/.keep"), + entrymock.Dir("/tmp/foo"), + entrymock.Dir("/tmp/foo/bar"), + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + ), + paths: []string{"/tmp/foo", "/tmp/foo/bar"}, + wantErr: nil, + }, + { + fs: fstesting.CreateMockFS( + entrymock.File("/tmp/.keep"), + entrymock.Dir("/tmp/foo"), + entrymock.Dir("/tmp/foo/bar"), + entrymock.File("/tmp/protos/some_file.txt"), + entrymock.File("/tmp/foo/some_file.proto"), + ), + paths: []string{"/tmp/foo/.keep", "/tmp/foo/bar"}, + wantErr: os.ErrNotExist, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := fsutils.ValidDirectories(tt.fs, tt.paths...) + if tt.wantErr == nil { + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, tt.wantErr) + } + }) + } +} diff --git a/pkg/utils/fsutils/fsutils.go b/pkg/utils/fsutils/fsutils.go new file mode 100644 index 0000000..ac3c81f --- /dev/null +++ b/pkg/utils/fsutils/fsutils.go @@ -0,0 +1,189 @@ +package fsutils + +import ( + "bytes" + "fmt" + stdfs "io/fs" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/fstesting/entry" +) + +func CurrentDir() string { + _, filePath, _, _ := runtime.Caller(1) + return filepath.Dir(filePath) +} + +func FindDirsWithContracts(fs afero.Fs, path string, match func(string) bool) ([]string, error) { + var dirsWithContracts []string + + walkF := func(path string, info stdfs.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("walk: %w", err) + } + + if info.IsDir() { + entries, err := afero.ReadDir(fs, path) + if err != nil { + return fmt.Errorf("read dir: %w", err) + } + + for _, ent := range entries { + if match(ent.Name()) { + dirsWithContracts = append(dirsWithContracts, path) + return filepath.SkipDir + } + } + } + + return nil + } + + if err := afero.Walk(fs, path, walkF); err != nil { + return nil, fmt.Errorf("walk: %w", err) + } + + return dirsWithContracts, nil +} + +func GatherEntries(fs afero.Fs, path string) ([]entry.Entry, error) { + var entries []entry.Entry + + walkF := func(path string, info stdfs.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("walkF, path '%s': %w", path, err) + } + + entries = append(entries, entry.NewEntry(path, info)) + + return nil + } + + if err := afero.Walk(fs, path, walkF); err != nil { + return nil, fmt.Errorf("walk throw fs: %w", err) + } + + return entries, nil +} + +func ReadFile(fs afero.Fs, path string) ([]byte, error) { + body, err := afero.ReadFile(fs, path) + if err != nil { + return nil, fmt.Errorf("read file %s body: %w", path, err) + } + + return bytes.TrimSpace(body), nil +} + +func ValidDirectories(fs afero.Fs, paths ...string) error { + for _, path := range paths { + info, err := fs.Stat(path) + if err != nil { + return fmt.Errorf("stat '%s': %w", path, err) + } + if !info.IsDir() { + return fmt.Errorf("is not dir '%s': %w", path, err) + } + } + return nil +} + +func GetFileExt(path string) string { + return strings.TrimPrefix(filepath.Ext(filepath.Base(path)), ".") +} + +func WriteFile(fs afero.Fs, path string, content string) error { + dir := filepath.Dir(path) + + if err := fs.MkdirAll(dir, os.ModePerm); err != nil { + return fmt.Errorf("create dir: %s, err: %w", dir, err) + } + + if err := afero.WriteFile(fs, path, []byte(content), os.ModePerm); err != nil { + return fmt.Errorf("write file: %s, err: %w", path, err) + } + + return nil +} + +func RemoveTmpDirs(fs afero.Fs, paths ...string) error { + for _, path := range paths { + if err := fs.RemoveAll(path); err != nil { + return fmt.Errorf("remove tmp dir '%s', %w", path, err) + } + if err := fs.MkdirAll(path, os.ModePerm); err != nil { + return fmt.Errorf("create tmp dir '%s', %w", path, err) + } + } + + return nil +} + +func RemoveWithSubdirs(fs afero.Fs, path, name string) error { + walkF := func(path string, info stdfs.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("walkF: %w", err) + } + + if info.IsDir() || info.Name() != name { + return nil + } + + if err = fs.Remove(path); err != nil { + return fmt.Errorf("remove: %w", err) + } + + return nil + } + + if err := afero.Walk(fs, path, walkF); err != nil { + return fmt.Errorf("walk throw fs: %w", err) + } + + return nil +} + +func GatherDirs(fs afero.Fs, projectsDir string) ([]os.FileInfo, error) { + entries, err := afero.ReadDir(fs, projectsDir) + if err != nil { + return nil, fmt.Errorf("read dir with projects: %w", err) + } + + var filtered []os.FileInfo + for _, ent := range entries { + if !ent.IsDir() || IsHiddenFile(ent.Name()) { + continue + } + + filtered = append(filtered, ent) + } + + return filtered, nil +} + +func IsHiddenFile(path string) bool { + base := filepath.Base(path) + return strings.HasPrefix(base, ".") +} + +func GatherMatchedEntriesInDir(fs afero.Fs, path string, match func(info os.FileInfo) bool) ([]string, error) { + entries, err := afero.ReadDir(fs, path) + if err != nil { + return nil, fmt.Errorf("read dir: %w", err) + } + + var headers []string + for _, entry := range entries { + if match(entry) { + path := filepath.Join(path, entry.Name()) + headers = append(headers, path) + } + } + + return headers, nil +} diff --git a/pkg/utils/httputils/request.go b/pkg/utils/httputils/request.go new file mode 100644 index 0000000..5929f30 --- /dev/null +++ b/pkg/utils/httputils/request.go @@ -0,0 +1,57 @@ +package httputils + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + + "github.com/avast/retry-go/v4" +) + +const defaultAttemptsValue = 7 + +func DoPost(ctx context.Context, client http.Client, url string, reader io.Reader) (int, error) { + httpRequest, err := http.NewRequest(http.MethodPost, url, reader) + if err != nil { + return 0, fmt.Errorf("new request: %w", err) + } + + httpRequest = httpRequest.WithContext(ctx) + + var httpResponse *http.Response + + err = retry.Do(func() error { + httpResponse, err = client.Do(httpRequest) + if err != nil { + return fmt.Errorf("do: %w", err) + } + + return nil + }, retry.Attempts(defaultAttemptsValue)) + + if err != nil { + return 0, fmt.Errorf("retry: %w", err) + } + + if httpResponse == nil { + return 0, fmt.Errorf("response is empty") + } + + defer func() { + if err = httpResponse.Body.Close(); err != nil { + log.Println("close body:", err) + } + }() + + return httpResponse.StatusCode, nil +} + +func AssertStatus(expected, actual int) error { + if actual != expected { + return fmt.Errorf("expected status: %d, actual status: %d", expected, actual) + } + + return nil +} diff --git a/pkg/utils/sliceutils/sliceutils.go b/pkg/utils/sliceutils/sliceutils.go new file mode 100644 index 0000000..0b8fc33 --- /dev/null +++ b/pkg/utils/sliceutils/sliceutils.go @@ -0,0 +1,21 @@ +package sliceutils + +func FirstOf[T any](slice []T) T { + var result T + + if len(slice) > 0 { + result = slice[0] + } + + return result +} + +func SliceToMap[T comparable](slice []T) map[T]struct{} { + mapAsSet := map[T]struct{}{} + + for _, el := range slice { + mapAsSet[el] = struct{}{} + } + + return mapAsSet +} diff --git a/pkg/utils/sliceutils/sliceutils_test.go b/pkg/utils/sliceutils/sliceutils_test.go new file mode 100644 index 0000000..c0697d5 --- /dev/null +++ b/pkg/utils/sliceutils/sliceutils_test.go @@ -0,0 +1,49 @@ +package sliceutils + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFirstOf(t *testing.T) { + tests := []struct { + name string + + slice []string + + want string + }{ + {slice: []string{"foo", "bar"}, want: "foo"}, + {slice: []string{}, want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FirstOf(tt.slice) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSliceToMap(t *testing.T) { + type testCase[T comparable] struct { + name string + + slice []T + + want map[T]struct{} + } + tests := []testCase[string]{ + {slice: []string{"1", "1", "2"}, want: map[string]struct{}{"1": {}, "2": {}}}, + {slice: []string{""}, want: map[string]struct{}{"": {}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := SliceToMap(tt.slice) + assert.True(t, reflect.DeepEqual(tt.want, got)) + }) + } +} diff --git a/pkg/utils/strutils/golang.go b/pkg/utils/strutils/golang.go new file mode 100644 index 0000000..f032903 --- /dev/null +++ b/pkg/utils/strutils/golang.go @@ -0,0 +1,35 @@ +package strutils + +import ( + "fmt" + "strings" +) + +// Data types are allowed to be used as a code identifiers. +// That's why there keywords only. +var golangKeywords = map[string]struct{}{ + "break": {}, "default": {}, "func": {}, "interface": {}, "select": {}, + "case": {}, "defer": {}, "go": {}, "map": {}, "struct": {}, "chan": {}, "else": {}, + "goto": {}, "package": {}, "switch": {}, "const": {}, "fallthrough": {}, "if": {}, + "continue": {}, "for": {}, "import": {}, "return": {}, "var": {}, "type": {}, +} + +func ResolveNameIfCollides(keyword string) string { + if isGolangKeyword(keyword) { + return resolve(keyword) + } + + return keyword +} + +func isGolangKeyword(keyword string) bool { + value := strings.ToLower(keyword) + _, isExist := golangKeywords[value] + + return isExist +} + +func resolve(value string) string { + const suffixToResolveCollision = "resolved" + return fmt.Sprintf("%s_%s", value, suffixToResolveCollision) +} diff --git a/pkg/utils/strutils/strutils.go b/pkg/utils/strutils/strutils.go new file mode 100644 index 0000000..5a8fc42 --- /dev/null +++ b/pkg/utils/strutils/strutils.go @@ -0,0 +1,76 @@ +package strutils + +import ( + "bytes" + "path/filepath" + "regexp" + "sort" + "strings" + "unicode" +) + +var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") +var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + +func ToSnakeCase(input string) string { + output := matchFirstCap.ReplaceAllString(input, "${1}_${2}") + output = matchAllCap.ReplaceAllString(output, "${1}_${2}") + output = strings.ReplaceAll(output, "-", "_") + output = strings.ToLower(output) + return strings.ReplaceAll(output, ".", "_") +} + +func ToCamelCase(input string) string { + res := bytes.NewBuffer(nil) + capNext := true + + for _, v := range input { + if unicode.IsUpper(v) { + res.WriteRune(v) + capNext = false + continue + } + + if unicode.IsDigit(v) { + res.WriteRune(v) + capNext = true + continue + } + + if unicode.IsLower(v) { + if capNext { + res.WriteRune(unicode.ToUpper(v)) + } else { + res.WriteRune(v) + } + capNext = false + continue + } + + capNext = true + } + + return res.String() +} + +func ToPackageName(input string) string { + return ToSnakeCase(filepath.Base(input)) +} + +func UniqueAndSorted(values ...string) []string { + uniq := map[string]struct{}{} + for _, value := range values { + uniq[value] = struct{}{} + } + + var uniqValues []string + for value := range uniq { + uniqValues = append(uniqValues, value) + } + + sort.Slice(uniqValues, func(i, j int) bool { + return uniqValues[i] < uniqValues[j] + }) + + return uniqValues +} diff --git a/pkg/utils/strutils/strutils_test.go b/pkg/utils/strutils/strutils_test.go new file mode 100644 index 0000000..3c14175 --- /dev/null +++ b/pkg/utils/strutils/strutils_test.go @@ -0,0 +1,122 @@ +package strutils + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToPackageName(t *testing.T) { + tests := []struct { + name string + + input string + + want string + }{ + {input: "github.com/foo/bar/baugi", want: "baugi"}, + {input: "github.com/foo/bar/baugiWithSmthg", want: "baugi_with_smthg"}, + {input: "github.com/foo/bar/baugi.with.point", want: "baugi_with_point"}, + {input: "baugi.with.point", want: "baugi_with_point"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToPackageName(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestToSnakeCase(t *testing.T) { + tests := []struct { + name string + + input string + + want string + }{ + {input: "baugi", want: "baugi"}, + {input: "baugiWithSmthg", want: "baugi_with_smthg"}, + {input: "baugi.with.point", want: "baugi_with_point"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToPackageName(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestUniqueAndSorted(t *testing.T) { + tests := []struct { + name string + + values []string + + want []string + }{ + {values: []string{"g", "a", "A", "a", "b"}, want: []string{"A", "a", "b", "g"}}, + {values: []string{"foo", "foo"}, want: []string{"foo"}}, + {values: []string{"foo", "bar"}, want: []string{"bar", "foo"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := UniqueAndSorted(tt.values...) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestToCamelCase(t *testing.T) { + tests := []struct { + name string + + input string + + want string + }{ + { + input: "some_string", + want: "SomeString", + }, + { + input: "", + want: "", + }, + { + input: "SomeString", + want: "SomeString", + }, + { + input: "Some String", + want: "SomeString", + }, + { + input: "Some-String", + want: "SomeString", + }, + { + input: "Some-string", + want: "SomeString", + }, + { + input: "some-string", + want: "SomeString", + }, + { + input: "some/string", + want: "SomeString", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToCamelCase(tt.input) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/utils/testutils/testing.go b/pkg/utils/testutils/testing.go new file mode 100644 index 0000000..d61da67 --- /dev/null +++ b/pkg/utils/testutils/testing.go @@ -0,0 +1,115 @@ +package testutils + +import ( + "fmt" + + "github.com/spf13/afero" + "gopkg.in/yaml.v3" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" +) + +type TestStatus int + +const ( + SkipStatus TestStatus = iota + SuccessStatus + FailStatus + UnknownStatus +) + +func (t TestStatus) String() string { + switch t { + case SkipStatus: + return "skip" + case FailStatus: + return "fail" + case SuccessStatus: + return "success" + } + + return "unknown" +} + +func ReadTestsStatuses(fs afero.Fs, path string) (TestToCases, error) { + body, err := afero.ReadFile(fs, path) + if err != nil { + return nil, fmt.Errorf("file %s read: %w", path, err) + } + + rawTests := map[string]interface{}{} + + if err = yaml.Unmarshal(body, &rawTests); err != nil { + return nil, fmt.Errorf("file %s unmarshal: %w", path, err) + } + + testsToCases := TestToCases{} + + for testName, rawTestCases := range rawTests { + skip := map[string]struct{}{} + fail := map[string]struct{}{} + success := map[string]struct{}{} + + testCases, ok := rawTestCases.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("parse tests statuses") + } + + for caseStatus, rawCaseNames := range testCases { + sliceCaseNames, ok := rawCaseNames.([]interface{}) + if !ok { + continue + } + + var caseNames []string + for _, rawCaseName := range sliceCaseNames { + caseNames = append(caseNames, rawCaseName.(string)) + } + + switch caseStatus { + case "skip": + skip = sliceutils.SliceToMap(caseNames) + case "fail": + fail = sliceutils.SliceToMap(caseNames) + case "success": + success = sliceutils.SliceToMap(caseNames) + } + } + + testsToCases[testName] = testCasesMaps{ + Skip: skip, + Fail: fail, + Success: success, + } + } + + return testsToCases, nil +} + +type testCasesMaps struct { + Skip map[string]struct{} + Fail map[string]struct{} + Success map[string]struct{} +} + +func (t testCasesMaps) getByCase(name string) TestStatus { + if _, exists := t.Fail[name]; exists { + return FailStatus + } + + if _, exists := t.Skip[name]; exists { + return SkipStatus + } + + if _, exists := t.Success[name]; exists { + return SuccessStatus + } + + return UnknownStatus +} + +type TestToCases map[string]testCasesMaps + +func (t TestToCases) GetByNameForTest(testName, caseName string) TestStatus { + return t[testName].getByCase(caseName) +} diff --git a/pkg/utils/testutils/testing_test.go b/pkg/utils/testutils/testing_test.go new file mode 100644 index 0000000..2b8fb01 --- /dev/null +++ b/pkg/utils/testutils/testing_test.go @@ -0,0 +1,46 @@ +package testutils + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + "github.com/SberMarket-Tech/grpc-wiremock/static" +) + +var fs = static.FromEmbed() + +func TestReadTestsStatuses(t *testing.T) { + tests := []struct { + name string + fs afero.Fs + path string + want TestToCases + }{ + { + fs: fs, + path: "tests-statuses.yml", + want: map[string]testCasesMaps{ + "generation": { + Skip: map[string]struct{}{"simple-1-flaky": {}}, + Fail: map[string]struct{}{"simple-2-fail": {}}, + Success: map[string]struct{}{"simple-success": {}}, + }, + "run-some-fancy-test-name": { + Skip: map[string]struct{}{"flaky-test-1": {}}, + Fail: map[string]struct{}{}, + Success: map[string]struct{}{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ReadTestsStatuses(tt.fs, tt.path) + require.NoError(t, err) + + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/watcher/notifier.go b/pkg/watcher/notifier.go new file mode 100644 index 0000000..f1ed62a --- /dev/null +++ b/pkg/watcher/notifier.go @@ -0,0 +1,35 @@ +package watcher + +import ( + "github.com/farmergreg/rfsnotify" + "gopkg.in/fsnotify.v1" +) + +type Notifier interface { + Errors() chan error + Events() chan fsnotify.Event +} + +type RecursiveNotifier struct { + notifier *rfsnotify.RWatcher +} + +func (e RecursiveNotifier) Errors() chan error { + return e.notifier.Errors +} + +func (e RecursiveNotifier) Events() chan fsnotify.Event { + return e.notifier.Events +} + +type NonRecursiveNotifier struct { + notifier *fsnotify.Watcher +} + +func (e NonRecursiveNotifier) Errors() chan error { + return e.notifier.Errors +} + +func (e NonRecursiveNotifier) Events() chan fsnotify.Event { + return e.notifier.Events +} diff --git a/pkg/watcher/types.go b/pkg/watcher/types.go new file mode 100644 index 0000000..08d0c03 --- /dev/null +++ b/pkg/watcher/types.go @@ -0,0 +1,104 @@ +package watcher + +import ( + "context" + "fmt" + "io" + "regexp" + "time" + + "golang.org/x/time/rate" + "gopkg.in/fsnotify.v1" +) + +type Action func(context.Context, string, string) error + +type Watchers []watcher + +var skipEventErr = fmt.Errorf("skip event") + +type WatcherDesc struct { + Do Action + + Name string + Path string + + Behave BehaviourDesc + + Recursive bool +} + +type watcher struct { + WatcherDesc + + notifier Notifier + + limiter *rate.Limiter + + allowedNameRules []*regexp.Regexp + + logger io.Writer +} + +type EventTypes map[fsnotify.Op]struct{} + +func NewEventTypes() EventTypes { + return map[fsnotify.Op]struct{}{} +} + +func (e EventTypes) WithCreate() EventTypes { + e[fsnotify.Create] = struct{}{} + return e +} + +func (e EventTypes) WithRemove() EventTypes { + e[fsnotify.Remove] = struct{}{} + return e +} + +func (e EventTypes) WithRename() EventTypes { + e[fsnotify.Rename] = struct{}{} + return e +} + +func (e EventTypes) WithWrite() EventTypes { + e[fsnotify.Write] = struct{}{} + return e +} + +func (e EventTypes) WithChmod() EventTypes { + e[fsnotify.Chmod] = struct{}{} + return e +} + +type EntryNamesRules []string + +type EntryRules struct { + NameRules EntryNamesRules +} + +func NewEntryRules() EntryRules { + return EntryRules{} +} + +func (e EntryRules) WithNameRule(rule string) EntryRules { + e.NameRules = append(e.NameRules, rule) + return e +} + +type ThrottlingRules struct { + DelayAfterEvent time.Duration + Interval time.Duration +} + +type RetryRules struct { + Attempts uint +} + +type BehaviourDesc struct { + Event EventTypes + Entry EntryRules + Retry RetryRules + + Throttle ThrottlingRules +} diff --git a/pkg/watcher/watch.go b/pkg/watcher/watch.go new file mode 100644 index 0000000..548d4ec --- /dev/null +++ b/pkg/watcher/watch.go @@ -0,0 +1,200 @@ +package watcher + +import ( + "context" + "errors" + "fmt" + "io" + "log" + "regexp" + "sync" + "time" + + "github.com/avast/retry-go/v4" + "github.com/farmergreg/rfsnotify" + "golang.org/x/time/rate" + "gopkg.in/fsnotify.v1" +) + +func NewRealWatcher(header WatcherDesc, logger io.Writer) (watcher, error) { + var allowedNameRules []*regexp.Regexp + + for _, rule := range header.Behave.Entry.NameRules { + compiledRule, err := regexp.Compile(rule) + if err != nil { + return watcher{}, fmt.Errorf("compile name rule %s: %w", rule, err) + } + + allowedNameRules = append(allowedNameRules, compiledRule) + } + + notifier, err := createNotifier(header.Recursive, header.Path) + if err != nil { + return watcher{}, fmt.Errorf("create notifier '%s': %w", header.Name, err) + } + + limiter := createLimiter(header.Behave.Throttle) + + return watcher{ + WatcherDesc: header, + + notifier: notifier, + limiter: limiter, + logger: logger, + + allowedNameRules: allowedNameRules, + }, nil +} + +func (w *Watchers) Watch(ctx context.Context) error { + if len(*w) == 0 { + return fmt.Errorf("watchers must be provided") + } + + var wg sync.WaitGroup + + for _, watcherToRun := range *w { + watcherToRun := watcherToRun + + log.Printf("watcher '%s' is ready\n", watcherToRun.Name) + + wg.Add(1) + go func() { + defer wg.Done() + + if err := watcherToRun.watch(ctx); err != nil { + log.Printf("watcher '%s': %s", watcherToRun.Name, err) + } + }() + } + + wg.Wait() + + return nil +} + +func createNotifier(recursive bool, path string) (Notifier, error) { + if recursive { + notifier, err := rfsnotify.NewWatcher() + if err != nil { + return nil, fmt.Errorf("create watchers: %w", err) + } + + if err = notifier.AddRecursive(path); err != nil { + return nil, fmt.Errorf("add recursive dir: %w", err) + } + + return RecursiveNotifier{notifier}, nil + } + + notifier, err := fsnotify.NewWatcher() + if err != nil { + return nil, fmt.Errorf("create watchers: %w", err) + } + + if err = notifier.Add(path); err != nil { + return nil, fmt.Errorf("add dir: %w", err) + } + + return NonRecursiveNotifier{notifier}, nil +} + +func createLimiter(throttle ThrottlingRules) *rate.Limiter { + const defaultBurst = 1 + + return rate.NewLimiter( + rate.Every(throttle.Interval), + defaultBurst, + ) +} + +func (w *watcher) watch(ctx context.Context) error { + for { + select { + case event, ok := <-w.notifier.Events(): + if !ok { + continue + } + + if err := w.handleEvent(ctx, event); err != nil { + if err = handleErrors(err); err != nil { + return err + } + + continue + } + + case err, ok := <-w.notifier.Errors(): + if !ok { + continue + } + + if err != nil { + return fmt.Errorf("notifier: %w", err) + } + + case <-ctx.Done(): + return nil + } + } +} + +func (w *watcher) handleEvent(ctx context.Context, event fsnotify.Event) error { + path := event.Name + + _, allowed := w.Behave.Event[event.Op] + if !allowed { + return skipEventErr + } + + if err := w.filterNames(path); err != nil { + return skipEventErr + } + + if !w.limiter.Allow() { + return skipEventErr + } + + retryableFunc := func() error { + if err := w.Do(ctx, w.Path, path); err != nil { + return err + } + + return nil + } + + opts := []retry.Option{retry.Attempts(w.Behave.Retry.Attempts)} + + time.AfterFunc(w.Behave.Throttle.DelayAfterEvent, func() { + if err := retry.Do(retryableFunc, opts...); err != nil { + log.Printf("do with retry: %s", err) + } + }) + + return nil +} + +func (w *watcher) filterNames(path string) error { + var atLeastOne bool + + for _, rule := range w.allowedNameRules { + if rule.MatchString(path) { + atLeastOne = true + continue + } + } + + if !atLeastOne { + return fmt.Errorf("name doesn't match") + } + + return nil +} + +func handleErrors(err error) error { + if errors.Is(err, skipEventErr) { + return nil + } + + return err +} diff --git a/pkg/watcher/watch_test.go b/pkg/watcher/watch_test.go new file mode 100644 index 0000000..bdef73e --- /dev/null +++ b/pkg/watcher/watch_test.go @@ -0,0 +1,175 @@ +package watcher + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" +) + +var ( + osfs = afero.NewOsFs() + mu sync.Mutex + + handledEvents []string +) + +func defaultGreeter(_ context.Context, _, path string) error { + mu.Lock() + handledEvents = append(handledEvents, path) + mu.Unlock() + + // do something useful. + + return nil +} + +const jsonFileRule = `.*\.json` + +func TestWatchers_Watch(t *testing.T) { + tests := []struct { + name string + count int + + interval time.Duration + + watcherDesc WatcherDesc + + wantHandledEvents []string + }{ + { + watcherDesc: WatcherDesc{ + Do: defaultGreeter, + Name: "greeter-with-throttling", + Path: "/tmp/test/watcher", + Behave: BehaviourDesc{ + Event: NewEventTypes().WithCreate(), + Entry: NewEntryRules(). + WithNameRule(jsonFileRule), + Throttle: ThrottlingRules{Interval: 400 * time.Millisecond}, + }, + }, + + count: 5, + interval: time.Millisecond * 50, + + wantHandledEvents: []string{ + filepath.Join("/tmp/test/watcher", createName(0)), + }, + }, + { + watcherDesc: WatcherDesc{ + Do: defaultGreeter, + Name: "greeter-without-throttling", + Path: "/tmp/test/watcher", + Behave: BehaviourDesc{ + Event: NewEventTypes().WithCreate(), + Entry: NewEntryRules(). + WithNameRule(jsonFileRule), + }, + }, + + count: 5, + interval: time.Millisecond * 100, + + wantHandledEvents: createWantBuffer(5, "/tmp/test/watcher"), + }, + { + watcherDesc: WatcherDesc{ + Do: defaultGreeter, + Name: "greeter-only-rename-events", + Path: "/tmp/test/watcher", + Behave: BehaviourDesc{ + Event: NewEventTypes().WithRename(), + }, + }, + + count: 5, + interval: time.Millisecond * 100, + + wantHandledEvents: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handledEvents = []string{} + + err := prepareEnvironment(tt.watcherDesc.Path) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + + go createFiles(ctx, cancel, tt.watcherDesc.Path, tt.count, tt.interval) + + realWatcher, err := NewRealWatcher(tt.watcherDesc, os.Stdout) + require.NoError(t, err) + + err = realWatcher.watch(ctx) + require.NoError(t, err) + + require.ElementsMatch(t, handledEvents, tt.wantHandledEvents) + }) + } +} + +func prepareEnvironment(path string) error { + if err := fsutils.RemoveTmpDirs(osfs, path); err != nil { + return fmt.Errorf("remove tmp dirs: %w", err) + } + + return nil +} + +func createFiles(ctx context.Context, cancelFn func(), path string, count int, interval time.Duration) { + defer cancelFn() + + var counter int + + ticker := time.Tick(interval) + + for { + select { + case <-ctx.Done(): + log.Println("ctx.done") + return + case <-ticker: + targetPath := filepath.Join(path, createName(counter)) + + if err := afero.WriteFile(osfs, targetPath, []byte{}, os.ModePerm); err != nil { + log.Printf("write file err: %s\n", err) + return + } + + if counter >= count { + log.Println("counter.done") + return + } + + counter++ + } + } +} + +func createWantBuffer(count int, path string) []string { + var wantBuffer []string + + for i := 0; i < count; i++ { + wantBuffer = append(wantBuffer, filepath.Join(path, createName(i))) + } + + return wantBuffer +} + +func createName(idx int) string { + return fmt.Sprintf("file_%d.json", idx) +} diff --git a/pkg/wiremock/client/mocks.go b/pkg/wiremock/client/mocks.go new file mode 100644 index 0000000..0267b0d --- /dev/null +++ b/pkg/wiremock/client/mocks.go @@ -0,0 +1,51 @@ +package client + +import ( + "context" + "fmt" + "net/http" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/httputils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +func (w *wiremock) UpdateMocks(ctx context.Context, domain string) error { + port, err := w.findPortByDomain(domain) + if err != nil { + return fmt.Errorf("find port by domain '%s': %w", domain, err) + } + + if err = w.resetMocks(ctx, port); err != nil { + return fmt.Errorf("reset mocks: %w", err) + } + + return nil +} + +func (w *wiremock) resetMocks(ctx context.Context, port uint) error { + const path = "__admin/mappings/reset" + + url := fmt.Sprintf("%s:%d/%s", w.host, port, path) + + status, err := httputils.DoPost(ctx, w.client, url, nil) + if err != nil { + return fmt.Errorf("request: %w", err) + } + + return httputils.AssertStatus(http.StatusOK, status) +} + +func (w *wiremock) findPortByDomain(domain string) (uint, error) { + wiremockConfig, err := w.configOpener.Open() + if err != nil { + return 0, fmt.Errorf("open: %w", err) + } + + for _, service := range wiremockConfig.Services { + if service.Name == domain { + return uint(service.Port), nil + } + } + + return 0, config.NoCorrespondingAPIErr +} diff --git a/pkg/wiremock/client/wiremock.go b/pkg/wiremock/client/wiremock.go new file mode 100644 index 0000000..5dd1a4e --- /dev/null +++ b/pkg/wiremock/client/wiremock.go @@ -0,0 +1,40 @@ +package client + +import ( + "net/http" + "time" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +type configOpener interface { + Open() (config.Wiremock, error) +} + +type wiremock struct { + host string + port uint + + client http.Client + + fs afero.Fs + + configOpener +} + +func NewDefaultClient(fs afero.Fs, opener configOpener) *wiremock { + var defaultTimeout = 15 * time.Second + + const ( + defaultHost = "http://localhost" + defaultPort = 9000 + ) + + return NewClient(fs, opener, defaultHost, defaultPort, http.Client{Timeout: defaultTimeout}) +} + +func NewClient(fs afero.Fs, opener configOpener, host string, port uint, httpClient http.Client) *wiremock { + return &wiremock{fs: fs, configOpener: opener, host: host, port: port, client: httpClient} +} diff --git a/pkg/wiremock/config/config.go b/pkg/wiremock/config/config.go new file mode 100644 index 0000000..d253c78 --- /dev/null +++ b/pkg/wiremock/config/config.go @@ -0,0 +1,26 @@ +package config + +import ( + "fmt" + "path/filepath" +) + +var ( + EmptyWiremockConfigErr = fmt.Errorf("empty wiremock config") + + NoCorrespondingAPIErr = fmt.Errorf("no API corresponding to the provided domain") +) + +type Service struct { + Name string `json:"name"` + Port int `json:"port"` + RootDir string `json:"rootDir"` +} + +type Wiremock struct { + Services []Service `json:"services"` +} + +func NewService(root, domain string, port int) Service { + return Service{Name: domain, Port: port, RootDir: filepath.Join(root, domain)} +} diff --git a/pkg/wiremock/config/ports.go b/pkg/wiremock/config/ports.go new file mode 100644 index 0000000..6ed60a6 --- /dev/null +++ b/pkg/wiremock/config/ports.go @@ -0,0 +1,42 @@ +package config + +import ( + "sort" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/sliceutils" +) + +type Ports []int + +func GatherPorts(wiremock Wiremock) Ports { + var ports Ports + + for _, service := range wiremock.Services { + ports = append(ports, service.Port) + } + + sort.Slice(ports, func(i, j int) bool { + return ports[i] < ports[j] + }) + + return ports +} + +func (p Ports) Allocate() int { + const defaultPort = 8000 + + if len(p) == 0 { + return defaultPort + } + + var ports Ports + for _, port := range p { + ports = append(ports, port) + } + + sort.Slice(ports, func(i, j int) bool { + return ports[i] > ports[j] + }) + + return sliceutils.FirstOf(ports) + 1 +} diff --git a/pkg/wiremock/config/ports_test.go b/pkg/wiremock/config/ports_test.go new file mode 100644 index 0000000..cb0f9ca --- /dev/null +++ b/pkg/wiremock/config/ports_test.go @@ -0,0 +1,26 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPorts_Allocate(t *testing.T) { + tests := []struct { + name string + p Ports + want int + }{ + {p: Ports{8000, 8002, 8003}, want: 8004}, + {p: Ports{8007}, want: 8008}, + {p: Ports{}, want: 8000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.p.Allocate() + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/wiremock/configopener/open.go b/pkg/wiremock/configopener/open.go new file mode 100644 index 0000000..93caf3d --- /dev/null +++ b/pkg/wiremock/configopener/open.go @@ -0,0 +1,87 @@ +package configopener + +import ( + "io" + "log" + "path/filepath" + "strconv" + "strings" + + supervisorconf "github.com/ochinchina/supervisord/config" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +type opener struct { + fs afero.Fs + + path string +} + +func New(fs afero.Fs, path string) *opener { + return &opener{fs: fs, path: path} +} + +func (o *opener) Open() (config.Wiremock, error) { + const configName = "supervisord.conf" + + var wiremock config.Wiremock + + path := filepath.Join(o.path, configName) + supervisordConf := supervisorconf.NewConfig(path) + + logrus.SetOutput(io.Discard) + + _, err := supervisordConf.Load() + if err != nil { + return wiremock, err + } + + for _, p := range supervisordConf.GetPrograms() { + envs := convertEnvs(p.GetEnv("environment")) + + name, exists := envs["NAME"] + if !exists { + continue + } + + root, exists := envs["ROOT"] + if !exists { + continue + } + + port, exists := envs["PORT"] + if !exists { + continue + } + + portInt, err := strconv.Atoi(port) + if err != nil { + continue + } + + wiremock.Services = append(wiremock.Services, config.Service{ + Name: name, RootDir: root, Port: portInt}) + } + + return wiremock, nil +} + +func convertEnvs(envs []string) map[string]string { + const separator = "=" + converted := map[string]string{} + + for _, env := range envs { + parts := strings.Split(env, separator) + if len(parts) != 2 { + log.Println("parse env error:", env) + continue + } + + converted[parts[0]] = parts[1] + } + + return converted +} diff --git a/pkg/wiremock/configopener/open_test.go b/pkg/wiremock/configopener/open_test.go new file mode 100644 index 0000000..4840040 --- /dev/null +++ b/pkg/wiremock/configopener/open_test.go @@ -0,0 +1,80 @@ +package configopener + +import ( + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +var osfs = afero.NewOsFs() + +var projectPath = filepath.Join(fsutils.CurrentDir(), "../../..") + +func Test_opener_Open(t *testing.T) { + tests := []struct { + name string + fs afero.Fs + path string + want config.Wiremock + wantErr bool + }{ + { + fs: osfs, + path: filepath.Join(projectPath, "static/tests/data/supervisord/simple"), + want: config.Wiremock{}, + }, + { + fs: osfs, + path: filepath.Join(projectPath, "static/tests/data/supervisord/empty-dir"), + want: config.Wiremock{}, + }, + { + fs: osfs, + path: filepath.Join(projectPath, "static/tests/data/supervisord/with-includes"), + want: config.Wiremock{Services: []config.Service{{Port: 8000, Name: "awesome", RootDir: "/home/mock/awesome"}}}, + }, + { + fs: osfs, + path: filepath.Join(projectPath, "static/tests/data/supervisord/two-services"), + want: config.Wiremock{Services: []config.Service{ + {Port: 8000, Name: "awesome", RootDir: "/home/mock/awesome"}, + {Port: 8001, Name: "push-sender", RootDir: "/home/mock/push-sender"}, + }}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &opener{fs: tt.fs, path: tt.path} + got, err := o.Open() + require.NoError(t, err) + require.ElementsMatch(t, got.Services, tt.want.Services) + }) + } +} + +func Test_convertEnvs(t *testing.T) { + tests := []struct { + name string + envs []string + want map[string]string + }{ + {envs: []string{}, want: map[string]string{}}, + {envs: []string{"A:c"}, want: map[string]string{}}, + {envs: []string{"A=c=d"}, want: map[string]string{}}, + {envs: []string{"A=c"}, want: map[string]string{"A": "c"}}, + {envs: []string{"A=c", "ac=123"}, want: map[string]string{"A": "c", "ac": "123"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := convertEnvs(tt.envs) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/wiremock/configsync/sync.go b/pkg/wiremock/configsync/sync.go new file mode 100644 index 0000000..a3674fe --- /dev/null +++ b/pkg/wiremock/configsync/sync.go @@ -0,0 +1,46 @@ +package configsync + +import ( + "fmt" + "path/filepath" + + "github.com/spf13/afero" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +func SyncWiremockConfig(fs afero.Fs, wiremockConfig config.Wiremock, wiremockPath string) (config.Wiremock, error) { + domainDirectories, err := fsutils.GatherDirs(fs, wiremockPath) + if err != nil { + return config.Wiremock{}, fmt.Errorf("gather domain directories: %w", err) + } + + nextAvailablePort := config.GatherPorts(wiremockConfig).Allocate() + + domainToService := map[string]config.Service{} + targetDomainToService := map[string]config.Service{} + + for _, service := range wiremockConfig.Services { + domainToService[service.Name] = service + } + + for _, domainDir := range domainDirectories { + domain := filepath.Base(domainDir.Name()) + + service, exists := domainToService[domain] + if exists { + targetDomainToService[domain] = service + } else { + targetDomainToService[domain] = config.NewService(wiremockPath, domain, nextAvailablePort) + nextAvailablePort++ + } + } + + var targetWiremockConfig config.Wiremock + for _, service := range targetDomainToService { + targetWiremockConfig.Services = append(targetWiremockConfig.Services, service) + } + + return targetWiremockConfig, nil +} diff --git a/pkg/wiremock/configsync/sync_test.go b/pkg/wiremock/configsync/sync_test.go new file mode 100644 index 0000000..c03c83f --- /dev/null +++ b/pkg/wiremock/configsync/sync_test.go @@ -0,0 +1,167 @@ +package configsync + +import ( + "path" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + "github.com/SberMarket-Tech/grpc-wiremock/pkg/utils/fsutils" + "github.com/SberMarket-Tech/grpc-wiremock/pkg/wiremock/config" +) + +var ( + osfs = afero.NewOsFs() + projectDir = path.Join(fsutils.CurrentDir(), "../../..") +) + +func TestSyncWiremockConfig(t *testing.T) { + tests := []struct { + name string + + fs afero.Fs + wiremockPath string + templatePath string + + source config.Wiremock + want config.Wiremock + }{ + { + fs: osfs, + wiremockPath: filepath.Join(projectDir, "tests", "two-services"), + templatePath: filepath.Join(projectDir, "static/tests/data/wiremock/with-domains"), + source: config.Wiremock{ + Services: []config.Service{ + {Name: "awesome", Port: 8000, RootDir: "/home/mock/awesome"}, + {Name: "push-sender", Port: 8001, RootDir: "/home/mock/push-sender"}, + }, + }, + want: config.Wiremock{ + Services: []config.Service{ + {Name: "awesome", Port: 8000, RootDir: "/home/mock/awesome"}, + {Name: "push-sender", Port: 8001, RootDir: "/home/mock/push-sender"}, + }, + }, + }, + { + wiremockPath: filepath.Join(projectDir, "tests", "two-services"), + templatePath: filepath.Join(projectDir, "static/tests/data/wiremock/with-domains"), + + fs: osfs, + source: config.Wiremock{ + Services: []config.Service{ + {Name: "domain1", Port: 8001, RootDir: "/home/mock/domain1"}, + {Name: "domain2", Port: 8002, RootDir: "/home/mock/domain2"}, + }, + }, + want: config.Wiremock{ + Services: []config.Service{ + {Name: "awesome", Port: 8003, RootDir: filepath.Join(projectDir, "tests/two-services/awesome")}, + {Name: "push-sender", Port: 8004, RootDir: filepath.Join(projectDir, "tests/two-services/push-sender")}, + }, + }, + }, + { + wiremockPath: filepath.Join(projectDir, "tests", "two-services"), + templatePath: filepath.Join(projectDir, "static/tests/data/wiremock/with-domains"), + + fs: osfs, + source: config.Wiremock{ + Services: []config.Service{ + {Name: "awesome", Port: 8000, RootDir: "/home/mock/awesome"}, + {Name: "domain2", Port: 8002, RootDir: "/home/mock/domain2"}, + }, + }, + want: config.Wiremock{ + Services: []config.Service{ + {Name: "awesome", Port: 8000, RootDir: "/home/mock/awesome"}, + {Name: "push-sender", Port: 8003, RootDir: filepath.Join(projectDir, "tests/two-services/push-sender")}, + }, + }, + }, + { + wiremockPath: filepath.Join(projectDir, "tests", "two-services"), + templatePath: filepath.Join(projectDir, "static/tests/data/wiremock/with-domains"), + + fs: osfs, + source: config.Wiremock{}, + want: config.Wiremock{ + Services: []config.Service{ + {Name: "awesome", Port: 8000, RootDir: filepath.Join(projectDir, "tests/two-services/awesome")}, + {Name: "push-sender", Port: 8001, RootDir: filepath.Join(projectDir, "tests/two-services/push-sender")}, + }, + }, + }, + } + + for _, tt := range tests { + err := fsutils.CopyDir(osfs, osfs, tt.templatePath, tt.wiremockPath, false) + require.NoError(t, err) + + t.Run(tt.name, func(t *testing.T) { + got, err := SyncWiremockConfig(tt.fs, tt.source, tt.wiremockPath) + require.NoError(t, err) + + require.ElementsMatch(t, got.Services, tt.want.Services) + }) + } +} + +func TestSyncWiremockConfig_Dynamic(t *testing.T) { + testCase := struct { + name string + + fs afero.Fs + + wiremockPath string + templatePath string + + sourceConfig config.Wiremock + wantBeforeDelete config.Wiremock + wantAfterDelete config.Wiremock + }{ + fs: osfs, + wiremockPath: filepath.Join(projectDir, "tests", "two-services"), + templatePath: filepath.Join(projectDir, "static/tests/data/wiremock/with-domains"), + + sourceConfig: config.Wiremock{ + Services: []config.Service{ + {Name: "awesome", Port: 8000, RootDir: "/home/mock/awesome"}, + {Name: "domain2", Port: 8002, RootDir: "/home/mock/domain2"}, + }, + }, + wantBeforeDelete: config.Wiremock{ + Services: []config.Service{ + {Name: "awesome", Port: 8000, RootDir: "/home/mock/awesome"}, + {Name: "push-sender", Port: 8003, RootDir: filepath.Join(projectDir, "tests/two-services/push-sender")}, + }, + }, + wantAfterDelete: config.Wiremock{ + Services: []config.Service{ + {Name: "push-sender", Port: 8003, RootDir: filepath.Join(projectDir, "tests/two-services/push-sender")}, + }, + }, + } + + err := fsutils.CopyDir(testCase.fs, testCase.fs, testCase.templatePath, testCase.wiremockPath, false) + require.NoError(t, err) + + t.Run(testCase.name, func(t *testing.T) { + got, err := SyncWiremockConfig(testCase.fs, testCase.sourceConfig, testCase.wiremockPath) + require.NoError(t, err) + + require.ElementsMatch(t, got.Services, testCase.wantBeforeDelete.Services) + }) + + err = testCase.fs.RemoveAll(filepath.Join(testCase.wiremockPath, "awesome")) + require.NoError(t, err) + + t.Run(testCase.name, func(t *testing.T) { + got, err := SyncWiremockConfig(testCase.fs, testCase.wantBeforeDelete, testCase.wiremockPath) + require.NoError(t, err) + + require.ElementsMatch(t, got.Services, testCase.wantAfterDelete.Services) + }) +} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..7fd7d0b --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euo pipefail + +log_header "Init Multi-API" "${ENTRYPOINT_HEADER}" +bash "${MULTIAPI}/init.sh" + +log_header "Install grpc-to-http proxy" "${ENTRYPOINT_HEADER}" +bash "${PROXY}/init.sh" + +# After mockgen task. +#log_header "Setup mocks" "${ENTRYPOINT_HEADER}" +#bash "${MOCKS}/init.sh" + +log_header "Install certificates. Setup nginx" "${ENTRYPOINT_HEADER}" +bash "${ROUTING}/init.sh" + +log_header "Initialization is done" "${ENTRYPOINT_HEADER}" +bash "${ROUTING}/logs.sh" diff --git a/scripts/init.sh b/scripts/init.sh new file mode 100644 index 0000000..e90a886 --- /dev/null +++ b/scripts/init.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +set -euo pipefail + +# User's host directories mounted as volume. +export GW_WIREMOCK_PATH="/home/mock" +export GW_CONTRACTS_PATH="/contracts" +export GW_CERTS_PATH="/etc/ssl/mock/share" + +SCRIPTS=$(realpath "$(dirname "${0}")") + +# grpc-wiremock setup scripts variables. +export SCRIPTS="${SCRIPTS}" +export MOCKS="${SCRIPTS}/mocks" +export PROXY="${SCRIPTS}/proxy" +export MULTIAPI="${SCRIPTS}/multiapi" +export OTHER="${SCRIPTS}/other" +export ROUTING="${SCRIPTS}/routing" +export CERTS="${SCRIPTS}/routing/certs" + +export WIREMOCK_ADDR="localhost:9000" + +# Headers for rsyslog. +export ENTRYPOINT_HEADER="gw.entrypoint" +export WIREMOCK_RUN_HEADER="gw.wiremock.run" +export PROXY_GEN_HEADER="gw.proxy.gen" +export PROXY_WATCH_HEADER="gw.proxy.watch" +export MOCKS_GEN_HEADER="gw.mocks.gen" +export MOCKS_WATCH_HEADER="gw.mocks.watch" +export ROUTING_CERTS_GEN_HEADER="gw.routing.certs.gen" +export ROUTING_NGINX_GEN_HEADER="gw.routing.nginx.gen" +export ROUTING_NGINX_WATCH_HEADER="gw.routing.nginx.watch" +export ROUTING_NGINX_LOGS_HEADER="gw.routing.nginx.logs" +export MULTIAPI_LOGS_HEADER="gw.multiapi.supervisord.logs" + +# Change owner of directories. +MOCK_CERTS_PATH="/etc/ssl/mock" +LOGS_SUPERVISORD_PATH="/var/log/supervisord" + +USER_ID=1000 + +sudo mkdir -p "${LOGS_SUPERVISORD_PATH}" + +sudo chown -R \ + "${USER_ID}:${USER_ID}" \ + "${GW_WIREMOCK_PATH}" \ + "${GW_CONTRACTS_PATH}" \ + "${LOGS_SUPERVISORD_PATH}" \ + "${MOCK_CERTS_PATH}" + +# Setup logger. +LOG="/var/log/wiremock" + +## remove 'imklog' because no need to monitor kernel events. +sudo sed -i '/imklog/s/^/#/' /etc/rsyslog.conf && sudo rsyslogd +sudo touch "${LOG}" && tail -f ${LOG} & + +# Include utilities. +source "${SCRIPTS}/other/log.sh" + +# Run grpc-wiremock entrypoint. +source "${SCRIPTS}/entrypoint.sh" diff --git a/scripts/mocks/init.sh b/scripts/mocks/init.sh new file mode 100644 index 0000000..3cc6daa --- /dev/null +++ b/scripts/mocks/init.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -euo pipefail + +DOMAINS_PATH="${GW_CONTRACTS_PATH}/services" + +if ! mockgen first-run \ + --domains-path="${DOMAINS_PATH}" \ + --wiremock-path="${GW_WIREMOCK_PATH}"; then + + echo "Mocks autogen exited with an error. Skip" +fi diff --git a/scripts/multiapi/init.sh b/scripts/multiapi/init.sh new file mode 100644 index 0000000..43463d9 --- /dev/null +++ b/scripts/multiapi/init.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +ETC_SUPERVISORD_PATH="/etc/supervisord" + +supervisord -d -c "${ETC_SUPERVISORD_PATH}/supervisord.conf" diff --git a/scripts/other/check_dependencies.sh b/scripts/other/check_dependencies.sh new file mode 100644 index 0000000..2518b1d --- /dev/null +++ b/scripts/other/check_dependencies.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +set -euo pipefail + +echo "===== Dependencies check is started..." + +if command -v protoc > /dev/null; then + echo "- proto compiler [OK]" +else + echo "- proto compiler [FAIL]" + echo "To install proto compiler: https://grpc.io/docs/protoc-installation" +fi + +GO="protoc-gen-go" +if command -v ${GO} > /dev/null; then + echo "- ${GO} plugin [OK]" +else + echo "- ${GO} plugin [FAIL]" + echo "To install ${GO} plugin:" + echo "- go install google.golang.org/protobuf/cmd/${GO}@latest" +fi + +GO_GRPC="protoc-gen-go-grpc" +if command -v ${GO_GRPC} > /dev/null; then + echo "- ${GO_GRPC} plugin [OK]" +else + echo "- ${GO_GRPC} plugin [FAIL]" + echo "To install ${GO_GRPC} plugin:" + echo "- go install google.golang.org/grpc/cmd/${GO_GRPC}@latest" +fi + +OPENAPI="protoc-gen-openapi" +if command -v ${OPENAPI} > /dev/null; then + echo "- ${OPENAPI} plugin [OK]" +else + echo "- ${OPENAPI} plugin [FAIL]" + echo "To install ${OPENAPI} plugin:" + echo "- https://github.com/solo-io/${OPENAPI}" +fi + +echo "===== Dependencies check is done." \ No newline at end of file diff --git a/scripts/other/log.sh b/scripts/other/log.sh new file mode 100644 index 0000000..d9e4030 --- /dev/null +++ b/scripts/other/log.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -euo pipefail + +log_header() { + local message="${1}" header="${2}" + + echo "▛ ▞ ▟ ${message}" \ + | tr '[a-z]' '[A-Z]' \ + | logger -t "${header}" +} \ No newline at end of file diff --git a/scripts/other/wait_for_it.sh b/scripts/other/wait_for_it.sh new file mode 100644 index 0000000..d990e0d --- /dev/null +++ b/scripts/other/wait_for_it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/scripts/proxy/init.sh b/scripts/proxy/init.sh new file mode 100644 index 0000000..212109c --- /dev/null +++ b/scripts/proxy/init.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -euo pipefail + +bash "${PROXY}/watch.sh" 2>&1 | logger -t "${PROXY_WATCH_HEADER}" & \ No newline at end of file diff --git a/scripts/proxy/install.sh b/scripts/proxy/install.sh new file mode 100644 index 0000000..7e5148b --- /dev/null +++ b/scripts/proxy/install.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -euo pipefail + +PROXY_HOST="http://localhost" +PROXY_INPUT_PORT="3010" +PROXY_OUTPUT_PORT="80" +PROXY_BASE_URL="${PROXY_HOST}:${PROXY_OUTPUT_PORT}" +PROXY_OUTPUT_PATH="/var/proxy/proxy" + +# Kill previous proxy instance if exists. +PID=$(sudo lsof -t -i:${PROXY_INPUT_PORT}) || true +if [ ! -z "${PID}" ]; then + kill -9 "${PID}" +fi + +# Generate new proxy. +rm -rf ${PROXY_OUTPUT_PATH} +mkdir -p ${PROXY_OUTPUT_PATH} + +grpc2http \ + --input "${GW_CONTRACTS_PATH}" \ + --output "${PROXY_OUTPUT_PATH}" \ + --base-url "${PROXY_BASE_URL}" + +# Install new proxy. +make -C "${PROXY_OUTPUT_PATH}" install diff --git a/scripts/proxy/run.sh b/scripts/proxy/run.sh new file mode 100644 index 0000000..c1d0086 --- /dev/null +++ b/scripts/proxy/run.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -euo pipefail + +bash "${PROXY}/install.sh" 2>&1 | logger -t "${PROXY_GEN_HEADER}" \ No newline at end of file diff --git a/scripts/proxy/watch.sh b/scripts/proxy/watch.sh new file mode 100644 index 0000000..b6a5e45 --- /dev/null +++ b/scripts/proxy/watch.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +# Watch contracts and restart grpc-to-http proxy. +CompileDaemon \ + -run-dir="/var/proxy" \ + -directory="${GW_CONTRACTS_PATH}" \ + -build="${PROXY}/run.sh" \ + -command="grpc-to-http-proxy" \ + -pattern="(.+\\.)proto$" \ + -log-prefix=false \ + -graceful-kill -graceful-timeout=3 diff --git a/scripts/routing/init.sh b/scripts/routing/init.sh new file mode 100644 index 0000000..82df4bc --- /dev/null +++ b/scripts/routing/init.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euo pipefail + +if ! certgen | logger -t "${ROUTING_CERTS_GEN_HEADER}"; then + echo "Certificates generator exited with an error. Skip" +fi + +sudo nginx + +if ! confgen | logger -t "${ROUTING_NGINX_GEN_HEADER}"; then + echo "Nginx configs generator exited with an error. Skip" +fi + diff --git a/scripts/routing/logs.sh b/scripts/routing/logs.sh new file mode 100644 index 0000000..000dcf8 --- /dev/null +++ b/scripts/routing/logs.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -euo pipefail + +sudo touch /var/log/nginx/{access,error,not_mocked}.log +tail -f /var/log/nginx/* | logger -t "${ROUTING_NGINX_LOGS_HEADER}" diff --git a/scripts/run_wiremock.sh b/scripts/run_wiremock.sh new file mode 100644 index 0000000..e282854 --- /dev/null +++ b/scripts/run_wiremock.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +bash /docker-entrypoint.sh 2>&1 | logger -t "${WIREMOCK_RUN_HEADER}" & + +bash "${OTHER}/wait_for_it.sh" "${WIREMOCK_ADDR}" | logger -t "${WIREMOCK_RUN_HEADER}" \ No newline at end of file diff --git a/static/health/health.proto b/static/health/health.proto new file mode 100644 index 0000000..f5c37cd --- /dev/null +++ b/static/health/health.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package grpc.health.v1; + +option go_package = "grpc-proxy/pkg/gitlab.sbmt.io/paas/health"; + +message HealthCheckRequest { + string service = 1; +} + +message HealthCheckResponse { + enum ServingStatus { + UNKNOWN = 0; + SERVING = 1; + NOT_SERVING = 2; + } + ServingStatus status = 1; +} + +service Health { + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); + rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); +} \ No newline at end of file diff --git a/static/proto-annotations/google/api/annotations.proto b/static/proto-annotations/google/api/annotations.proto new file mode 100644 index 0000000..efdab3d --- /dev/null +++ b/static/proto-annotations/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 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/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/static/proto-annotations/google/api/http.proto b/static/proto-annotations/google/api/http.proto new file mode 100644 index 0000000..113fa93 --- /dev/null +++ b/static/proto-annotations/google/api/http.proto @@ -0,0 +1,375 @@ +// Copyright 2015 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/static/proto-includes/google/protobuf/any.proto b/static/proto-includes/google/protobuf/any.proto new file mode 100644 index 0000000..6ed8a23 --- /dev/null +++ b/static/proto-includes/google/protobuf/any.proto @@ -0,0 +1,158 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option go_package = "google.golang.org/protobuf/types/known/anypb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "AnyProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// `Any` contains an arbitrary serialized protocol buffer message along with a +// URL that describes the type of the serialized message. +// +// Protobuf library provides support to pack/unpack Any values in the form +// of utility functions or additional generated methods of the Any type. +// +// Example 1: Pack and unpack a message in C++. +// +// Foo foo = ...; +// Any any; +// any.PackFrom(foo); +// ... +// if (any.UnpackTo(&foo)) { +// ... +// } +// +// Example 2: Pack and unpack a message in Java. +// +// Foo foo = ...; +// Any any = Any.pack(foo); +// ... +// if (any.is(Foo.class)) { +// foo = any.unpack(Foo.class); +// } +// +// Example 3: Pack and unpack a message in Python. +// +// foo = Foo(...) +// any = Any() +// any.Pack(foo) +// ... +// if any.Is(Foo.DESCRIPTOR): +// any.Unpack(foo) +// ... +// +// Example 4: Pack and unpack a message in Go +// +// foo := &pb.Foo{...} +// any, err := anypb.New(foo) +// if err != nil { +// ... +// } +// ... +// foo := &pb.Foo{} +// if err := any.UnmarshalTo(foo); err != nil { +// ... +// } +// +// The pack methods provided by protobuf library will by default use +// 'type.googleapis.com/full.type.name' as the type URL and the unpack +// methods only use the fully qualified type name after the last '/' +// in the type URL, for example "foo.bar.com/x/y.z" will yield type +// name "y.z". +// +// +// JSON +// ==== +// The JSON representation of an `Any` value uses the regular +// representation of the deserialized, embedded message, with an +// additional field `@type` which contains the type URL. Example: +// +// package google.profile; +// message Person { +// string first_name = 1; +// string last_name = 2; +// } +// +// { +// "@type": "type.googleapis.com/google.profile.Person", +// "firstName": , +// "lastName": +// } +// +// If the embedded message type is well-known and has a custom JSON +// representation, that representation will be embedded adding a field +// `value` which holds the custom JSON in addition to the `@type` +// field. Example (for message [google.protobuf.Duration][]): +// +// { +// "@type": "type.googleapis.com/google.protobuf.Duration", +// "value": "1.212s" +// } +// +message Any { + // A URL/resource name that uniquely identifies the type of the serialized + // protocol buffer message. This string must contain at least + // one "/" character. The last segment of the URL's path must represent + // the fully qualified name of the type (as in + // `path/google.protobuf.Duration`). The name should be in a canonical form + // (e.g., leading "." is not accepted). + // + // In practice, teams usually precompile into the binary all types that they + // expect it to use in the context of Any. However, for URLs which use the + // scheme `http`, `https`, or no scheme, one can optionally set up a type + // server that maps type URLs to message definitions as follows: + // + // * If no scheme is provided, `https` is assumed. + // * An HTTP GET on the URL must yield a [google.protobuf.Type][] + // value in binary format, or produce an error. + // * Applications are allowed to cache lookup results based on the + // URL, or have them precompiled into a binary to avoid any + // lookup. Therefore, binary compatibility needs to be preserved + // on changes to types. (Use versioned type names to manage + // breaking changes.) + // + // Note: this functionality is not currently available in the official + // protobuf release, and it is not used for type URLs beginning with + // type.googleapis.com. + // + // Schemes other than `http`, `https` (or the empty scheme) might be + // used with implementation specific semantics. + // + string type_url = 1; + + // Must be a valid serialized protocol buffer of the above specified type. + bytes value = 2; +} diff --git a/static/proto-includes/google/protobuf/api.proto b/static/proto-includes/google/protobuf/api.proto new file mode 100644 index 0000000..3d598fc --- /dev/null +++ b/static/proto-includes/google/protobuf/api.proto @@ -0,0 +1,208 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +import "google/protobuf/source_context.proto"; +import "google/protobuf/type.proto"; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "ApiProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option go_package = "google.golang.org/protobuf/types/known/apipb"; + +// Api is a light-weight descriptor for an API Interface. +// +// Interfaces are also described as "protocol buffer services" in some contexts, +// such as by the "service" keyword in a .proto file, but they are different +// from API Services, which represent a concrete implementation of an interface +// as opposed to simply a description of methods and bindings. They are also +// sometimes simply referred to as "APIs" in other contexts, such as the name of +// this message itself. See https://cloud.google.com/apis/design/glossary for +// detailed terminology. +message Api { + // The fully qualified name of this interface, including package name + // followed by the interface's simple name. + string name = 1; + + // The methods of this interface, in unspecified order. + repeated Method methods = 2; + + // Any metadata attached to the interface. + repeated Option options = 3; + + // A version string for this interface. If specified, must have the form + // `major-version.minor-version`, as in `1.10`. If the minor version is + // omitted, it defaults to zero. If the entire version field is empty, the + // major version is derived from the package name, as outlined below. If the + // field is not empty, the version in the package name will be verified to be + // consistent with what is provided here. + // + // The versioning schema uses [semantic + // versioning](http://semver.org) where the major version number + // indicates a breaking change and the minor version an additive, + // non-breaking change. Both version numbers are signals to users + // what to expect from different versions, and should be carefully + // chosen based on the product plan. + // + // The major version is also reflected in the package name of the + // interface, which must end in `v`, as in + // `google.feature.v1`. For major versions 0 and 1, the suffix can + // be omitted. Zero major versions must only be used for + // experimental, non-GA interfaces. + // + // + string version = 4; + + // Source context for the protocol buffer service represented by this + // message. + SourceContext source_context = 5; + + // Included interfaces. See [Mixin][]. + repeated Mixin mixins = 6; + + // The source syntax of the service. + Syntax syntax = 7; +} + +// Method represents a method of an API interface. +message Method { + // The simple name of this method. + string name = 1; + + // A URL of the input message type. + string request_type_url = 2; + + // If true, the request is streamed. + bool request_streaming = 3; + + // The URL of the output message type. + string response_type_url = 4; + + // If true, the response is streamed. + bool response_streaming = 5; + + // Any metadata attached to the method. + repeated Option options = 6; + + // The source syntax of this method. + Syntax syntax = 7; +} + +// Declares an API Interface to be included in this interface. The including +// interface must redeclare all the methods from the included interface, but +// documentation and options are inherited as follows: +// +// - If after comment and whitespace stripping, the documentation +// string of the redeclared method is empty, it will be inherited +// from the original method. +// +// - Each annotation belonging to the service config (http, +// visibility) which is not set in the redeclared method will be +// inherited. +// +// - If an http annotation is inherited, the path pattern will be +// modified as follows. Any version prefix will be replaced by the +// version of the including interface plus the [root][] path if +// specified. +// +// Example of a simple mixin: +// +// package google.acl.v1; +// service AccessControl { +// // Get the underlying ACL object. +// rpc GetAcl(GetAclRequest) returns (Acl) { +// option (google.api.http).get = "/v1/{resource=**}:getAcl"; +// } +// } +// +// package google.storage.v2; +// service Storage { +// rpc GetAcl(GetAclRequest) returns (Acl); +// +// // Get a data record. +// rpc GetData(GetDataRequest) returns (Data) { +// option (google.api.http).get = "/v2/{resource=**}"; +// } +// } +// +// Example of a mixin configuration: +// +// apis: +// - name: google.storage.v2.Storage +// mixins: +// - name: google.acl.v1.AccessControl +// +// The mixin construct implies that all methods in `AccessControl` are +// also declared with same name and request/response types in +// `Storage`. A documentation generator or annotation processor will +// see the effective `Storage.GetAcl` method after inheriting +// documentation and annotations as follows: +// +// service Storage { +// // Get the underlying ACL object. +// rpc GetAcl(GetAclRequest) returns (Acl) { +// option (google.api.http).get = "/v2/{resource=**}:getAcl"; +// } +// ... +// } +// +// Note how the version in the path pattern changed from `v1` to `v2`. +// +// If the `root` field in the mixin is specified, it should be a +// relative path under which inherited HTTP paths are placed. Example: +// +// apis: +// - name: google.storage.v2.Storage +// mixins: +// - name: google.acl.v1.AccessControl +// root: acls +// +// This implies the following inherited HTTP annotation: +// +// service Storage { +// // Get the underlying ACL object. +// rpc GetAcl(GetAclRequest) returns (Acl) { +// option (google.api.http).get = "/v2/acls/{resource=**}:getAcl"; +// } +// ... +// } +message Mixin { + // The fully qualified name of the interface which is included. + string name = 1; + + // If non-empty specifies a path under which inherited HTTP paths + // are rooted. + string root = 2; +} diff --git a/static/proto-includes/google/protobuf/compiler/plugin.proto b/static/proto-includes/google/protobuf/compiler/plugin.proto new file mode 100644 index 0000000..9242aac --- /dev/null +++ b/static/proto-includes/google/protobuf/compiler/plugin.proto @@ -0,0 +1,183 @@ +// 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) +// +// WARNING: The plugin interface is currently EXPERIMENTAL and is subject to +// change. +// +// protoc (aka the Protocol Compiler) can be extended via plugins. A plugin is +// just a program that reads a CodeGeneratorRequest from stdin and writes a +// CodeGeneratorResponse to stdout. +// +// Plugins written using C++ can use google/protobuf/compiler/plugin.h instead +// of dealing with the raw protocol defined here. +// +// A plugin executable needs only to be placed somewhere in the path. The +// plugin should be named "protoc-gen-$NAME", and will then be used when the +// flag "--${NAME}_out" is passed to protoc. + +syntax = "proto2"; + +package google.protobuf.compiler; +option java_package = "com.google.protobuf.compiler"; +option java_outer_classname = "PluginProtos"; + +option go_package = "google.golang.org/protobuf/types/pluginpb"; + +import "google/protobuf/descriptor.proto"; + +// The version number of protocol compiler. +message Version { + optional int32 major = 1; + optional int32 minor = 2; + optional int32 patch = 3; + // A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should + // be empty for mainline stable releases. + optional string suffix = 4; +} + +// An encoded CodeGeneratorRequest is written to the plugin's stdin. +message CodeGeneratorRequest { + // The .proto files that were explicitly listed on the command-line. The + // code generator should generate code only for these files. Each file's + // descriptor will be included in proto_file, below. + repeated string file_to_generate = 1; + + // The generator parameter passed on the command-line. + optional string parameter = 2; + + // FileDescriptorProtos for all files in files_to_generate and everything + // they import. The files will appear in topological order, so each file + // appears before any file that imports it. + // + // protoc guarantees that all proto_files will be written after + // the fields above, even though this is not technically guaranteed by the + // protobuf wire format. This theoretically could allow a plugin to stream + // in the FileDescriptorProtos and handle them one by one rather than read + // the entire set into memory at once. However, as of this writing, this + // is not similarly optimized on protoc's end -- it will store all fields in + // memory at once before sending them to the plugin. + // + // Type names of fields and extensions in the FileDescriptorProto are always + // fully qualified. + repeated FileDescriptorProto proto_file = 15; + + // The version number of protocol compiler. + optional Version compiler_version = 3; + +} + +// The plugin writes an encoded CodeGeneratorResponse to stdout. +message CodeGeneratorResponse { + // Error message. If non-empty, code generation failed. The plugin process + // should exit with status code zero even if it reports an error in this way. + // + // This should be used to indicate errors in .proto files which prevent the + // code generator from generating correct code. Errors which indicate a + // problem in protoc itself -- such as the input CodeGeneratorRequest being + // unparseable -- should be reported by writing a message to stderr and + // exiting with a non-zero status code. + optional string error = 1; + + // A bitmask of supported features that the code generator supports. + // This is a bitwise "or" of values from the Feature enum. + optional uint64 supported_features = 2; + + // Sync with code_generator.h. + enum Feature { + FEATURE_NONE = 0; + FEATURE_PROTO3_OPTIONAL = 1; + } + + // Represents a single generated file. + message File { + // The file name, relative to the output directory. The name must not + // contain "." or ".." components and must be relative, not be absolute (so, + // the file cannot lie outside the output directory). "/" must be used as + // the path separator, not "\". + // + // If the name is omitted, the content will be appended to the previous + // file. This allows the generator to break large files into small chunks, + // and allows the generated text to be streamed back to protoc so that large + // files need not reside completely in memory at one time. Note that as of + // this writing protoc does not optimize for this -- it will read the entire + // CodeGeneratorResponse before writing files to disk. + optional string name = 1; + + // If non-empty, indicates that the named file should already exist, and the + // content here is to be inserted into that file at a defined insertion + // point. This feature allows a code generator to extend the output + // produced by another code generator. The original generator may provide + // insertion points by placing special annotations in the file that look + // like: + // @@protoc_insertion_point(NAME) + // The annotation can have arbitrary text before and after it on the line, + // which allows it to be placed in a comment. NAME should be replaced with + // an identifier naming the point -- this is what other generators will use + // as the insertion_point. Code inserted at this point will be placed + // immediately above the line containing the insertion point (thus multiple + // insertions to the same point will come out in the order they were added). + // The double-@ is intended to make it unlikely that the generated code + // could contain things that look like insertion points by accident. + // + // For example, the C++ code generator places the following line in the + // .pb.h files that it generates: + // // @@protoc_insertion_point(namespace_scope) + // This line appears within the scope of the file's package namespace, but + // outside of any particular class. Another plugin can then specify the + // insertion_point "namespace_scope" to generate additional classes or + // other declarations that should be placed in this scope. + // + // Note that if the line containing the insertion point begins with + // whitespace, the same whitespace will be added to every line of the + // inserted text. This is useful for languages like Python, where + // indentation matters. In these languages, the insertion point comment + // should be indented the same amount as any inserted code will need to be + // in order to work correctly in that context. + // + // The code generator that generates the initial file and the one which + // inserts into it must both run as part of a single invocation of protoc. + // Code generators are executed in the order in which they appear on the + // command line. + // + // If |insertion_point| is present, |name| must also be present. + optional string insertion_point = 2; + + // The file contents. + optional string content = 15; + + // Information describing the file content being inserted. If an insertion + // point is used, this information will be appropriately offset and inserted + // into the code generation metadata for the generated files. + optional GeneratedCodeInfo generated_code_info = 16; + } + repeated File file = 15; +} diff --git a/static/proto-includes/google/protobuf/descriptor.proto b/static/proto-includes/google/protobuf/descriptor.proto new file mode 100644 index 0000000..156e410 --- /dev/null +++ b/static/proto-includes/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. 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; + } +} diff --git a/static/proto-includes/google/protobuf/duration.proto b/static/proto-includes/google/protobuf/duration.proto new file mode 100644 index 0000000..81c3e36 --- /dev/null +++ b/static/proto-includes/google/protobuf/duration.proto @@ -0,0 +1,116 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/durationpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DurationProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// A Duration represents a signed, fixed-length span of time represented +// as a count of seconds and fractions of seconds at nanosecond +// resolution. It is independent of any calendar and concepts like "day" +// or "month". It is related to Timestamp in that the difference between +// two Timestamp values is a Duration and it can be added or subtracted +// from a Timestamp. Range is approximately +-10,000 years. +// +// # Examples +// +// Example 1: Compute Duration from two Timestamps in pseudo code. +// +// Timestamp start = ...; +// Timestamp end = ...; +// Duration duration = ...; +// +// duration.seconds = end.seconds - start.seconds; +// duration.nanos = end.nanos - start.nanos; +// +// if (duration.seconds < 0 && duration.nanos > 0) { +// duration.seconds += 1; +// duration.nanos -= 1000000000; +// } else if (duration.seconds > 0 && duration.nanos < 0) { +// duration.seconds -= 1; +// duration.nanos += 1000000000; +// } +// +// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. +// +// Timestamp start = ...; +// Duration duration = ...; +// Timestamp end = ...; +// +// end.seconds = start.seconds + duration.seconds; +// end.nanos = start.nanos + duration.nanos; +// +// if (end.nanos < 0) { +// end.seconds -= 1; +// end.nanos += 1000000000; +// } else if (end.nanos >= 1000000000) { +// end.seconds += 1; +// end.nanos -= 1000000000; +// } +// +// Example 3: Compute Duration from datetime.timedelta in Python. +// +// td = datetime.timedelta(days=3, minutes=10) +// duration = Duration() +// duration.FromTimedelta(td) +// +// # JSON Mapping +// +// In JSON format, the Duration type is encoded as a string rather than an +// object, where the string ends in the suffix "s" (indicating seconds) and +// is preceded by the number of seconds, with nanoseconds expressed as +// fractional seconds. For example, 3 seconds with 0 nanoseconds should be +// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should +// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 +// microsecond should be expressed in JSON format as "3.000001s". +// +// +message Duration { + // Signed seconds of the span of time. Must be from -315,576,000,000 + // to +315,576,000,000 inclusive. Note: these bounds are computed from: + // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + int64 seconds = 1; + + // Signed fractions of a second at nanosecond resolution of the span + // of time. Durations less than one second are represented with a 0 + // `seconds` field and a positive or negative `nanos` field. For durations + // of one second or more, a non-zero value for the `nanos` field must be + // of the same sign as the `seconds` field. Must be from -999,999,999 + // to +999,999,999 inclusive. + int32 nanos = 2; +} diff --git a/static/proto-includes/google/protobuf/empty.proto b/static/proto-includes/google/protobuf/empty.proto new file mode 100644 index 0000000..5f992de --- /dev/null +++ b/static/proto-includes/google/protobuf/empty.proto @@ -0,0 +1,52 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option go_package = "google.golang.org/protobuf/types/known/emptypb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "EmptyProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; + +// A generic empty message that you can re-use to avoid defining duplicated +// empty messages in your APIs. A typical example is to use it as the request +// or the response type of an API method. For instance: +// +// service Foo { +// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); +// } +// +// The JSON representation for `Empty` is empty JSON object `{}`. +message Empty {} diff --git a/static/proto-includes/google/protobuf/field_mask.proto b/static/proto-includes/google/protobuf/field_mask.proto new file mode 100644 index 0000000..6b5104f --- /dev/null +++ b/static/proto-includes/google/protobuf/field_mask.proto @@ -0,0 +1,245 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "FieldMaskProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option go_package = "google.golang.org/protobuf/types/known/fieldmaskpb"; +option cc_enable_arenas = true; + +// `FieldMask` represents a set of symbolic field paths, for example: +// +// paths: "f.a" +// paths: "f.b.d" +// +// Here `f` represents a field in some root message, `a` and `b` +// fields in the message found in `f`, and `d` a field found in the +// message in `f.b`. +// +// Field masks are used to specify a subset of fields that should be +// returned by a get operation or modified by an update operation. +// Field masks also have a custom JSON encoding (see below). +// +// # Field Masks in Projections +// +// When used in the context of a projection, a response message or +// sub-message is filtered by the API to only contain those fields as +// specified in the mask. For example, if the mask in the previous +// example is applied to a response message as follows: +// +// f { +// a : 22 +// b { +// d : 1 +// x : 2 +// } +// y : 13 +// } +// z: 8 +// +// The result will not contain specific values for fields x,y and z +// (their value will be set to the default, and omitted in proto text +// output): +// +// +// f { +// a : 22 +// b { +// d : 1 +// } +// } +// +// A repeated field is not allowed except at the last position of a +// paths string. +// +// If a FieldMask object is not present in a get operation, the +// operation applies to all fields (as if a FieldMask of all fields +// had been specified). +// +// Note that a field mask does not necessarily apply to the +// top-level response message. In case of a REST get operation, the +// field mask applies directly to the response, but in case of a REST +// list operation, the mask instead applies to each individual message +// in the returned resource list. In case of a REST custom method, +// other definitions may be used. Where the mask applies will be +// clearly documented together with its declaration in the API. In +// any case, the effect on the returned resource/resources is required +// behavior for APIs. +// +// # Field Masks in Update Operations +// +// A field mask in update operations specifies which fields of the +// targeted resource are going to be updated. The API is required +// to only change the values of the fields as specified in the mask +// and leave the others untouched. If a resource is passed in to +// describe the updated values, the API ignores the values of all +// fields not covered by the mask. +// +// If a repeated field is specified for an update operation, new values will +// be appended to the existing repeated field in the target resource. Note that +// a repeated field is only allowed in the last position of a `paths` string. +// +// If a sub-message is specified in the last position of the field mask for an +// update operation, then new value will be merged into the existing sub-message +// in the target resource. +// +// For example, given the target message: +// +// f { +// b { +// d: 1 +// x: 2 +// } +// c: [1] +// } +// +// And an update message: +// +// f { +// b { +// d: 10 +// } +// c: [2] +// } +// +// then if the field mask is: +// +// paths: ["f.b", "f.c"] +// +// then the result will be: +// +// f { +// b { +// d: 10 +// x: 2 +// } +// c: [1, 2] +// } +// +// An implementation may provide options to override this default behavior for +// repeated and message fields. +// +// In order to reset a field's value to the default, the field must +// be in the mask and set to the default value in the provided resource. +// Hence, in order to reset all fields of a resource, provide a default +// instance of the resource and set all fields in the mask, or do +// not provide a mask as described below. +// +// If a field mask is not present on update, the operation applies to +// all fields (as if a field mask of all fields has been specified). +// Note that in the presence of schema evolution, this may mean that +// fields the client does not know and has therefore not filled into +// the request will be reset to their default. If this is unwanted +// behavior, a specific service may require a client to always specify +// a field mask, producing an error if not. +// +// As with get operations, the location of the resource which +// describes the updated values in the request message depends on the +// operation kind. In any case, the effect of the field mask is +// required to be honored by the API. +// +// ## Considerations for HTTP REST +// +// The HTTP kind of an update operation which uses a field mask must +// be set to PATCH instead of PUT in order to satisfy HTTP semantics +// (PUT must only be used for full updates). +// +// # JSON Encoding of Field Masks +// +// In JSON, a field mask is encoded as a single string where paths are +// separated by a comma. Fields name in each path are converted +// to/from lower-camel naming conventions. +// +// As an example, consider the following message declarations: +// +// message Profile { +// User user = 1; +// Photo photo = 2; +// } +// message User { +// string display_name = 1; +// string address = 2; +// } +// +// In proto a field mask for `Profile` may look as such: +// +// mask { +// paths: "user.display_name" +// paths: "photo" +// } +// +// In JSON, the same mask is represented as below: +// +// { +// mask: "user.displayName,photo" +// } +// +// # Field Masks and Oneof Fields +// +// Field masks treat fields in oneofs just as regular fields. Consider the +// following message: +// +// message SampleMessage { +// oneof test_oneof { +// string name = 4; +// SubMessage sub_message = 9; +// } +// } +// +// The field mask can be: +// +// mask { +// paths: "name" +// } +// +// Or: +// +// mask { +// paths: "sub_message" +// } +// +// Note that oneof type names ("test_oneof" in this case) cannot be used in +// paths. +// +// ## Field Mask Verification +// +// The implementation of any API method which has a FieldMask type field in the +// request should verify the included field paths, and return an +// `INVALID_ARGUMENT` error if any path is unmappable. +message FieldMask { + // The set of field mask paths. + repeated string paths = 1; +} diff --git a/static/proto-includes/google/protobuf/source_context.proto b/static/proto-includes/google/protobuf/source_context.proto new file mode 100644 index 0000000..06bfc43 --- /dev/null +++ b/static/proto-includes/google/protobuf/source_context.proto @@ -0,0 +1,48 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "SourceContextProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option go_package = "google.golang.org/protobuf/types/known/sourcecontextpb"; + +// `SourceContext` represents information about the source of a +// protobuf element, like the file in which it is defined. +message SourceContext { + // The path-qualified name of the .proto file that contained the associated + // protobuf element. For example: `"google/protobuf/source_context.proto"`. + string file_name = 1; +} diff --git a/static/proto-includes/google/protobuf/struct.proto b/static/proto-includes/google/protobuf/struct.proto new file mode 100644 index 0000000..545215c --- /dev/null +++ b/static/proto-includes/google/protobuf/struct.proto @@ -0,0 +1,95 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/structpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "StructProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// `Struct` represents a structured data value, consisting of fields +// which map to dynamically typed values. In some languages, `Struct` +// might be supported by a native representation. For example, in +// scripting languages like JS a struct is represented as an +// object. The details of that representation are described together +// with the proto support for the language. +// +// The JSON representation for `Struct` is JSON object. +message Struct { + // Unordered map of dynamically typed values. + map fields = 1; +} + +// `Value` represents a dynamically typed value which can be either +// null, a number, a string, a boolean, a recursive struct value, or a +// list of values. A producer of value is expected to set one of that +// variants, absence of any variant indicates an error. +// +// The JSON representation for `Value` is JSON value. +message Value { + // The kind of value. + oneof kind { + // Represents a null value. + NullValue null_value = 1; + // Represents a double value. + double number_value = 2; + // Represents a string value. + string string_value = 3; + // Represents a boolean value. + bool bool_value = 4; + // Represents a structured value. + Struct struct_value = 5; + // Represents a repeated `Value`. + ListValue list_value = 6; + } +} + +// `NullValue` is a singleton enumeration to represent the null value for the +// `Value` type union. +// +// The JSON representation for `NullValue` is JSON `null`. +enum NullValue { + // Null value. + NULL_VALUE = 0; +} + +// `ListValue` is a wrapper around a repeated field of values. +// +// The JSON representation for `ListValue` is JSON array. +message ListValue { + // Repeated field of dynamically typed values. + repeated Value values = 1; +} diff --git a/static/proto-includes/google/protobuf/timestamp.proto b/static/proto-includes/google/protobuf/timestamp.proto new file mode 100644 index 0000000..3b2df6d --- /dev/null +++ b/static/proto-includes/google/protobuf/timestamp.proto @@ -0,0 +1,147 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/timestamppb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "TimestampProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// A Timestamp represents a point in time independent of any time zone or local +// calendar, encoded as a count of seconds and fractions of seconds at +// nanosecond resolution. The count is relative to an epoch at UTC midnight on +// January 1, 1970, in the proleptic Gregorian calendar which extends the +// Gregorian calendar backwards to year one. +// +// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap +// second table is needed for interpretation, using a [24-hour linear +// smear](https://developers.google.com/time/smear). +// +// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By +// restricting to that range, we ensure that we can convert to and from [RFC +// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. +// +// # Examples +// +// Example 1: Compute Timestamp from POSIX `time()`. +// +// Timestamp timestamp; +// timestamp.set_seconds(time(NULL)); +// timestamp.set_nanos(0); +// +// Example 2: Compute Timestamp from POSIX `gettimeofday()`. +// +// struct timeval tv; +// gettimeofday(&tv, NULL); +// +// Timestamp timestamp; +// timestamp.set_seconds(tv.tv_sec); +// timestamp.set_nanos(tv.tv_usec * 1000); +// +// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. +// +// FILETIME ft; +// GetSystemTimeAsFileTime(&ft); +// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; +// +// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z +// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. +// Timestamp timestamp; +// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); +// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); +// +// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. +// +// long millis = System.currentTimeMillis(); +// +// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) +// .setNanos((int) ((millis % 1000) * 1000000)).build(); +// +// +// Example 5: Compute Timestamp from Java `Instant.now()`. +// +// Instant now = Instant.now(); +// +// Timestamp timestamp = +// Timestamp.newBuilder().setSeconds(now.getEpochSecond()) +// .setNanos(now.getNano()).build(); +// +// +// Example 6: Compute Timestamp from current time in Python. +// +// timestamp = Timestamp() +// timestamp.GetCurrentTime() +// +// # JSON Mapping +// +// In JSON format, the Timestamp type is encoded as a string in the +// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the +// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" +// where {year} is always expressed using four digits while {month}, {day}, +// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional +// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), +// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone +// is required. A proto3 JSON serializer should always use UTC (as indicated by +// "Z") when printing the Timestamp type and a proto3 JSON parser should be +// able to accept both UTC and other timezones (as indicated by an offset). +// +// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past +// 01:30 UTC on January 15, 2017. +// +// In JavaScript, one can convert a Date object to this format using the +// standard +// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) +// method. In Python, a standard `datetime.datetime` object can be converted +// to this format using +// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with +// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use +// the Joda Time's [`ISODateTimeFormat.dateTime()`]( +// http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D +// ) to obtain a formatter capable of generating timestamps in this format. +// +// +message Timestamp { + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + int64 seconds = 1; + + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + int32 nanos = 2; +} diff --git a/static/proto-includes/google/protobuf/type.proto b/static/proto-includes/google/protobuf/type.proto new file mode 100644 index 0000000..d3f6a68 --- /dev/null +++ b/static/proto-includes/google/protobuf/type.proto @@ -0,0 +1,187 @@ +// 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. + +syntax = "proto3"; + +package google.protobuf; + +import "google/protobuf/any.proto"; +import "google/protobuf/source_context.proto"; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option java_package = "com.google.protobuf"; +option java_outer_classname = "TypeProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option go_package = "google.golang.org/protobuf/types/known/typepb"; + +// A protocol buffer message type. +message Type { + // The fully qualified message name. + string name = 1; + // The list of fields. + repeated Field fields = 2; + // The list of types appearing in `oneof` definitions in this type. + repeated string oneofs = 3; + // The protocol buffer options. + repeated Option options = 4; + // The source context. + SourceContext source_context = 5; + // The source syntax. + Syntax syntax = 6; +} + +// A single field of a message type. +message Field { + // Basic field types. + enum Kind { + // Field type unknown. + TYPE_UNKNOWN = 0; + // Field type double. + TYPE_DOUBLE = 1; + // Field type float. + TYPE_FLOAT = 2; + // Field type int64. + TYPE_INT64 = 3; + // Field type uint64. + TYPE_UINT64 = 4; + // Field type int32. + TYPE_INT32 = 5; + // Field type fixed64. + TYPE_FIXED64 = 6; + // Field type fixed32. + TYPE_FIXED32 = 7; + // Field type bool. + TYPE_BOOL = 8; + // Field type string. + TYPE_STRING = 9; + // Field type group. Proto2 syntax only, and deprecated. + TYPE_GROUP = 10; + // Field type message. + TYPE_MESSAGE = 11; + // Field type bytes. + TYPE_BYTES = 12; + // Field type uint32. + TYPE_UINT32 = 13; + // Field type enum. + TYPE_ENUM = 14; + // Field type sfixed32. + TYPE_SFIXED32 = 15; + // Field type sfixed64. + TYPE_SFIXED64 = 16; + // Field type sint32. + TYPE_SINT32 = 17; + // Field type sint64. + TYPE_SINT64 = 18; + } + + // Whether a field is optional, required, or repeated. + enum Cardinality { + // For fields with unknown cardinality. + CARDINALITY_UNKNOWN = 0; + // For optional fields. + CARDINALITY_OPTIONAL = 1; + // For required fields. Proto2 syntax only. + CARDINALITY_REQUIRED = 2; + // For repeated fields. + CARDINALITY_REPEATED = 3; + } + + // The field type. + Kind kind = 1; + // The field cardinality. + Cardinality cardinality = 2; + // The field number. + int32 number = 3; + // The field name. + string name = 4; + // The field type URL, without the scheme, for message or enumeration + // types. Example: `"type.googleapis.com/google.protobuf.Timestamp"`. + string type_url = 6; + // The index of the field type in `Type.oneofs`, for message or enumeration + // types. The first type has index 1; zero means the type is not in the list. + int32 oneof_index = 7; + // Whether to use alternative packed wire representation. + bool packed = 8; + // The protocol buffer options. + repeated Option options = 9; + // The field JSON name. + string json_name = 10; + // The string value of the default value of this field. Proto2 syntax only. + string default_value = 11; +} + +// Enum type definition. +message Enum { + // Enum type name. + string name = 1; + // Enum value definitions. + repeated EnumValue enumvalue = 2; + // Protocol buffer options. + repeated Option options = 3; + // The source context. + SourceContext source_context = 4; + // The source syntax. + Syntax syntax = 5; +} + +// Enum value definition. +message EnumValue { + // Enum value name. + string name = 1; + // Enum value number. + int32 number = 2; + // Protocol buffer options. + repeated Option options = 3; +} + +// A protocol buffer option, which can be attached to a message, field, +// enumeration, etc. +message Option { + // The option's name. For protobuf built-in options (options defined in + // descriptor.proto), this is the short name. For example, `"map_entry"`. + // For custom options, it should be the fully-qualified name. For example, + // `"google.api.http"`. + string name = 1; + // The option's value packed in an Any message. If the value is a primitive, + // the corresponding wrapper type defined in google/protobuf/wrappers.proto + // should be used. If the value is an enum, it should be stored as an int32 + // value using the google.protobuf.Int32Value type. + Any value = 2; +} + +// The syntax in which a protocol buffer element is defined. +enum Syntax { + // Syntax `proto2`. + SYNTAX_PROTO2 = 0; + // Syntax `proto3`. + SYNTAX_PROTO3 = 1; +} diff --git a/static/proto-includes/google/protobuf/wrappers.proto b/static/proto-includes/google/protobuf/wrappers.proto new file mode 100644 index 0000000..d49dd53 --- /dev/null +++ b/static/proto-includes/google/protobuf/wrappers.proto @@ -0,0 +1,123 @@ +// 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. + +// Wrappers for primitive (non-message) types. These types are useful +// for embedding primitives in the `google.protobuf.Any` type and for places +// where we need to distinguish between the absence of a primitive +// typed field and its default value. +// +// These wrappers have no meaningful use within repeated fields as they lack +// the ability to detect presence on individual elements. +// These wrappers have no meaningful use within a map or a oneof since +// individual entries of a map or fields of a oneof can already detect presence. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/wrapperspb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "WrappersProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +// Wrapper message for `double`. +// +// The JSON representation for `DoubleValue` is JSON number. +message DoubleValue { + // The double value. + double value = 1; +} + +// Wrapper message for `float`. +// +// The JSON representation for `FloatValue` is JSON number. +message FloatValue { + // The float value. + float value = 1; +} + +// Wrapper message for `int64`. +// +// The JSON representation for `Int64Value` is JSON string. +message Int64Value { + // The int64 value. + int64 value = 1; +} + +// Wrapper message for `uint64`. +// +// The JSON representation for `UInt64Value` is JSON string. +message UInt64Value { + // The uint64 value. + uint64 value = 1; +} + +// Wrapper message for `int32`. +// +// The JSON representation for `Int32Value` is JSON number. +message Int32Value { + // The int32 value. + int32 value = 1; +} + +// Wrapper message for `uint32`. +// +// The JSON representation for `UInt32Value` is JSON number. +message UInt32Value { + // The uint32 value. + uint32 value = 1; +} + +// Wrapper message for `bool`. +// +// The JSON representation for `BoolValue` is JSON `true` and `false`. +message BoolValue { + // The bool value. + bool value = 1; +} + +// Wrapper message for `string`. +// +// The JSON representation for `StringValue` is JSON string. +message StringValue { + // The string value. + string value = 1; +} + +// Wrapper message for `bytes`. +// +// The JSON representation for `BytesValue` is JSON string. +message BytesValue { + // The bytes value. + bytes value = 1; +} diff --git a/static/proxy-nginx/files/nginx.conf.tpl b/static/proxy-nginx/files/nginx.conf.tpl new file mode 100644 index 0000000..bb5c294 --- /dev/null +++ b/static/proxy-nginx/files/nginx.conf.tpl @@ -0,0 +1,18 @@ +server { + server_name {{ .Domain }} {{ .Domain }}.*; + + listen 80; + listen [::]:80; + listen 443 ssl; + + ssl_certificate /etc/ssl/mock/mock.crt; + ssl_certificate_key /etc/ssl/mock/mock.key; + + access_log /var/log/nginx/{{ .Domain }}.access.log main; + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://localhost:{{ .Port }}; + } +} diff --git a/static/proxy/files/main.go.tpl b/static/proxy/files/main.go.tpl new file mode 100644 index 0000000..0a898bc --- /dev/null +++ b/static/proxy/files/main.go.tpl @@ -0,0 +1,94 @@ +package main + +/* + Code generated by + grpc-wiremock grpc to http proxy generator. +*/ + +import ( + "context" + "fmt" + "log" + "net" + "os" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "grpc-proxy/internal/health" + "grpc-proxy/pkg/getenv" + pb_health "grpc-proxy/pkg/gitlab.sbmt.io/paas/health" + + {{ range .OriginalGoPackagesWithService -}} + "grpc-proxy/internal/{{ . }}" + {{ end }} + {{ range .GoPackages }} + pb_{{ . | ToPackageName }} "{{ . }}" + {{- end }} +) + +func main() { + ctx := context.Background() + address := fmt.Sprintf(":%s", getenv.GetPort()) + + if err := Run(ctx, address); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} + +func Run(ctx context.Context, address string) error { + logger := getLogger() + logger.Info(fmt.Sprintf("Starting listening on %s", address)) + + listener, err := net.Listen("tcp", address) + if err != nil { + return err + } + defer func() { + if err = listener.Close(); err != nil { + logger.Info(fmt.Sprintf("Failed to close server: %s, %s", address, err.Error())) + } + }() + + logger.Info(fmt.Sprintf("Listening on %s", address)) + + logUnaryInterceptor := grpc.UnaryInterceptor( + grpc_middleware.ChainUnaryServer( + grpc_zap.UnaryServerInterceptor(logger), + ), + ) + + server := grpc.NewServer(logUnaryInterceptor) + {{ range .PackageToServices }} + pb_{{ .ProtoPackage }}.Register{{ .Service }}Server(server, {{ .ServicePackage }}.NewService()) + {{- end }} + + reflection.Register(server) + pb_health.RegisterHealthServer(server, health.NewService()) + + go func() { + defer server.GracefulStop() + <-ctx.Done() + }() + + return server.Serve(listener) +} + +func getLogger() *zap.Logger { + encoderCfg := zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + EncodeLevel: zapcore.CapitalLevelEncoder, + } + + core := zapcore.NewCore( + zapcore.NewConsoleEncoder(encoderCfg), + os.Stdout, + zap.DebugLevel, + ) + + return zap.New(core) +} diff --git a/static/proxy/files/method-bidirectional-streaming.go.tpl b/static/proxy/files/method-bidirectional-streaming.go.tpl new file mode 100644 index 0000000..0f4a181 --- /dev/null +++ b/static/proxy/files/method-bidirectional-streaming.go.tpl @@ -0,0 +1,78 @@ +package {{ .PackageHeader }} + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + + "grpc-proxy/pkg/wiremock" + + {{ range .GoPackages }} + "{{ . }}" + {{- end }} +) + +func (p *Service) {{ .Method }}(stream {{ .MethodPackage }}.{{ .Service }}_{{ .Method }}Server) error { + const url = "{{ .URL }}" + + ctx := stream.Context() + + unmarshalAndSend := func(responseBody []byte) error { + var protoResponse {{ .MethodOutPackage }}.{{ .MethodOutName }} + if processErr := protojson.Unmarshal(responseBody, &protoResponse); processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("marshal json object to proto: %v", processErr)) + } + if processErr := stream.Send(&protoResponse); processErr != nil { + return processErr + } + return nil + } + + processStream := func(cursor int) error { + httpRequest, processErr := wiremock.RequestWithCursor(ctx, url, cursor, bytes.NewReader([]byte{})) + if processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", processErr)) + } + httpResponseBody, processErr := wiremock.DoRequestDefault(httpRequest) + if processErr != nil { + return processErr + } + return unmarshalAndSend(httpResponseBody) + } + + streamCursor := 1 + + request, err := wiremock.RequestWithCursor(ctx, url, streamCursor, bytes.NewReader([]byte{})) + if err != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", err)) + } + + _, streamSize, err := wiremock.DoRequestWithStreamSize(request) + if err != nil { + return err + } + + for { + req, errReceive := stream.Recv() + if errReceive != nil && errReceive == io.EOF { + return nil + } + if errReceive != nil { + return errReceive + } + if req == nil { + continue + } + if err = processStream(streamCursor); err != nil { + return err + } + if streamCursor >= streamSize { + return nil + } + streamCursor++ + } +} diff --git a/static/proxy/files/method-client-streaming.go.tpl b/static/proxy/files/method-client-streaming.go.tpl new file mode 100644 index 0000000..2954d4c --- /dev/null +++ b/static/proxy/files/method-client-streaming.go.tpl @@ -0,0 +1,61 @@ +package {{ .PackageHeader }} + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + + "grpc-proxy/pkg/wiremock" + + {{ range .GoPackages }} + "{{ . }}" + {{- end }} +) + +func (p *Service) {{ .Method }}(stream {{ .MethodPackage }}.{{ .Service }}_{{ .Method }}Server) error { + const url = "{{ .URL }}" + + unmarshalAndSend := func(responseBody []byte) error { + var protoResponse {{ .MethodOutPackage }}.{{ .MethodOutName }} + if processErr := protojson.Unmarshal(responseBody, &protoResponse); processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("marshal json object to proto: %v", processErr)) + } + if processErr := stream.SendAndClose(&protoResponse); processErr != nil { + return processErr + } + return nil + } + + defaultRequest, err := wiremock.DefaultRequest(stream.Context(), url, bytes.NewReader([]byte{})) + if err != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", err)) + } + + httpResponseBody, streamSize, err := wiremock.DoRequestWithStreamSize(defaultRequest) + if err != nil { + return err + } + + streamCursor := 1 + + for { + req, errReceive := stream.Recv() + if errReceive != nil && errReceive == io.EOF { + return unmarshalAndSend(httpResponseBody) + } + if errReceive != nil { + return errReceive + } + if streamCursor >= streamSize { + return unmarshalAndSend(httpResponseBody) + } + if req == nil { + continue + } + streamCursor++ + } +} diff --git a/static/proxy/files/method-server-streaming.go.tpl b/static/proxy/files/method-server-streaming.go.tpl new file mode 100644 index 0000000..cf1dba3 --- /dev/null +++ b/static/proxy/files/method-server-streaming.go.tpl @@ -0,0 +1,68 @@ +package {{ .PackageHeader }} + +import ( + "bytes" + "fmt" + "net/http" + + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + + "grpc-proxy/pkg/wiremock" + + {{ range .GoPackages }} + "{{ . }}" + {{- end }} +) + +func (p *Service) {{ .Method }}(in *{{ .MethodInPackage }}.{{ .MethodInName }}, stream {{ .MethodPackage }}.{{ .Service }}_{{ .Method }}Server) error { + const url = "{{ .URL }}" + const streamCursor = 1 + + ctx := stream.Context() + + unmarshalAndSend := func(responseBody []byte) error { + var protoResponse {{ .MethodOutPackage }}.{{ .MethodOutName }} + if processErr := protojson.Unmarshal(responseBody, &protoResponse); processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("marshal json object to proto: %v", processErr)) + } + if processErr := stream.Send(&protoResponse); processErr != nil { + return processErr + } + return nil + } + + processStream := func(cursor int) error { + httpRequest, processErr := wiremock.RequestWithCursor(ctx, url, cursor, bytes.NewReader([]byte{})) + if processErr != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", processErr)) + } + httpResponseBody, processErr := wiremock.DoRequestDefault(httpRequest) + if processErr != nil { + return processErr + } + return unmarshalAndSend(httpResponseBody) + } + + defaultRequest, err := wiremock.RequestWithCursor(ctx, url, streamCursor, bytes.NewReader([]byte{})) + if err != nil { + return status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", err)) + } + + httpResponseBody, streamSize, err := wiremock.DoRequestWithStreamSize(defaultRequest) + if err != nil { + return err + } + + if err = unmarshalAndSend(httpResponseBody); err != nil { + return err + } + + for cursor := streamCursor + 1; cursor <= streamSize; cursor++ { + if err = processStream(cursor); err != nil { + return err + } + } + + return nil +} diff --git a/static/proxy/files/method-unary.go.tpl b/static/proxy/files/method-unary.go.tpl new file mode 100644 index 0000000..eafba8e --- /dev/null +++ b/static/proxy/files/method-unary.go.tpl @@ -0,0 +1,43 @@ +package {{ .PackageHeader }} + +import ( + "bytes" + "context" + "fmt" + "net/http" + + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + + "grpc-proxy/pkg/wiremock" + + {{ range .GoPackages }} + "{{ . }}" + {{- end }} +) + +func (p *Service) {{ .Method }}(ctx context.Context, in *{{ .MethodInPackage }}.{{ .MethodInName }}) (*{{ .MethodOutPackage }}.{{ .MethodOutName }}, error) { + const url = "{{ .URL }}" + + requestBody, err := protojson.Marshal(in) + if err != nil { + return nil, status.Error(http.StatusBadGateway, fmt.Sprintf("create http request body: %v", err)) + } + + request, err := wiremock.DefaultRequest(ctx, url, bytes.NewReader(requestBody)) + if err != nil { + return nil, status.Error(http.StatusBadGateway, fmt.Sprintf("create http request: %v", err)) + } + + httpResponseBody, err := wiremock.DoRequestDefault(request) + if err != nil { + return nil, err + } + + var protoResponse {{ .MethodOutPackage }}.{{ .MethodOutName }} + if err = protojson.Unmarshal(httpResponseBody, &protoResponse); err != nil { + return nil, status.Error(http.StatusBadGateway, fmt.Sprintf("marshal json object to proto: %v", err)) + } + + return &protoResponse, nil +} diff --git a/static/proxy/files/service.go.tpl b/static/proxy/files/service.go.tpl new file mode 100644 index 0000000..43fa724 --- /dev/null +++ b/static/proxy/files/service.go.tpl @@ -0,0 +1,19 @@ +package {{ .PackageHeader }} + +import ( + "google.golang.org/grpc" + + "{{ .GoPackage }}" +) + +type Service struct { + {{ .Package }}.Unimplemented{{ .Service }}Server +} + +func NewService() *Service { + return &Service{} +} + +func (p *Service) RegisterGRPC(server *grpc.Server) { + {{ .Package }}.Register{{ .Service }}Server(server, p) +} diff --git a/static/proxy/template/layout/Makefile b/static/proxy/template/layout/Makefile new file mode 100644 index 0000000..8b30614 --- /dev/null +++ b/static/proxy/template/layout/Makefile @@ -0,0 +1,10 @@ +PROXY_BINARY_NAME=grpc-to-http-proxy + +run: + @go mod tidy + @go run cmd/*.go + +install: + @go mod tidy + @go build -o ${PROXY_BINARY_NAME} cmd/*.go + @mv ${PROXY_BINARY_NAME} ${GOBIN}/${PROXY_BINARY_NAME} diff --git a/static/proxy/template/layout/cmd/.keep b/static/proxy/template/layout/cmd/.keep new file mode 100644 index 0000000..e69de29 diff --git a/static/proxy/template/layout/go.mod.rename.me b/static/proxy/template/layout/go.mod.rename.me new file mode 100644 index 0000000..cdf970b --- /dev/null +++ b/static/proxy/template/layout/go.mod.rename.me @@ -0,0 +1,22 @@ +module grpc-proxy + +go 1.19 + +require ( + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 + go.uber.org/zap v1.23.0 + google.golang.org/grpc v1.49.0 + google.golang.org/protobuf v1.28.1 +) + +require ( + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.7 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect + golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de // indirect +) diff --git a/static/proxy/template/layout/go.sum b/static/proxy/template/layout/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/static/proxy/template/layout/internal/health/health.go b/static/proxy/template/layout/internal/health/health.go new file mode 100644 index 0000000..d58d813 --- /dev/null +++ b/static/proxy/template/layout/internal/health/health.go @@ -0,0 +1,33 @@ +package health + +import ( + "context" + + "google.golang.org/grpc" + + "grpc-proxy/pkg/gitlab.sbmt.io/paas/health" +) + +type Service struct { + health.UnimplementedHealthServer +} + +func NewService() *Service { + return &Service{} +} + +func (s *Service) RegisterGRPC(server *grpc.Server) { + health.RegisterHealthServer(server, s) +} + +func (s *Service) Check(_ context.Context, req *health.HealthCheckRequest) (*health.HealthCheckResponse, error) { + return &health.HealthCheckResponse{ + Status: health.HealthCheckResponse_SERVING, + }, nil +} + +func (s *Service) Watch(_ *health.HealthCheckRequest, server health.Health_WatchServer) error { + return server.Send(&health.HealthCheckResponse{ + Status: health.HealthCheckResponse_SERVING, + }) +} diff --git a/static/proxy/template/layout/pkg/getenv/const.go b/static/proxy/template/layout/pkg/getenv/const.go new file mode 100644 index 0000000..0d5578c --- /dev/null +++ b/static/proxy/template/layout/pkg/getenv/const.go @@ -0,0 +1,5 @@ +package getenv + +const ( + defaultPort = "3010" +) diff --git a/static/proxy/template/layout/pkg/getenv/getenv.go b/static/proxy/template/layout/pkg/getenv/getenv.go new file mode 100644 index 0000000..a8afc36 --- /dev/null +++ b/static/proxy/template/layout/pkg/getenv/getenv.go @@ -0,0 +1,17 @@ +package getenv + +import ( + "os" + "strconv" +) + +func GetPort() string { + rawPort := os.Getenv("GRPC_TO_HTTP_PROXY_PORT") + + _, err := strconv.ParseInt(rawPort, 10, 0) + if err != nil { + return defaultPort + } + + return rawPort +} diff --git a/static/proxy/template/layout/pkg/gitlab.sbmt.io/paas/health/health.pb.go b/static/proxy/template/layout/pkg/gitlab.sbmt.io/paas/health/health.pb.go new file mode 100644 index 0000000..7d1fde5 --- /dev/null +++ b/static/proxy/template/layout/pkg/gitlab.sbmt.io/paas/health/health.pb.go @@ -0,0 +1,284 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.18.1 +// source: health/health.proto + +package health + +import ( + 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) +) + +type HealthCheckResponse_ServingStatus int32 + +const ( + HealthCheckResponse_UNKNOWN HealthCheckResponse_ServingStatus = 0 + HealthCheckResponse_SERVING HealthCheckResponse_ServingStatus = 1 + HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2 +) + +// Enum value maps for HealthCheckResponse_ServingStatus. +var ( + HealthCheckResponse_ServingStatus_name = map[int32]string{ + 0: "UNKNOWN", + 1: "SERVING", + 2: "NOT_SERVING", + } + HealthCheckResponse_ServingStatus_value = map[string]int32{ + "UNKNOWN": 0, + "SERVING": 1, + "NOT_SERVING": 2, + } +) + +func (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse_ServingStatus { + p := new(HealthCheckResponse_ServingStatus) + *p = x + return p +} + +func (x HealthCheckResponse_ServingStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor { + return file_health_checks_health_proto_enumTypes[0].Descriptor() +} + +func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType { + return &file_health_checks_health_proto_enumTypes[0] +} + +func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. +func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { + return file_health_checks_health_proto_rawDescGZIP(), []int{1, 0} +} + +type HealthCheckRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` +} + +func (x *HealthCheckRequest) Reset() { + *x = HealthCheckRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_health_checks_health_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckRequest) ProtoMessage() {} + +func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_health_checks_health_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 HealthCheckRequest.ProtoReflect.Descriptor instead. +func (*HealthCheckRequest) Descriptor() ([]byte, []int) { + return file_health_checks_health_proto_rawDescGZIP(), []int{0} +} + +func (x *HealthCheckRequest) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +type HealthCheckResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status HealthCheckResponse_ServingStatus `protobuf:"varint,1,opt,name=status,proto3,enum=grpc.health.v1.HealthCheckResponse_ServingStatus" json:"status,omitempty"` +} + +func (x *HealthCheckResponse) Reset() { + *x = HealthCheckResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_health_checks_health_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckResponse) ProtoMessage() {} + +func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_health_checks_health_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 HealthCheckResponse.ProtoReflect.Descriptor instead. +func (*HealthCheckResponse) Descriptor() ([]byte, []int) { + return file_health_checks_health_proto_rawDescGZIP(), []int{1} +} + +func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { + if x != nil { + return x.Status + } + return HealthCheckResponse_UNKNOWN +} + +var File_health_checks_health_proto protoreflect.FileDescriptor + +var file_health_checks_health_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x2f, + 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x22, 0x2e, 0x0a, 0x12, + 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x9c, 0x01, 0x0a, + 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x31, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x3a, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, + 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, + 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x32, 0xae, 0x01, 0x0a, 0x06, + 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x50, 0x0a, 0x05, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, + 0x22, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, 0x76, 0x31, + 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, + 0x68, 0x12, 0x22, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x2e, + 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x68, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x2b, 0x5a, 0x29, + 0x67, 0x72, 0x70, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, + 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x73, 0x62, 0x6d, 0x74, 0x2e, 0x69, 0x6f, 0x2f, 0x70, 0x61, + 0x61, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_health_checks_health_proto_rawDescOnce sync.Once + file_health_checks_health_proto_rawDescData = file_health_checks_health_proto_rawDesc +) + +func file_health_checks_health_proto_rawDescGZIP() []byte { + file_health_checks_health_proto_rawDescOnce.Do(func() { + file_health_checks_health_proto_rawDescData = protoimpl.X.CompressGZIP(file_health_checks_health_proto_rawDescData) + }) + return file_health_checks_health_proto_rawDescData +} + +var file_health_checks_health_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_health_checks_health_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_health_checks_health_proto_goTypes = []interface{}{ + (HealthCheckResponse_ServingStatus)(0), // 0: grpc.health.v1.HealthCheckResponse.ServingStatus + (*HealthCheckRequest)(nil), // 1: grpc.health.v1.HealthCheckRequest + (*HealthCheckResponse)(nil), // 2: grpc.health.v1.HealthCheckResponse +} +var file_health_checks_health_proto_depIdxs = []int32{ + 0, // 0: grpc.health.v1.HealthCheckResponse.status:type_name -> grpc.health.v1.HealthCheckResponse.ServingStatus + 1, // 1: grpc.health.v1.Health.Check:input_type -> grpc.health.v1.HealthCheckRequest + 1, // 2: grpc.health.v1.Health.Watch:input_type -> grpc.health.v1.HealthCheckRequest + 2, // 3: grpc.health.v1.Health.Check:output_type -> grpc.health.v1.HealthCheckResponse + 2, // 4: grpc.health.v1.Health.Watch:output_type -> grpc.health.v1.HealthCheckResponse + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_health_checks_health_proto_init() } +func file_health_checks_health_proto_init() { + if File_health_checks_health_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_health_checks_health_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HealthCheckRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_health_checks_health_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HealthCheckResponse); 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_health_checks_health_proto_rawDesc, + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_health_checks_health_proto_goTypes, + DependencyIndexes: file_health_checks_health_proto_depIdxs, + EnumInfos: file_health_checks_health_proto_enumTypes, + MessageInfos: file_health_checks_health_proto_msgTypes, + }.Build() + File_health_checks_health_proto = out.File + file_health_checks_health_proto_rawDesc = nil + file_health_checks_health_proto_goTypes = nil + file_health_checks_health_proto_depIdxs = nil +} diff --git a/static/proxy/template/layout/pkg/gitlab.sbmt.io/paas/health/health_grpc.pb.go b/static/proxy/template/layout/pkg/gitlab.sbmt.io/paas/health/health_grpc.pb.go new file mode 100644 index 0000000..951840a --- /dev/null +++ b/static/proxy/template/layout/pkg/gitlab.sbmt.io/paas/health/health_grpc.pb.go @@ -0,0 +1,169 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.18.1 +// source: health/health.proto + +package health + +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 + +// HealthClient is the client API for Health 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 HealthClient interface { + Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) + Watch(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (Health_WatchClient, error) +} + +type healthClient struct { + cc grpc.ClientConnInterface +} + +func NewHealthClient(cc grpc.ClientConnInterface) HealthClient { + return &healthClient{cc} +} + +func (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { + out := new(HealthCheckResponse) + err := c.cc.Invoke(ctx, "/grpc.health.v1.Health/Check", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *healthClient) Watch(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (Health_WatchClient, error) { + stream, err := c.cc.NewStream(ctx, &Health_ServiceDesc.Streams[0], "/grpc.health.v1.Health/Watch", opts...) + if err != nil { + return nil, err + } + x := &healthWatchClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Health_WatchClient interface { + Recv() (*HealthCheckResponse, error) + grpc.ClientStream +} + +type healthWatchClient struct { + grpc.ClientStream +} + +func (x *healthWatchClient) Recv() (*HealthCheckResponse, error) { + m := new(HealthCheckResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// HealthServer is the server API for Health service. +// All implementations must embed UnimplementedHealthServer +// for forward compatibility +type HealthServer interface { + Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) + Watch(*HealthCheckRequest, Health_WatchServer) error + mustEmbedUnimplementedHealthServer() +} + +// UnimplementedHealthServer must be embedded to have forward compatible implementations. +type UnimplementedHealthServer struct { +} + +func (UnimplementedHealthServer) Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Check not implemented") +} +func (UnimplementedHealthServer) Watch(*HealthCheckRequest, Health_WatchServer) error { + return status.Errorf(codes.Unimplemented, "method Watch not implemented") +} +func (UnimplementedHealthServer) mustEmbedUnimplementedHealthServer() {} + +// UnsafeHealthServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HealthServer will +// result in compilation errors. +type UnsafeHealthServer interface { + mustEmbedUnimplementedHealthServer() +} + +func RegisterHealthServer(s grpc.ServiceRegistrar, srv HealthServer) { + s.RegisterService(&Health_ServiceDesc, srv) +} + +func _Health_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HealthServer).Check(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/grpc.health.v1.Health/Check", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HealthServer).Check(ctx, req.(*HealthCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Health_Watch_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(HealthCheckRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(HealthServer).Watch(m, &healthWatchServer{stream}) +} + +type Health_WatchServer interface { + Send(*HealthCheckResponse) error + grpc.ServerStream +} + +type healthWatchServer struct { + grpc.ServerStream +} + +func (x *healthWatchServer) Send(m *HealthCheckResponse) error { + return x.ServerStream.SendMsg(m) +} + +// Health_ServiceDesc is the grpc.ServiceDesc for Health service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Health_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "grpc.health.v1.Health", + HandlerType: (*HealthServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Check", + Handler: _Health_Check_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Watch", + Handler: _Health_Watch_Handler, + ServerStreams: true, + }, + }, + Metadata: "health/health.proto", +} diff --git a/static/proxy/template/layout/pkg/status/status.go b/static/proxy/template/layout/pkg/status/status.go new file mode 100644 index 0000000..365dbfb --- /dev/null +++ b/static/proxy/template/layout/pkg/status/status.go @@ -0,0 +1,49 @@ +package statustocode + +import ( + "net/http" + + "google.golang.org/grpc/codes" +) + +func GetCodeFromResponse(response *http.Response) codes.Code { + if response == nil { + return codes.Unknown + } + return codeFromHTTPStatus(response.StatusCode) +} + +func GetStatusFromResponse(response *http.Response) int { + if response == nil { + return http.StatusNotFound + } + return response.StatusCode +} + +func codeFromHTTPStatus(code int) codes.Code { + switch code { + 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.AlreadyExists + 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 http.StatusOK: + return codes.OK + } + return codes.Unknown +} diff --git a/static/proxy/template/layout/pkg/wiremock/client.go b/static/proxy/template/layout/pkg/wiremock/client.go new file mode 100644 index 0000000..7f54bd5 --- /dev/null +++ b/static/proxy/template/layout/pkg/wiremock/client.go @@ -0,0 +1,102 @@ +package wiremock + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + statustocode "grpc-proxy/pkg/status" +) + +var client = http.Client{Timeout: time.Second * 3} + +const streamCursor = "streamCursor" + +func DefaultRequest(ctx context.Context, url string, body io.Reader) (*http.Request, error) { + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + if err != nil { + return nil, err + } + return httpRequest, nil +} + +func RequestWithCursor(ctx context.Context, url string, cursor int, body io.Reader) (*http.Request, error) { + httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + if err != nil { + return nil, err + } + httpRequest.Header.Add(streamCursor, strconv.Itoa(cursor)) + return httpRequest, nil +} + +func DoRequestDefault(request *http.Request) ([]byte, error) { + response, err := doRequest(request) + if err != nil { + return nil, err + } + + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, status.Error(http.StatusBadGateway, fmt.Sprintf("read http response: %v", err)) + } + + return body, nil +} + +func DoRequestWithStreamSize(request *http.Request) ([]byte, int, error) { + response, err := doRequest(request) + if err != nil { + return nil, 0, err + } + + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, 0, status.Error(http.StatusBadGateway, fmt.Sprintf("read http response: %v", err)) + } + + streamSizeRaw := response.Header.Get("streamSize") + if len(streamSizeRaw) == 0 { + return nil, 0, status.Error(http.StatusBadGateway, fmt.Sprintf("read http response, stream size: %v", err)) + } + + streamSize, err := strconv.Atoi(streamSizeRaw) + if err != nil { + return nil, 0, fmt.Errorf("convert streamSize header to int: %w", err) + } + + return body, streamSize, nil +} + +func doRequest(request *http.Request) (*http.Response, error) { + if md, ok := metadata.FromIncomingContext(request.Context()); ok { + if authority := md.Get(":authority"); len(authority) > 0 { + request.Host = authority[0] + } + } + + httpResponse, err := client.Do(request) + if err != nil { + code := statustocode.GetCodeFromResponse(httpResponse) + return nil, status.Error(code, fmt.Sprintf("do http response: %v", err)) + } + + if httpStatus := statustocode.GetStatusFromResponse(httpResponse); httpStatus >= http.StatusBadRequest { + code := statustocode.GetCodeFromResponse(httpResponse) + message := fmt.Sprintf("wiremock bad status: %d", httpStatus) + if body, err := io.ReadAll(httpResponse.Body); err == nil { + message += "\n" + string(body) + } + return nil, status.Error(code, message) + } + + return httpResponse, nil +} diff --git a/static/static.go b/static/static.go new file mode 100644 index 0000000..e79d06d --- /dev/null +++ b/static/static.go @@ -0,0 +1,14 @@ +package static + +import ( + "embed" + + "github.com/spf13/afero" +) + +//go:embed * +var static embed.FS + +func FromEmbed() afero.Fs { + return afero.FromIOFS{FS: static} +} diff --git a/static/supervisord/default-config-path/mocks/keep b/static/supervisord/default-config-path/mocks/keep new file mode 100644 index 0000000..e69de29 diff --git a/static/supervisord/default-config-path/supervisord.conf b/static/supervisord/default-config-path/supervisord.conf new file mode 100644 index 0000000..8c8036f --- /dev/null +++ b/static/supervisord/default-config-path/supervisord.conf @@ -0,0 +1,16 @@ +[include] +files = /etc/supervisord/mocks/*.conf + +[inet_http_server] +port=127.0.0.1:9000 +;username=test1 +;password=thepassword + +[supervisorctl] +serverurl=http://127.0.0.1:9000 + +[program:watcher_domains] +command = watcher --domains=/home/mock +autorestart = true +redirect_stderr = true +stdout_logfile = watcher_domains.log, /dev/stdout diff --git a/static/supervisord/files/supervisord.conf.tpl b/static/supervisord/files/supervisord.conf.tpl new file mode 100644 index 0000000..f4eab5c --- /dev/null +++ b/static/supervisord/files/supervisord.conf.tpl @@ -0,0 +1,6 @@ +[program:mock-{{ .Domain }}-{{ .Port }}] +environment = ROOT="{{ .Root }}",PORT="{{ .Port }}",NAME="{{ .Domain }}" +autorestart = true +redirect_stderr = true +stdout_logfile = /var/log/supervisord/mock-{{ .Domain }}.log +command = java -cp "/var/wiremock/lib/*:/var/wiremock/extensions/*" com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --port {{ .Port }} --root-dir {{ .Root }} --global-response-templating --record-mappings --verbose diff --git a/static/tests-statuses.yml b/static/tests-statuses.yml new file mode 100644 index 0000000..976bb85 --- /dev/null +++ b/static/tests-statuses.yml @@ -0,0 +1,11 @@ +generation: + skip: + - simple-1-flaky + success: + - simple-success + fail: + - simple-2-fail + +run-some-fancy-test-name: + skip: + - flaky-test-1 \ No newline at end of file diff --git a/static/tests/data/openapi/petstore/petstore.yaml b/static/tests/data/openapi/petstore/petstore.yaml new file mode 100644 index 0000000..a214cf6 --- /dev/null +++ b/static/tests/data/openapi/petstore/petstore.yaml @@ -0,0 +1,29 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Petstore +paths: + /pets: + get: + operationId: listPets + responses: + '200': + description: pet response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Pet: + $ref: './schemas/pet.yml' + Error: + $ref: './schemas/error.yml' diff --git a/static/tests/data/openapi/petstore/schemas/error.yml b/static/tests/data/openapi/petstore/schemas/error.yml new file mode 100644 index 0000000..2d87b74 --- /dev/null +++ b/static/tests/data/openapi/petstore/schemas/error.yml @@ -0,0 +1,10 @@ +type: object +required: + - code + - message +properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/static/tests/data/openapi/petstore/schemas/pet.yml b/static/tests/data/openapi/petstore/schemas/pet.yml new file mode 100644 index 0000000..f056998 --- /dev/null +++ b/static/tests/data/openapi/petstore/schemas/pet.yml @@ -0,0 +1,10 @@ +type: object +properties: + id: + type: integer + format: int64 + name: + type: string +required: + - id + - name diff --git a/static/tests/data/openapi/petstore/users.yaml b/static/tests/data/openapi/petstore/users.yaml new file mode 100644 index 0000000..4253291 --- /dev/null +++ b/static/tests/data/openapi/petstore/users.yaml @@ -0,0 +1,47 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Users +paths: + /users: + get: + operationId: listUsers + responses: + '200': + description: user response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + User: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + required: + - id + - name + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/static/tests/data/static-examples/simple/api/grpc/example.proto b/static/tests/data/static-examples/simple/api/grpc/example.proto new file mode 100644 index 0000000..775bb5d --- /dev/null +++ b/static/tests/data/static-examples/simple/api/grpc/example.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package example; + +option go_package = "gitlab.io/paas/example"; + +message Request {} + +message Response {} + +service Example { + rpc Unary(Request) returns (Response); + rpc ClientSideStream(stream Request) returns (Response); + rpc ServerSideStream(Request) returns (stream Response); + rpc BidirectionalStream(stream Request) returns (stream Response); +} \ No newline at end of file diff --git a/static/tests/data/static-examples/two-service/api/grpc/example.proto b/static/tests/data/static-examples/two-service/api/grpc/example.proto new file mode 100644 index 0000000..f35aa6c --- /dev/null +++ b/static/tests/data/static-examples/two-service/api/grpc/example.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package example; + +option go_package = "gitlab.io/paas/example"; + +message Request {} + +message Response {} + +service Example { + rpc Unary(Request) returns (Response); + rpc ClientSideStream(stream Request) returns (Response); + rpc ServerSideStream(Request) returns (stream Response); + rpc BidirectionalStream(stream Request) returns (stream Response); +} + +service Example2 { + rpc Unary(Request) returns (Response); + rpc ClientSideStream(stream Request) returns (Response); + rpc ServerSideStream(Request) returns (stream Response); + rpc BidirectionalStream(stream Request) returns (stream Response); +} \ No newline at end of file diff --git a/static/tests/data/static-examples/with-common-imports/api/grpc/example.proto b/static/tests/data/static-examples/with-common-imports/api/grpc/example.proto new file mode 100644 index 0000000..81fd64c --- /dev/null +++ b/static/tests/data/static-examples/with-common-imports/api/grpc/example.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package example; + +option go_package = "gitlab.io/paas/example"; + +import "google/protobuf/empty.proto"; + +message Request { + google.protobuf.Empty name = 1; +} + +message Response {} + +service Example { + rpc Unary(Request) returns (Response); + rpc ClientSideStream(stream Request) returns (Response); + rpc ServerSideStream(Request) returns (stream Response); + rpc BidirectionalStream(stream Request) returns (stream Response); +} \ No newline at end of file diff --git a/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/example.proto b/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/example.proto new file mode 100644 index 0000000..d49c052 --- /dev/null +++ b/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/example.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package example; + +option go_package = "gitlab.io/paas/example"; + +import "go/custom.proto"; +import "fallthrough/fallthrough.proto"; + +service Example { + rpc Unary(fallthrough.CustomFallthroughEmpty) returns (go.CustomEmpty); + rpc ClientSideStream(stream fallthrough.CustomFallthroughEmpty) returns (go.CustomEmpty); + rpc ServerSideStream(fallthrough.CustomFallthroughEmpty) returns (stream go.CustomEmpty); + rpc BidirectionalStream(stream fallthrough.CustomFallthroughEmpty) returns (stream go.CustomEmpty); +} \ No newline at end of file diff --git a/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/fallthrough/fallthrough.proto b/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/fallthrough/fallthrough.proto new file mode 100644 index 0000000..7e6245f --- /dev/null +++ b/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/fallthrough/fallthrough.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package fallthrough; + +option go_package = "gitlab.io/paas/example/fallthrough"; + +message CustomFallthroughEmpty {} \ No newline at end of file diff --git a/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/go/custom.proto b/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/go/custom.proto new file mode 100644 index 0000000..9f4924e --- /dev/null +++ b/static/tests/data/static-examples/with-local-imports-as-reciever-and-golang-keywords/api/grpc/go/custom.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package go; + +option go_package = "gitlab.io/paas/example/go"; + +message CustomEmpty {} diff --git a/static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/example.proto b/static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/example.proto new file mode 100644 index 0000000..4d07a6e --- /dev/null +++ b/static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/example.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package example; + +option go_package = "gitlab.io/paas/example"; + +import "local/custom.proto"; + +message Request {} + +message Response {} + +service Example { + rpc Unary(local.CustomEmpty) returns (Response); + rpc ClientSideStream(stream Request) returns (Response); + rpc ServerSideStream(Request) returns (stream Response); + rpc BidirectionalStream(stream Request) returns (stream Response); +} \ No newline at end of file diff --git a/static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/local/custom.proto b/static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/local/custom.proto new file mode 100644 index 0000000..551af6e --- /dev/null +++ b/static/tests/data/static-examples/with-local-imports-as-reciever/api/grpc/local/custom.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package local; + +option go_package = "gitlab.io/paas/example/local"; + +message CustomEmpty {} diff --git a/static/tests/data/static-examples/with-local-imports/api/grpc/example.proto b/static/tests/data/static-examples/with-local-imports/api/grpc/example.proto new file mode 100644 index 0000000..3daf7f7 --- /dev/null +++ b/static/tests/data/static-examples/with-local-imports/api/grpc/example.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package example; + +option go_package = "gitlab.io/paas/example"; + +import "import/custom.proto"; + +message Request { + import1.CustomEmpty name = 1; +} + +message Response {} + +service Example { + rpc Unary(Request) returns (Response); + rpc ClientSideStream(stream Request) returns (Response); + rpc ServerSideStream(Request) returns (stream Response); + rpc BidirectionalStream(stream Request) returns (stream Response); +} \ No newline at end of file diff --git a/static/tests/data/static-examples/with-local-imports/api/grpc/import/custom.proto b/static/tests/data/static-examples/with-local-imports/api/grpc/import/custom.proto new file mode 100644 index 0000000..8a845e7 --- /dev/null +++ b/static/tests/data/static-examples/with-local-imports/api/grpc/import/custom.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package import1; + +option go_package = "gitlab.io/paas/example/import"; + +message CustomEmpty {} diff --git a/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/another.proto b/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/another.proto new file mode 100644 index 0000000..4b7ab41 --- /dev/null +++ b/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/another.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +package example; + +message Response {} diff --git a/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/example.proto b/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/example.proto new file mode 100644 index 0000000..87092c1 --- /dev/null +++ b/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/example.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package example; + +import "another.proto"; +import "import/another.proto"; + +message Request {} + +service Example { + rpc Unary(Request) returns (Response); + rpc ClientSideStream(stream Request) returns (Response); + rpc ServerSideStream(Request) returns (stream Response); + rpc BidirectionalStream(stream Request) returns (stream inner.AnotherResponse); +} \ No newline at end of file diff --git a/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/import/another.proto b/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/import/another.proto new file mode 100644 index 0000000..d1e343e --- /dev/null +++ b/static/tests/data/static-examples/without-gopackage-with-local-imports/api/grpc/import/another.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +package inner; + +message AnotherResponse {} diff --git a/static/tests/data/static-examples/without-gopackage/api/grpc/example.proto b/static/tests/data/static-examples/without-gopackage/api/grpc/example.proto new file mode 100644 index 0000000..fabf307 --- /dev/null +++ b/static/tests/data/static-examples/without-gopackage/api/grpc/example.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package example; + +message Request {} + +message Response {} + +service Example { + rpc Unary(Request) returns (Response); + rpc ClientSideStream(stream Request) returns (Response); + rpc ServerSideStream(Request) returns (stream Response); + rpc BidirectionalStream(stream Request) returns (stream Response); +} \ No newline at end of file diff --git a/static/tests/data/static-examples/without-methods/api/grpc/example.proto b/static/tests/data/static-examples/without-methods/api/grpc/example.proto new file mode 100644 index 0000000..6c8a582 --- /dev/null +++ b/static/tests/data/static-examples/without-methods/api/grpc/example.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package example; + +option go_package = "gitlab.io/paas/example"; + +message Request {} + +message Response {} + +service Example {} \ No newline at end of file diff --git a/static/tests/data/static-examples/without-proto-files/api/grpc/example.txt b/static/tests/data/static-examples/without-proto-files/api/grpc/example.txt new file mode 100644 index 0000000..6c8a582 --- /dev/null +++ b/static/tests/data/static-examples/without-proto-files/api/grpc/example.txt @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package example; + +option go_package = "gitlab.io/paas/example"; + +message Request {} + +message Response {} + +service Example {} \ No newline at end of file diff --git a/static/tests/data/supervisord/empty/supervisord.conf b/static/tests/data/supervisord/empty/supervisord.conf new file mode 100644 index 0000000..014c8ef --- /dev/null +++ b/static/tests/data/supervisord/empty/supervisord.conf @@ -0,0 +1,6 @@ +[include] +files = mocks/*.conf + +[program:mocks-watcher] +environment = A="env 1",B="this is a test" +command = watcher --mocks="hello" diff --git a/static/tests/data/supervisord/one-service/mocks/mock-awesome.conf b/static/tests/data/supervisord/one-service/mocks/mock-awesome.conf new file mode 100644 index 0000000..da83981 --- /dev/null +++ b/static/tests/data/supervisord/one-service/mocks/mock-awesome.conf @@ -0,0 +1,3 @@ +[program:mock-awesome] +environment = ROOT="/home/mock/awesome",PORT="8000",NAME="awesome" +command = java -cp "/var/wiremock/lib/*:/var/wiremock/extensions/*" com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --port 8000 --root-dir /static/tests/data/wiremock/configs/two-services/awesome diff --git a/static/tests/data/supervisord/one-service/supervisord.conf b/static/tests/data/supervisord/one-service/supervisord.conf new file mode 100644 index 0000000..014c8ef --- /dev/null +++ b/static/tests/data/supervisord/one-service/supervisord.conf @@ -0,0 +1,6 @@ +[include] +files = mocks/*.conf + +[program:mocks-watcher] +environment = A="env 1",B="this is a test" +command = watcher --mocks="hello" diff --git a/static/tests/data/supervisord/simple/supervisord.conf b/static/tests/data/supervisord/simple/supervisord.conf new file mode 100644 index 0000000..34462df --- /dev/null +++ b/static/tests/data/supervisord/simple/supervisord.conf @@ -0,0 +1,2 @@ +[program:mock-awesome] +command = java -cp "/var/wiremock/lib/*:/var/wiremock/extensions/*" com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --port 8000 --root-dir static/tests/data/wiremock/configs/two-services/awesome diff --git a/static/tests/data/supervisord/two-services/mocks/mock-awesome.conf b/static/tests/data/supervisord/two-services/mocks/mock-awesome.conf new file mode 100644 index 0000000..22cab84 --- /dev/null +++ b/static/tests/data/supervisord/two-services/mocks/mock-awesome.conf @@ -0,0 +1,3 @@ +[program:mock-awesome] +environment = ROOT="/home/mock/awesome",PORT="8000",NAME="awesome" +command = java -cp "/var/wiremock/lib/*:/var/wiremock/extensions/*" com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --port 8000 --root-dir static/tests/data/wiremock/configs/two-services/awesome diff --git a/static/tests/data/supervisord/two-services/mocks/mock-push-sender.conf b/static/tests/data/supervisord/two-services/mocks/mock-push-sender.conf new file mode 100644 index 0000000..95fb59f --- /dev/null +++ b/static/tests/data/supervisord/two-services/mocks/mock-push-sender.conf @@ -0,0 +1,3 @@ +[program:mock-push-sender] +environment = ROOT="/home/mock/push-sender",PORT="8001",NAME="push-sender" +command = java -cp "/var/wiremock/lib/*:/var/wiremock/extensions/*" com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --port 8001 --root-dir static/tests/data/wiremock/configs/two-services/awesome diff --git a/static/tests/data/supervisord/two-services/supervisord.conf b/static/tests/data/supervisord/two-services/supervisord.conf new file mode 100644 index 0000000..014c8ef --- /dev/null +++ b/static/tests/data/supervisord/two-services/supervisord.conf @@ -0,0 +1,6 @@ +[include] +files = mocks/*.conf + +[program:mocks-watcher] +environment = A="env 1",B="this is a test" +command = watcher --mocks="hello" diff --git a/static/tests/data/supervisord/with-includes/mocks/mock-awesome.conf b/static/tests/data/supervisord/with-includes/mocks/mock-awesome.conf new file mode 100644 index 0000000..22cab84 --- /dev/null +++ b/static/tests/data/supervisord/with-includes/mocks/mock-awesome.conf @@ -0,0 +1,3 @@ +[program:mock-awesome] +environment = ROOT="/home/mock/awesome",PORT="8000",NAME="awesome" +command = java -cp "/var/wiremock/lib/*:/var/wiremock/extensions/*" com.github.tomakehurst.wiremock.standalone.WireMockServerRunner --port 8000 --root-dir static/tests/data/wiremock/configs/two-services/awesome diff --git a/static/tests/data/supervisord/with-includes/supervisord.conf b/static/tests/data/supervisord/with-includes/supervisord.conf new file mode 100644 index 0000000..014c8ef --- /dev/null +++ b/static/tests/data/supervisord/with-includes/supervisord.conf @@ -0,0 +1,6 @@ +[include] +files = mocks/*.conf + +[program:mocks-watcher] +environment = A="env 1",B="this is a test" +command = watcher --mocks="hello" diff --git a/static/tests/data/wiremock/with-domains/awesome/keep b/static/tests/data/wiremock/with-domains/awesome/keep new file mode 100644 index 0000000..e69de29 diff --git a/static/tests/data/wiremock/with-domains/push-sender/keep b/static/tests/data/wiremock/with-domains/push-sender/keep new file mode 100644 index 0000000..e69de29 diff --git a/tests/.keep b/tests/.keep new file mode 100644 index 0000000..e69de29