From cc4a9617a031087ac62bd56a48576528ac0c81ba Mon Sep 17 00:00:00 2001 From: Johnny Graettinger Date: Thu, 5 Dec 2024 16:00:24 -0600 Subject: [PATCH] remove data-plane-gateway implementation and make it a simple proxy Since data-plane-gateway was written: * The connector networking frontend was moved into reactors and significantly improved. * gRPC Web REST handlers were implemented in reactors and gazette. * reactors and gazette implemented first-class fine grain authorizations. Remove the historical implementations of these features, instead updating to the implementations used by reactors and brokers today. Remove authorization checks, and just verify and pass-through an authorization header. Issue #estuary/flow/issues/1627 --- .dockerignore | 2 - .github/workflows/ci.yaml | 40 +- .gitignore | 7 +- Dockerfile | 44 +- Makefile | 42 - auth/auth.go | 184 ----- broker_service.yaml | 11 - consumer_service.yaml | 11 - docs/data-plane-gateway-v1.excalidraw | 755 ------------------ docs/data-plane-gateway-v1.png | Bin 56128 -> 0 bytes gen/broker/protocol/protocol.pb.gw.go | 226 ------ gen/broker/protocol/protocol.swagger.json | 687 ---------------- gen/consumer/protocol/protocol.pb.gw.go | 251 ------ gen/consumer/protocol/protocol.swagger.json | 670 ---------------- go.mod | 56 +- go.sum | 230 +++--- grpc.go | 147 ---- journal_server.go | 192 ++--- main.go | 235 +++--- proxy/connection.go | 120 --- proxy/http.go | 255 ------ proxy/proxy.go | 675 ---------------- proxy/redirect.go | 98 --- rest.go | 131 --- schema_inference.go | 65 -- shard_server.go | 84 +- test.sh | 154 ---- .../arabic-source-hello-world.flow.yaml | 15 - test/acmeCo/greetings.schema.yaml | 5 - test/acmeCo/source-hello-world.config.yaml | 1 - test/acmeCo/source-hello-world.flow.yaml | 15 - 31 files changed, 363 insertions(+), 5045 deletions(-) delete mode 100644 Makefile delete mode 100644 auth/auth.go delete mode 100644 broker_service.yaml delete mode 100644 consumer_service.yaml delete mode 100644 docs/data-plane-gateway-v1.excalidraw delete mode 100644 docs/data-plane-gateway-v1.png delete mode 100644 gen/broker/protocol/protocol.pb.gw.go delete mode 100644 gen/broker/protocol/protocol.swagger.json delete mode 100644 gen/consumer/protocol/protocol.pb.gw.go delete mode 100644 gen/consumer/protocol/protocol.swagger.json delete mode 100644 grpc.go delete mode 100644 proxy/connection.go delete mode 100644 proxy/http.go delete mode 100644 proxy/proxy.go delete mode 100644 proxy/redirect.go delete mode 100644 rest.go delete mode 100644 schema_inference.go delete mode 100755 test.sh delete mode 100644 test/acmeCo/arabic-source-hello-world.flow.yaml delete mode 100644 test/acmeCo/greetings.schema.yaml delete mode 100644 test/acmeCo/source-hello-world.config.yaml delete mode 100644 test/acmeCo/source-hello-world.flow.yaml diff --git a/.dockerignore b/.dockerignore index 676f850..0fa14aa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,2 @@ docs/ client/ -test/ -*.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4fd0886..318d2a5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,12 +8,10 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v2 - with: - fetch-depth: 0 - name: Prepare id: prep @@ -26,45 +24,11 @@ jobs: echo "${{ secrets.GITHUB_TOKEN }}" | \ docker login --username ${{ github.actor }} --password-stdin ghcr.io - - uses: actions/setup-go@v3 - with: - # Installs the go version specified in go.mod - go-version-file: 'go.mod' - cache: true - - - name: Install protobuf-compiler - run: sudo apt install -y libprotobuf-dev protobuf-compiler - - - uses: denoland/setup-deno@v1 - with: - deno-version: v1.32.1 - - - name: Fetch Flow - run: | - mkdir $HOME/bin \ - && curl -L --proto '=https' --tlsv1.2 -sSf "https://github.com/estuary/flow/releases/download/dev/flow-x86-linux.tar.gz" \ - | tar -zx -C $HOME/bin - - - name: Setup Protobuf Tools - run: make protobuf_tools - - - name: Add GOBIN to PATH - run: echo "$HOME/go/bin" >> $GITHUB_PATH - - name: Install Go deps run: go mod download - name: Build - run: make && go build -o $HOME/bin/data-plane-gateway . - - - name: Ensure that generated files are unchanged. - run: | - git status \ - && git diff \ - && [[ -z "$(git status --porcelain)" ]] || exit 1 - - - name: Run Tests - run: ./test.sh run $HOME/bin/data-plane-gateway $HOME/bin/flowctl-go + run: go build -o data-plane-gateway - name: Build Docker Image uses: docker/build-push-action@v2 diff --git a/.gitignore b/.gitignore index 1c76e0b..67e9db6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1 @@ -# This certificate and key are generated by start-flow.sh for local development -local-tls-cert.pem -local-tls-private-key.pem - -data-plane-gateway -test/tmp/* +data-plane-gateway \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6290f3b..4dfca99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,15 @@ -# Build Stage -################################################################################ -FROM golang as builder - -WORKDIR /builder - -RUN apt-get update && apt-get install -y openssl - -# Download & compile dependencies early. Doing this separately allows for layer -# caching opportunities when no dependencies are updated. -COPY go.* ./ -RUN go mod download - -# Build the gateway. -COPY *.go ./ -COPY gen ./gen -COPY auth ./auth -COPY proxy ./proxy -RUN go build . - -# Generate a self-signed certificate to allow the server to use TLS -RUN openssl req -x509 -nodes -days 1095 \ - -subj "/C=CA/ST=QC/O=Estuary/CN=not-a-real-hostname.test" \ - -newkey rsa:2048 -keyout tls-private-key.pem \ - -out tls-cert.pem - -# We'll copy the sh executable out of this, since distroless doesn't have a package manager with -# which to install one -FROM busybox:1.34-musl as busybox - -# Runtime Stage -################################################################################ -FROM gcr.io/distroless/base-debian12 - -COPY --from=busybox /bin/sh /bin/sh +FROM ubuntu:24.04 WORKDIR /app ENV PATH="/app:$PATH" # Bring in the compiled artifact from the builder. -COPY --from=builder /builder/data-plane-gateway ./ -COPY --from=builder --chown=nonroot /builder/tls-private-key.pem ./ -COPY --from=builder --chown=nonroot /builder/tls-cert.pem ./ +COPY data-plane-gateway ./ # Avoid running the data-plane-gateway as root. -USER nonroot:nonroot +USER 65534:65534 # Ensure data-plane-gateway can run on this runtime image. -RUN ./data-plane-gateway --help +RUN /app/data-plane-gateway print-config ENTRYPOINT ["/app/data-plane-gateway"] diff --git a/Makefile b/Makefile deleted file mode 100644 index cc2535d..0000000 --- a/Makefile +++ /dev/null @@ -1,42 +0,0 @@ -default: gen_rest_broker gen_rest_consumer gen_js_client - -gen_rest_broker: - ./build.sh broker - -gen_rest_consumer: - ./build.sh consumer - -gen_js_client: - deno run --allow-read --allow-env --allow-write --allow-run=npm client/bin/build_package.ts - -clean: - rm -rf gen/* client/dist/* client/src/gen/* test/tmp/* - -.PHONY: test -test: - deno test client/test/ --allow-net --allow-read --allow-write --unstable --unsafely-ignore-certificate-errors - -.PHONY: update_snapshots -update_snapshots: - deno test client/test/ --allow-net --allow-read --allow-write --unstable -- --update - - -PROTOBUF_TOOLS = \ - protoc-gen-grpc-gateway \ - protoc-gen-swagger \ - protoc-gen-go \ - protoc-gen-gogo - -protobuf_tools: $(PROTOBUF_TOOLS) - -protoc-gen-grpc-gateway: - GO111MODULEOFF=true go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@v1.16.0 - -protoc-gen-swagger: - GO111MODULEOFF=true go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger@v1.16.0 - -protoc-gen-go: - GO111MODULEOFF=true go install github.com/golang/protobuf/protoc-gen-go@v1.5.2 - -protoc-gen-gogo: - GO111MODULEOFF=true go install github.com/gogo/protobuf/protoc-gen-gogo@v1.3.2 diff --git a/auth/auth.go b/auth/auth.go deleted file mode 100644 index a31a564..0000000 --- a/auth/auth.go +++ /dev/null @@ -1,184 +0,0 @@ -package auth - -import ( - context "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - jwt "github.com/golang-jwt/jwt/v4" - "github.com/sirupsen/logrus" - pb "go.gazette.dev/core/broker/protocol" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -// AuthCookieName is the name of the cookie that we use for passing the JWT for interactive logins. -// It's name begins with '__Host-' in order to opt in to some additional security restrictions. -// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie_prefixes -const AuthCookieName = "__Host-flow_auth" - -var ( - MissingAuthToken = errors.New("missing or empty authentication token") - InvalidAuthToken = errors.New("invalid authentication token") - UnsupportedAuthType = errors.New("invalid or unsupported Authorization header (expected 'Bearer')") - Unauthorized = errors.New("you are not authorized to access this resource") -) - -// AuthenticateGrpcReq parses a JWT from the request context metadata, or returns an error if -// the request does not contain a valid auth token. This returns an error if the token exists -// but is expired, not signed by the provided `jwtVerificationKey`, or is structurally invalid in -// some other way. This does _not_ perform any sort of authorization checks, which must be handled -// separately. -func AuthenticateGrpcReq(ctx context.Context, jwtVerificationKey []byte) (*AuthorizedClaims, error) { - md, ok := metadata.FromIncomingContext(ctx) - - if !ok { - return nil, fmt.Errorf("Unauthenticated: No Headers") - } - - auth := md.Get("authorization") - if len(auth) == 0 { - return nil, MissingAuthToken - } else if len(auth[0]) == 0 { - return nil, MissingAuthToken - } else if !strings.HasPrefix(auth[0], "Bearer ") { - return nil, UnsupportedAuthType - } - - value := strings.TrimPrefix(auth[0], "Bearer ") - - var claims, err = decodeJwt(value, jwtVerificationKey) - if err != nil { - return nil, status.Error(codes.Unauthenticated, err.Error()) - } - return claims, nil -} - -// AuthenticateHttpReq parses a JWT from the request headers, or returns an error if -// the request does not contain a valid auth token. This returns an error if the token exists -// but is expired, not signed by the provided `jwtVerificationKey`, or is structurally invalid in -// some other way. This does _not_ perform any sort of authorization checks, which must be handled -// separately. -// The auth token may be provided in one of the following ways: -// - A `Bearer` token in the `Authorization` header -// - A `Cookie` called `__Host-flow_auth` -func AuthenticateHttpReq(req *http.Request, jwtVerificationKey []byte) (*AuthorizedClaims, error) { - var tokenValue string - var authSource string - auth := req.Header.Get("authorization") - if auth != "" { - if !strings.HasPrefix(auth, "Bearer ") { - return nil, InvalidAuthToken - } - tokenValue = strings.TrimPrefix(auth, "Bearer ") - authSource = "Authorization" - } - var cookie, err = req.Cookie(AuthCookieName) - if tokenValue == "" && err == http.ErrNoCookie { - return nil, MissingAuthToken - } else if tokenValue == "" && err != nil { - return nil, InvalidAuthToken - } else if cookie != nil { - tokenValue = cookie.Value - authSource = "Cookie" - } - - claims, err := decodeJwt(tokenValue, jwtVerificationKey) - // The error returned from decodeJwt may contain helpful details, but we don't want to provide all those details - // to the client. Instead we log the detailed error here and return a simpler error. This also makes it easier to - // match errors as part of error handling. - if err != nil { - logrus.WithFields(logrus.Fields{ - "host": req.Host, - "URI": req.RequestURI, - "error": err, - "authSource": authSource, - }).Debug("invalid jwt") - return nil, InvalidAuthToken - } - return claims, nil -} - -type AuthorizedClaims struct { - Prefixes []string `json:"prefixes"` - Operation string `json:"operation"` - jwt.RegisteredClaims -} - -func decodeJwt(tokenString string, jwtVerificationKey []byte) (*AuthorizedClaims, error) { - parseOpts := jwt.WithValidMethods([]string{"HS256"}) - token, err := jwt.ParseWithClaims(tokenString, new(AuthorizedClaims), func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || token.Method.Alg() != "HS256" { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - - return jwtVerificationKey, nil - }, parseOpts) - - if err != nil { - return nil, fmt.Errorf("parsing jwt: %w", err) - } - - if !token.Valid { - return nil, fmt.Errorf("JWT validation failed") - } - - authClaims := token.Claims.(*AuthorizedClaims) - - if !authClaims.VerifyExpiresAt(time.Now(), true) { - return nil, fmt.Errorf("JWT expired at %v", authClaims.ExpiresAt) - } - if !authClaims.VerifyIssuedAt(time.Now(), true) { - return nil, fmt.Errorf("JWT iat is invalid: %v", authClaims.IssuedAt) - } - - return authClaims, nil -} - -var authorizingLabels = []string{ - "name", - "prefix", - "estuary.dev/collection", - "estuary.dev/task-name", -} - -func EnforceSelectorPrefix(claims *AuthorizedClaims, selector pb.LabelSelector) error { - - var authorizedLabels = 0 - - for _, authorizingLabel := range authorizingLabels { - for _, label := range selector.Include.Labels { - if label.Name != authorizingLabel { - continue - } - - err := EnforcePrefix(claims, label.Value) - if err != nil { - return fmt.Errorf("unauthorized `%v` label: %w", authorizingLabel, err) - } - - authorizedLabels++ - } - } - - if authorizedLabels == 0 { - return fmt.Errorf("No authorizing labels provided") - } - - return nil -} - -func EnforcePrefix(claims *AuthorizedClaims, name string) error { - for _, prefix := range claims.Prefixes { - if strings.HasPrefix(name, prefix) { - return nil - } - } - - return Unauthorized -} diff --git a/broker_service.yaml b/broker_service.yaml deleted file mode 100644 index d75c93e..0000000 --- a/broker_service.yaml +++ /dev/null @@ -1,11 +0,0 @@ -type: google.api.Service -config_version: 3 - -http: - rules: - - selector: protocol.Journal.List - post: /v1/journals/list - body: "*" - - selector: protocol.Journal.Read - post: /v1/journals/read - body: "*" diff --git a/consumer_service.yaml b/consumer_service.yaml deleted file mode 100644 index b62f7bb..0000000 --- a/consumer_service.yaml +++ /dev/null @@ -1,11 +0,0 @@ -type: google.api.Service -config_version: 3 - -http: - rules: - - selector: consumer.Shard.List - post: /v1/shards/list - body: "*" - - selector: consumer.Shard.Stat - post: /v1/shards/stat - body: "*" diff --git a/docs/data-plane-gateway-v1.excalidraw b/docs/data-plane-gateway-v1.excalidraw deleted file mode 100644 index 126b0cd..0000000 --- a/docs/data-plane-gateway-v1.excalidraw +++ /dev/null @@ -1,755 +0,0 @@ -{ - "type": "excalidraw", - "version": 2, - "source": "https://excalidraw.com", - "elements": [ - { - "id": "MJdySeitpjByLhuswD7TO", - "type": "text", - "x": 920, - "y": 672.5, - "width": 156, - "height": 26, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 850728034, - "version": 84, - "versionNonce": 1729644962, - "isDeleted": false, - "boundElements": [ - { - "id": "cqAVsR4Mivbalda0Mn8l0", - "type": "arrow" - }, - { - "id": "J39rIXnj4TLS6eaOr0hWo", - "type": "arrow" - } - ], - "updated": 1650659473690, - "link": null, - "locked": false, - "text": "Gazette / Flow", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 18, - "containerId": null, - "originalText": "Gazette / Flow" - }, - { - "id": "-n-GicAM1ElFc3X-l85yH", - "type": "text", - "x": 783, - "y": 747, - "width": 150, - "height": 26, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 2015968446, - "version": 73, - "versionNonce": 1220287614, - "isDeleted": false, - "boundElements": null, - "updated": 1650659073928, - "link": null, - "locked": false, - "text": "Journal Service", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 18, - "containerId": null, - "originalText": "Journal Service" - }, - { - "id": "CT5Vc0FiOZmI5mubYjGQO", - "type": "text", - "x": 1085, - "y": 750, - "width": 134, - "height": 26, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 1941673726, - "version": 96, - "versionNonce": 1174434402, - "isDeleted": false, - "boundElements": null, - "updated": 1650659073928, - "link": null, - "locked": false, - "text": "Shard Service", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 18, - "containerId": null, - "originalText": "Shard Service" - }, - { - "id": "1LMp9_JcJqvesBebAP35F", - "type": "text", - "x": 795, - "y": 411, - "width": 103, - "height": 52, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 693751202, - "version": 76, - "versionNonce": 948356990, - "isDeleted": false, - "boundElements": [ - { - "id": "lYhqfrhy6p6Z1RssJhTBh", - "type": "arrow" - }, - { - "id": "cqAVsR4Mivbalda0Mn8l0", - "type": "arrow" - }, - { - "id": "QB5J8hOyI3wRqRX1MxUPb", - "type": "arrow" - } - ], - "updated": 1650659443286, - "link": null, - "locked": false, - "text": "REST API\n(HTTP)", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 44, - "containerId": null, - "originalText": "REST API\n(HTTP)" - }, - { - "id": "PbSrGEyeu74m9MywLnE4j", - "type": "text", - "x": 1070, - "y": 410, - "width": 142, - "height": 52, - "angle": 0, - "strokeColor": "#aaa", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 1045088098, - "version": 46, - "versionNonce": 1678915902, - "isDeleted": false, - "boundElements": [ - { - "id": "lYhqfrhy6p6Z1RssJhTBh", - "type": "arrow" - }, - { - "id": "J39rIXnj4TLS6eaOr0hWo", - "type": "arrow" - }, - { - "id": "YGZQmX4Jm5F-tEn5cSqNO", - "type": "arrow" - } - ], - "updated": 1650659439199, - "link": null, - "locked": false, - "text": "Auth Gateway\n(GRPC)", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 44, - "containerId": null, - "originalText": "Auth Gateway\n(GRPC)" - }, - { - "id": "WFZ2gqvZx_t3VT08wloZg", - "type": "text", - "x": 893, - "y": 342, - "width": 213, - "height": 26, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 2117683618, - "version": 39, - "versionNonce": 1469488446, - "isDeleted": false, - "boundElements": null, - "updated": 1650659044550, - "link": null, - "locked": false, - "text": "Data Plane Gateway", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "left", - "verticalAlign": "top", - "baseline": 18, - "containerId": null, - "originalText": "Data Plane Gateway" - }, - { - "id": "23XzPCaPfT9BQLSf99IEw", - "type": "rectangle", - "x": 763, - "y": 704, - "width": 482, - "height": 110, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 1339161890, - "version": 49, - "versionNonce": 1849679038, - "isDeleted": false, - "boundElements": null, - "updated": 1650659073928, - "link": null, - "locked": false - }, - { - "type": "rectangle", - "version": 70, - "versionNonce": 362568930, - "isDeleted": false, - "id": "9hG26SAcmdArYvSxbxopM", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 765, - "y": 380, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "width": 482, - "height": 110, - "seed": 71870590, - "groupIds": [], - "strokeSharpness": "sharp", - "boundElements": [ - { - "id": "cqAVsR4Mivbalda0Mn8l0", - "type": "arrow" - }, - { - "id": "YGZQmX4Jm5F-tEn5cSqNO", - "type": "arrow" - }, - { - "id": "QB5J8hOyI3wRqRX1MxUPb", - "type": "arrow" - } - ], - "updated": 1650659465111, - "link": null, - "locked": false - }, - { - "id": "cqAVsR4Mivbalda0Mn8l0", - "type": "arrow", - "x": 847.6816054955311, - "y": 477.2827206791286, - "width": 136.48279174792322, - "height": 182.72209296590643, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "round", - "seed": 1737246078, - "version": 177, - "versionNonce": 848712930, - "isDeleted": false, - "boundElements": null, - "updated": 1650659474314, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 136.48279174792322, - 182.72209296590643 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "1LMp9_JcJqvesBebAP35F", - "focus": 0.4075661947007907, - "gap": 14.282720679128545 - }, - "endBinding": { - "elementId": "MJdySeitpjByLhuswD7TO", - "focus": 0.05937528507666102, - "gap": 12.495186354964972 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "lYhqfrhy6p6Z1RssJhTBh", - "type": "arrow", - "x": 904, - "y": 435, - "width": 158, - "height": 1, - "angle": 0, - "strokeColor": "#aaa", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "round", - "seed": 1880012350, - "version": 40, - "versionNonce": 568683326, - "isDeleted": false, - "boundElements": null, - "updated": 1650659108336, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 158, - 1 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "1LMp9_JcJqvesBebAP35F", - "focus": -0.08979444644789038, - "gap": 6 - }, - "endBinding": { - "elementId": "PbSrGEyeu74m9MywLnE4j", - "focus": -0.01890404402967217, - "gap": 8 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "J39rIXnj4TLS6eaOr0hWo", - "type": "arrow", - "x": 1153.0506546664765, - "y": 472, - "width": 153.91336934686592, - "height": 191, - "angle": 0, - "strokeColor": "#aaa", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "round", - "seed": 2084826018, - "version": 65, - "versionNonce": 1350396066, - "isDeleted": false, - "boundElements": null, - "updated": 1650659474315, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -153.91336934686592, - 191 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "PbSrGEyeu74m9MywLnE4j", - "focus": -0.44655847789591496, - "gap": 10 - }, - "endBinding": { - "elementId": "MJdySeitpjByLhuswD7TO", - "focus": -0.19207370576191868, - "gap": 9.5 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "ShdF4UgM-MhQs5whuvhAT", - "type": "line", - "x": 918, - "y": 562, - "width": 26, - "height": 29, - "angle": 0, - "strokeColor": "#aaa", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "round", - "seed": 533225250, - "version": 15, - "versionNonce": 824383842, - "isDeleted": false, - "boundElements": null, - "updated": 1650659264210, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 26, - -29 - ] - ], - "lastCommittedPoint": null, - "startBinding": null, - "endBinding": null, - "startArrowhead": null, - "endArrowhead": null - }, - { - "type": "line", - "version": 88, - "versionNonce": 528162786, - "isDeleted": false, - "id": "ZzbRlZV-xnVh3meURLjQx", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 4.71238898038469, - "x": 918, - "y": 560.5, - "strokeColor": "#aaa", - "backgroundColor": "transparent", - "width": 26, - "height": 29, - "seed": 960439742, - "groupIds": [], - "strokeSharpness": "round", - "boundElements": [], - "updated": 1650659277925, - "link": null, - "locked": false, - "startBinding": null, - "endBinding": null, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": null, - "points": [ - [ - 0, - 0 - ], - [ - 26, - -29 - ] - ] - }, - { - "id": "opgpUWAmAGw8hlSgX0ii-", - "type": "text", - "x": 1189, - "y": 461.5, - "width": 46, - "height": 21, - "angle": 0, - "strokeColor": "#aaa", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 2089866146, - "version": 50, - "versionNonce": 202574178, - "isDeleted": false, - "boundElements": null, - "updated": 1650659386492, - "link": null, - "locked": false, - "text": "*TBD", - "fontSize": 16, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 15, - "containerId": null, - "originalText": "*TBD" - }, - { - "id": "hJiBG97kNhzpsAOCdRQPv", - "type": "text", - "x": 760, - "y": 159, - "width": 166, - "height": 26, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 829394210, - "version": 67, - "versionNonce": 1095899582, - "isDeleted": false, - "boundElements": [ - { - "id": "QB5J8hOyI3wRqRX1MxUPb", - "type": "arrow" - } - ], - "updated": 1650659449038, - "link": null, - "locked": false, - "text": "Typescript Client", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 18, - "containerId": null, - "originalText": "Typescript Client" - }, - { - "id": "frdcqGeJn14yhylHnJy-f", - "type": "text", - "x": 1116, - "y": 161, - "width": 62, - "height": 26, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "sharp", - "seed": 1224034494, - "version": 61, - "versionNonce": 1008206434, - "isDeleted": false, - "boundElements": [ - { - "id": "YGZQmX4Jm5F-tEn5cSqNO", - "type": "arrow" - } - ], - "updated": 1650659683512, - "link": null, - "locked": false, - "text": "gazctl", - "fontSize": 20, - "fontFamily": 1, - "textAlign": "center", - "verticalAlign": "top", - "baseline": 18, - "containerId": null, - "originalText": "gazctl" - }, - { - "id": "YGZQmX4Jm5F-tEn5cSqNO", - "type": "arrow", - "x": 1145.875248814946, - "y": 195, - "width": 0.47929021754112, - "height": 174.8529603938107, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "round", - "seed": 447883966, - "version": 177, - "versionNonce": 1752000034, - "isDeleted": false, - "boundElements": null, - "updated": 1650659683512, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -0.47929021754112, - 174.8529603938107 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "frdcqGeJn14yhylHnJy-f", - "focus": 0.03324397162922169, - "gap": 8 - }, - "endBinding": { - "elementId": "9hG26SAcmdArYvSxbxopM", - "focus": 0.5773043524017547, - "gap": 10.14703960618931 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - }, - { - "id": "QB5J8hOyI3wRqRX1MxUPb", - "type": "arrow", - "x": 841.0005513444667, - "y": 196, - "width": 0.4912366299598716, - "height": 169.29008437548765, - "angle": 0, - "strokeColor": "#000000", - "backgroundColor": "transparent", - "fillStyle": "hachure", - "strokeWidth": 1, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "strokeSharpness": "round", - "seed": 1211597218, - "version": 185, - "versionNonce": 1717291518, - "isDeleted": false, - "boundElements": null, - "updated": 1650659465111, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -0.4912366299598716, - 169.29008437548765 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "hJiBG97kNhzpsAOCdRQPv", - "focus": 0.02324012168030883, - "gap": 11 - }, - "endBinding": { - "elementId": "9hG26SAcmdArYvSxbxopM", - "focus": -0.6870676864830009, - "gap": 14.709915624512348 - }, - "startArrowhead": null, - "endArrowhead": "arrow" - } - ], - "appState": { - "gridSize": null, - "viewBackgroundColor": "#ffffff" - }, - "files": {} -} \ No newline at end of file diff --git a/docs/data-plane-gateway-v1.png b/docs/data-plane-gateway-v1.png deleted file mode 100644 index 9029c227073b085c5318f9a02d428bb1b2599d1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56128 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 ZSi^E{uU*YC^~8cd>dIP5Wr}7&{|_g9_TT^j diff --git a/gen/broker/protocol/protocol.pb.gw.go b/gen/broker/protocol/protocol.pb.gw.go deleted file mode 100644 index e82b9ad..0000000 --- a/gen/broker/protocol/protocol.pb.gw.go +++ /dev/null @@ -1,226 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: broker/protocol/protocol.proto - -/* -Package protocol is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package gw - -import ( - "context" - ext "go.gazette.dev/core/broker/protocol" - "io" - "net/http" - - "github.com/golang/protobuf/descriptor" - "github.com/golang/protobuf/proto" - "github.com/grpc-ecosystem/grpc-gateway/runtime" - "github.com/grpc-ecosystem/grpc-gateway/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = descriptor.ForMessage -var _ = metadata.Join - -func request_Journal_List_0(ctx context.Context, marshaler runtime.Marshaler, client ext.JournalClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ext.ListRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.List(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_Journal_List_0(ctx context.Context, marshaler runtime.Marshaler, server ext.JournalServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ext.ListRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.List(ctx, &protoReq) - return msg, metadata, err - -} - -func request_Journal_Read_0(ctx context.Context, marshaler runtime.Marshaler, client ext.JournalClient, req *http.Request, pathParams map[string]string) (ext.Journal_ReadClient, runtime.ServerMetadata, error) { - var protoReq ext.ReadRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - stream, err := client.Read(ctx, &protoReq) - if err != nil { - return nil, metadata, err - } - header, err := stream.Header() - if err != nil { - return nil, metadata, err - } - metadata.HeaderMD = header - return stream, metadata, nil - -} - -// ext.RegisterJournalHandlerServer registers the http handlers for service Journal to "mux". -// UnaryRPC :call ext.JournalServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterJournalHandlerFromEndpoint instead. -func RegisterJournalHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ext.JournalServer) error { - - mux.Handle("POST", pattern_Journal_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_Journal_List_0(rctx, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Journal_List_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_Journal_Read_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") - _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - }) - - return nil -} - -// RegisterJournalHandlerFromEndpoint is same as RegisterJournalHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterJournalHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.Dial(endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterJournalHandler(ctx, mux, conn) -} - -// RegisterJournalHandler registers the http handlers for service Journal to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterJournalHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterJournalHandlerClient(ctx, mux, ext.NewJournalClient(conn)) -} - -// ext.RegisterJournalHandlerClient registers the http handlers for service Journal -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "JournalClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "JournalClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "JournalClient" to call the correct interceptors. -func RegisterJournalHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ext.JournalClient) error { - - mux.Handle("POST", pattern_Journal_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_Journal_List_0(rctx, inboundMarshaler, client, req, pathParams) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Journal_List_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_Journal_Read_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_Journal_Read_0(rctx, inboundMarshaler, client, req, pathParams) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Journal_Read_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_Journal_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "journals", "list"}, "", runtime.AssumeColonVerbOpt(true))) - - pattern_Journal_Read_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "journals", "read"}, "", runtime.AssumeColonVerbOpt(true))) -) - -var ( - forward_Journal_List_0 = runtime.ForwardResponseMessage - - forward_Journal_Read_0 = runtime.ForwardResponseStream -) diff --git a/gen/broker/protocol/protocol.swagger.json b/gen/broker/protocol/protocol.swagger.json deleted file mode 100644 index f61d1ba..0000000 --- a/gen/broker/protocol/protocol.swagger.json +++ /dev/null @@ -1,687 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "title": "broker/protocol/protocol.proto", - "version": "version not set" - }, - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "paths": { - "/v1/journals/list": { - "post": { - "summary": "List Journals, their JournalSpecs and current Routes.", - "operationId": "Journal_List", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/protocolListResponse" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/runtimeError" - } - } - }, - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/protocolListRequest" - } - } - ], - "tags": [ - "Journal" - ] - } - }, - "/v1/journals/read": { - "post": { - "summary": "Read from a specific Journal.", - "operationId": "Journal_Read", - "responses": { - "200": { - "description": "A successful response.(streaming responses)", - "schema": { - "type": "object", - "properties": { - "result": { - "$ref": "#/definitions/protocolReadResponse" - }, - "error": { - "$ref": "#/definitions/runtimeStreamError" - } - }, - "title": "Stream result of protocolReadResponse" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/runtimeError" - } - } - }, - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/protocolReadRequest" - } - } - ], - "tags": [ - "Journal" - ] - } - } - }, - "definitions": { - "ApplyRequestChange": { - "type": "object", - "properties": { - "expectModRevision": { - "type": "string", - "format": "int64", - "description": "Expected ModRevision of the current JournalSpec. If the Journal is being\ncreated, expect_mod_revision is zero." - }, - "upsert": { - "$ref": "#/definitions/protocolJournalSpec", - "description": "JournalSpec to be updated (if expect_mod_revision \u003e 0) or created\n(if expect_mod_revision == 0)." - }, - "delete": { - "type": "string", - "description": "Journal to be deleted. expect_mod_revision must not be zero." - } - }, - "description": "Change defines an insertion, update, or deletion to be applied to the set\nof JournalSpecs. Exactly one of |upsert| or |delete| must be set." - }, - "FragmentsResponse_Fragment": { - "type": "object", - "properties": { - "spec": { - "$ref": "#/definitions/protocolFragment" - }, - "signedUrl": { - "type": "string", - "description": "SignedURL is a temporary URL at which a direct GET of the Fragment may\nbe issued, signed by the broker's credentials. Set only if the request\nspecified a SignatureTTL." - } - }, - "description": "Fragments of the Response." - }, - "HeaderEtcd": { - "type": "object", - "properties": { - "clusterId": { - "type": "string", - "format": "uint64", - "description": "cluster_id is the ID of the cluster." - }, - "memberId": { - "type": "string", - "format": "uint64", - "description": "member_id is the ID of the member." - }, - "revision": { - "type": "string", - "format": "int64", - "description": "revision is the Etcd key-value store revision when the request was\napplied." - }, - "raftTerm": { - "type": "string", - "format": "uint64", - "description": "raft_term is the raft term when the request was applied." - } - }, - "description": "Etcd represents the effective Etcd MVCC state under which a Gazette broker\nis operating in its processing of requests and responses. Its inclusion\nallows brokers to reason about relative \"happened before\" Revision ordering\nof apparent routing conflicts in proxied or replicated requests, as well\nas enabling sanity checks over equality of Etcd ClusterId (and precluding,\nfor example, split-brain scenarios where different brokers are backed by\ndifferent Etcd clusters). Etcd is kept in sync with\netcdserverpb.ResponseHeader." - }, - "ListResponseJournal": { - "type": "object", - "properties": { - "spec": { - "$ref": "#/definitions/protocolJournalSpec" - }, - "modRevision": { - "type": "string", - "format": "int64", - "description": "Current ModRevision of the JournalSpec." - }, - "route": { - "$ref": "#/definitions/protocolRoute", - "description": "Route of the journal, including endpoints." - } - }, - "description": "Journals of the response." - }, - "ProcessSpecID": { - "type": "object", - "properties": { - "zone": { - "type": "string", - "description": "\"Zone\" in which the process is running. Zones may be AWS, Azure, or\nGoogle Cloud Platform zone identifiers, or rack locations within a colo,\nor given some other custom meaning. Gazette will replicate across\nmultiple zones, and seeks to minimize traffic which must cross zones (for\nexample, by proxying reads to a broker in the current zone)." - }, - "suffix": { - "type": "string", - "description": "Unique suffix of the process within |zone|. It is permissible for a\nsuffix value to repeat across zones, but never within zones. In practice,\nit's recommended to use a FQDN, Kubernetes Pod name, or comparable unique\nand self-describing value as the ID suffix." - } - }, - "description": "ID composes a zone and a suffix to uniquely identify a ProcessSpec." - }, - "protobufAny": { - "type": "object", - "properties": { - "typeUrl": { - "type": "string" - }, - "value": { - "type": "string", - "format": "byte" - } - } - }, - "protocolAppendResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/protocolStatus", - "description": "Status of the Append RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response." - }, - "commit": { - "$ref": "#/definitions/protocolFragment", - "description": "If status is OK, then |commit| is the Fragment which places the\ncommitted Append content within the Journal." - }, - "registers": { - "$ref": "#/definitions/protocolLabelSet", - "description": "Current registers of the journal." - }, - "totalChunks": { - "type": "string", - "format": "int64", - "description": "Total number of RPC content chunks processed in this append." - }, - "delayedChunks": { - "type": "string", - "format": "int64", - "description": "Number of content chunks which were delayed by journal flow control." - } - }, - "description": "AppendResponse is the unary response message of the broker Append RPC." - }, - "protocolApplyResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/protocolStatus", - "description": "Status of the Apply RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response." - } - }, - "description": "ApplyResponse is the unary response message of the broker Apply RPC." - }, - "protocolCompressionCodec": { - "type": "string", - "enum": [ - "INVALID", - "NONE", - "GZIP", - "ZSTANDARD", - "SNAPPY", - "GZIP_OFFLOAD_DECOMPRESSION" - ], - "default": "INVALID", - "description": "CompressionCode defines codecs known to Gazette.\n\n - INVALID: INVALID is the zero-valued CompressionCodec, and is not a valid codec.\n - NONE: NONE encodes Fragments without any applied compression, with default suffix\n\".raw\".\n - GZIP: GZIP encodes Fragments using the Gzip library, with default suffix \".gz\".\n - ZSTANDARD: ZSTANDARD encodes Fragments using the ZStandard library, with default\nsuffix \".zst\".\n - SNAPPY: SNAPPY encodes Fragments using the Snappy library, with default suffix\n\".sz\".\n - GZIP_OFFLOAD_DECOMPRESSION: GZIP_OFFLOAD_DECOMPRESSION is the GZIP codec with additional behavior\naround reads and writes to remote Fragment stores, designed to offload\nthe work of decompression onto compatible stores. Specifically:\n * Fragments are written with a \"Content-Encoding: gzip\" header.\n * Client read requests are made with \"Accept-Encoding: identity\".\nThis can be helpful in contexts where reader IO bandwidth to the storage\nAPI is unconstrained, as the cost of decompression is offloaded to the\nstore and CPU-intensive batch readers may receive a parallelism benefit.\nWhile this codec may provide substantial read-time performance\nimprovements, it is an advanced configuration and the \"Content-Encoding\"\nheader handling can be subtle and sometimes confusing. It uses the default\nsuffix \".gzod\"." - }, - "protocolFragment": { - "type": "object", - "properties": { - "journal": { - "type": "string", - "description": "Journal of the Fragment." - }, - "begin": { - "type": "string", - "format": "int64", - "description": "Begin (inclusive) and end (exclusive) offset of the Fragment within the\nJournal." - }, - "end": { - "type": "string", - "format": "int64" - }, - "sum": { - "$ref": "#/definitions/protocolSHA1Sum", - "description": "SHA1 sum of the Fragment's content." - }, - "compressionCodec": { - "$ref": "#/definitions/protocolCompressionCodec", - "description": "Codec with which the Fragment's content is compressed." - }, - "backingStore": { - "type": "string", - "description": "Fragment store which backs the Fragment. Empty if the Fragment has yet to\nbe persisted and is still local to a Broker." - }, - "modTime": { - "type": "string", - "format": "int64", - "description": "Modification timestamp of the Fragment within the backing store,\nrepresented as seconds since the epoch." - }, - "pathPostfix": { - "type": "string", - "description": "Path postfix under which the fragment is persisted to the store.\nThe complete Fragment store path is built from any path components of the\nbacking store, followed by the journal name, followed by the path postfix." - } - }, - "description": "Fragment is a content-addressed description of a contiguous Journal span,\ndefined by the [begin, end) offset range covered by the Fragment and the\nSHA1 sum of the corresponding Journal content." - }, - "protocolFragmentsResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/protocolStatus", - "description": "Status of the Apply RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response." - }, - "fragments": { - "type": "array", - "items": { - "$ref": "#/definitions/FragmentsResponse_Fragment" - } - }, - "nextPageToken": { - "type": "string", - "format": "int64", - "description": "The NextPageToke value to be returned on subsequent Fragments requests. If\nthe value is zero then there are no more fragments to be returned for this\npage." - } - }, - "description": "FragmentsResponse is the unary response message of the broker ListFragments\nRPC." - }, - "protocolHeader": { - "type": "object", - "properties": { - "processId": { - "$ref": "#/definitions/ProcessSpecID", - "description": "ID of the process responsible for request processing. May be empty iff\nHeader is being used within a proxied request, and that request may be\ndispatched to any member of the Route." - }, - "route": { - "$ref": "#/definitions/protocolRoute", - "description": "Route of processes specifically responsible for this RPC, or an empty Route\nif any process is capable of serving the RPC." - }, - "etcd": { - "$ref": "#/definitions/HeaderEtcd" - } - }, - "description": "Header captures metadata such as the process responsible for processing\nan RPC, and its effective Etcd state." - }, - "protocolJournalSpec": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the Journal." - }, - "replication": { - "type": "integer", - "format": "int32", - "description": "Desired replication of this Journal. This defines the Journal's tolerance\nto broker failures before data loss can occur (eg, a replication factor\nof three means two failures are tolerated)." - }, - "labels": { - "$ref": "#/definitions/protocolLabelSet", - "description": "User-defined Labels of this JournalSpec. Two label names are reserved\nand may not be used within a JournalSpec's Labels: \"name\" and \"prefix\"." - }, - "fragment": { - "$ref": "#/definitions/protocolJournalSpecFragment" - }, - "flags": { - "type": "integer", - "format": "int64", - "description": "Flags of the Journal, as a combination of Flag enum values. The Flag enum\nis not used directly, as protobuf enums do not allow for or'ed bitfields." - }, - "maxAppendRate": { - "type": "string", - "format": "int64", - "description": "Maximum rate, in bytes-per-second, at which appends of this journal will\nbe processed. If zero (the default), no rate limiting is applied. A global\nrate limit still may be in effect, in which case the effective rate is the\nsmaller of the journal vs global rate." - } - }, - "description": "JournalSpec describes a Journal and its configuration." - }, - "protocolJournalSpecFragment": { - "type": "object", - "properties": { - "length": { - "type": "string", - "format": "int64", - "description": "Target content length of each Fragment. In normal operation after\nFragments reach at least this length, they will be closed and new ones\nbegun. Note lengths may be smaller at times (eg, due to changes in\nJournal routing topology). Content length differs from Fragment file\nsize, in that the former reflects uncompressed bytes." - }, - "compressionCodec": { - "$ref": "#/definitions/protocolCompressionCodec", - "description": "Codec used to compress Journal Fragments." - }, - "stores": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Multiple stores may be specified, in which case the Journal's Fragments\nare the union of all Fragments present across all stores, and new\nFragments always persist to the first specified store. This can be\nhelpful in performing incremental migrations, where new Journal content\nis written to the new store, while content in the old store remains\navailable (and, depending on fragment_retention or recovery log pruning,\nmay eventually be removed).\n\nIf no stores are specified, the Journal is still use-able but will\nnot persist Fragments to any a backing fragment store. This allows for\nreal-time streaming use cases where reads of historical data are not\nneeded.", - "title": "Storage backend base path for this Journal's Fragments. Must be in URL\nform, with the choice of backend defined by the scheme. The full path of\na Journal's Fragment is derived by joining the store path with the\nFragment's ContentPath. Eg, given a fragment_store of\n \"s3://My-AWS-bucket/a/prefix\" and a JournalSpec of name \"my/journal\",\na complete Fragment path might be:\n \"s3://My-AWS-bucket/a/prefix/my/journal/000123-000456-789abcdef.gzip" - }, - "refreshInterval": { - "type": "string", - "description": "Interval of time between refreshes of remote Fragment listings from\nconfigured fragment_stores." - }, - "retention": { - "type": "string", - "description": "Retention duration for historical Fragments of this Journal within the\nFragment stores. If less than or equal to zero, Fragments are retained\nindefinitely." - }, - "flushInterval": { - "type": "string", - "description": "Flush interval defines a uniform UTC time segment which, when passed,\nwill prompt brokers to close and persist a fragment presently being\nwritten.\n\nFlush interval may be helpful in integrating the journal with a regularly\nscheduled batch work-flow which processes new files from the fragment\nstore and has no particular awareness of Gazette. For example, setting\nflush_interval to 3600s will cause brokers to persist their present\nfragment on the hour, every hour, even if it has not yet reached its\ntarget length. A batch work-flow running at 5 minutes past the hour is\nthen reasonably assured of seeing all events from the past hour.\n\nSee also \"gazctl journals fragments --help\" for more discussion." - }, - "pathPostfixTemplate": { - "type": "string", - "description": "date={{ .Spool.FirstAppendTime.Format \"2006-01-02\" }}/hour={{\n .Spool.FirstAppendTime.Format \"15\" }}\n\nWhich will produce a path postfix like \"date=2019-11-19/hour=22\".", - "title": "Path postfix template is a Go template which evaluates to a partial\npath under which fragments are persisted to the store. A complete\nfragment path is constructed by appending path components from the\nfragment store, then the journal name, and then the postfix template.\nPath post-fixes can help in maintaining Hive compatible partitioning\nover fragment creation time. The fields \".Spool\" and \".JournalSpec\"\nare available for introspection in the template. For example,\nto partition on the UTC date and hour of creation, use:" - } - }, - "description": "Fragment is JournalSpec configuration which pertains to the creation,\npersistence, and indexing of the Journal's Fragments." - }, - "protocolLabel": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "description": "Label defines a key \u0026 value pair which can be attached to entities like\nJournalSpecs and BrokerSpecs. Labels may be used to provide identifying\nattributes which do not directly imply semantics to the core system, but\nare meaningful to users or for higher-level Gazette tools." - }, - "protocolLabelSelector": { - "type": "object", - "properties": { - "include": { - "$ref": "#/definitions/protocolLabelSet", - "description": "Include is Labels which must be matched for a LabelSet to be selected. If\nempty, all Labels are included. An include Label with empty (\"\") value is\nmatched by a Label of the same name having any value." - }, - "exclude": { - "$ref": "#/definitions/protocolLabelSet", - "description": "Exclude is Labels which cannot be matched for a LabelSet to be selected. If\nempty, no Labels are excluded. An exclude Label with empty (\"\") value\nexcludes a Label of the same name having any value." - } - }, - "description": "LabelSelector defines a filter over LabelSets." - }, - "protocolLabelSet": { - "type": "object", - "properties": { - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/protocolLabel" - }, - "description": "Labels of the set. Instances must be unique and sorted over (Name, Value)." - } - }, - "description": "LabelSet is a collection of labels and their values." - }, - "protocolListRequest": { - "type": "object", - "properties": { - "selector": { - "$ref": "#/definitions/protocolLabelSelector", - "description": "Selector optionally refines the set of journals which will be enumerated.\nIf zero-valued, all journals are returned. Otherwise, only JournalSpecs\nmatching the LabelSelector will be returned. Two meta-labels \"name\" and\n\"prefix\" are additionally supported by the selector, where:\n * name=examples/a-name will match a JournalSpec with Name\n \"examples/a-name\"\n * prefix=examples/ will match any JournalSpec having prefix \"examples/\".\n The prefix Label value must end in '/'." - } - }, - "description": "ListRequest is the unary request message of the broker List RPC." - }, - "protocolListResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/protocolStatus", - "description": "Status of the List RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response." - }, - "journals": { - "type": "array", - "items": { - "$ref": "#/definitions/ListResponseJournal" - } - } - }, - "description": "ListResponse is the unary response message of the broker List RPC." - }, - "protocolReadRequest": { - "type": "object", - "properties": { - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header is attached by a proxying broker peer." - }, - "journal": { - "type": "string", - "description": "Journal to be read." - }, - "offset": { - "type": "string", - "format": "int64", - "description": "Desired offset to begin reading from. Value -1 has special handling, where\nthe read is performed from the current write head. All other positive\nvalues specify a desired exact byte offset to read from. If the offset is\nnot available (eg, because it represents a portion of Journal which has\nbeen permanently deleted), the broker will return the next available\noffset. Callers should therefore always inspect the ReadResponse offset." - }, - "block": { - "type": "boolean", - "description": "Whether the operation should block until content becomes available.\nOFFSET_NOT_YET_AVAILABLE is returned if a non-blocking read has no ready\ncontent." - }, - "doNotProxy": { - "type": "boolean", - "description": "If do_not_proxy is true, the broker will not proxy the read to another\nbroker, or open and proxy a remote Fragment on the client's behalf." - }, - "metadataOnly": { - "type": "boolean", - "description": "If metadata_only is true, the broker will respond with Journal and\nFragment metadata but not content." - }, - "endOffset": { - "type": "string", - "format": "int64", - "description": "Offset to read through. If zero, then the read end offset is unconstrained." - }, - "beginModTime": { - "type": "string", - "format": "int64", - "description": "BeginModTime is an optional inclusive lower bound on the modification\ntimestamps of fragments read from the backing store, represented as\nseconds since the epoch. The request Offset will be advanced as-needed\nto skip persisted Fragments having a modication time before the bound." - } - }, - "description": "ReadRequest is the unary request message of the broker Read RPC." - }, - "protocolReadResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/protocolStatus", - "description": "Status of the Read RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response. Accompanies the first ReadResponse of the response\nstream." - }, - "offset": { - "type": "string", - "format": "int64", - "description": "The effective offset of the read. See ReadRequest offset." - }, - "writeHead": { - "type": "string", - "format": "int64", - "description": "The offset to next be written, by the next append transaction served by\nbroker. In other words, the last offset through which content is\navailable to be read from the Journal. This is a metadata field and will\nnot be returned with a content response." - }, - "fragment": { - "$ref": "#/definitions/protocolFragment", - "description": "Fragment to which the offset was mapped. This is a metadata field and will\nnot be returned with a content response." - }, - "fragmentUrl": { - "type": "string", - "description": "If Fragment is remote, a URL from which it may be directly read." - }, - "content": { - "type": "string", - "format": "byte", - "description": "Content chunks of the read." - } - }, - "description": "* \"Metadata\" messages, which conveys the journal Fragment addressed by the\n request which is ready to be read.\n* \"Chunk\" messages, which carry associated journal Fragment content bytes.\n\nA metadata message specifying a Fragment always precedes all \"chunks\" of the\nFragment's content. Response streams may be very long lived, having many\nmetadata and accompanying chunk messages. The reader may also block for long\nperiods of time awaiting the next metadata message (eg, if the next offset\nhasn't yet committed). However once a metadata message is read, the reader\nis assured that its associated chunk messages are immediately forthcoming.", - "title": "ReadResponse is the streamed response message of the broker Read RPC.\nResponses messages are of two types:" - }, - "protocolReplicateResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/protocolStatus", - "description": "Status of the Replicate RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response. Accompanies the first ReplicateResponse of the\nresponse stream." - }, - "fragment": { - "$ref": "#/definitions/protocolFragment", - "description": "If status is PROPOSAL_MISMATCH, then |fragment| is the replica's current\njournal Fragment, and either it or |registers| will differ from the\nprimary's proposal." - }, - "registers": { - "$ref": "#/definitions/protocolLabelSet", - "description": "If status is PROPOSAL_MISMATCH, then |registers| are the replica's current\njournal registers." - } - }, - "description": "ReplicateResponse is the streamed response message of the broker's internal\nReplicate RPC. Each message is a 1:1 response to a previously read \"proposal\"\nReplicateRequest with |acknowledge| set." - }, - "protocolRoute": { - "type": "object", - "properties": { - "members": { - "type": "array", - "items": { - "$ref": "#/definitions/ProcessSpecID" - }, - "description": "Members of the Route, ordered on ascending ProcessSpec.ID (zone, suffix)." - }, - "primary": { - "type": "integer", - "format": "int32", - "description": "Index of the ProcessSpec serving as primary within |members|,\nor -1 of no member is currently primary." - }, - "endpoints": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Endpoints of each Route member. If not empty, |endpoints| has the same\nlength and order as |members|, and captures the endpoint of each one." - } - }, - "description": "Route captures the current topology of an item and the processes serving it." - }, - "protocolSHA1Sum": { - "type": "object", - "properties": { - "part1": { - "type": "string", - "format": "uint64" - }, - "part2": { - "type": "string", - "format": "uint64" - }, - "part3": { - "type": "integer", - "format": "int64" - } - }, - "description": "SHA1Sum is a 160-bit SHA1 digest." - }, - "protocolStatus": { - "type": "string", - "enum": [ - "OK", - "JOURNAL_NOT_FOUND", - "NO_JOURNAL_PRIMARY_BROKER", - "NOT_JOURNAL_PRIMARY_BROKER", - "NOT_JOURNAL_BROKER", - "INSUFFICIENT_JOURNAL_BROKERS", - "OFFSET_NOT_YET_AVAILABLE", - "WRONG_ROUTE", - "PROPOSAL_MISMATCH", - "ETCD_TRANSACTION_FAILED", - "NOT_ALLOWED", - "WRONG_APPEND_OFFSET", - "INDEX_HAS_GREATER_OFFSET", - "REGISTER_MISMATCH" - ], - "default": "OK", - "description": "Status is a response status code, used universally across Gazette RPC APIs.\n\n - JOURNAL_NOT_FOUND: The named journal does not exist.\n - NO_JOURNAL_PRIMARY_BROKER: There is no current primary broker for the journal. This is a temporary\ncondition which should quickly resolve, assuming sufficient broker\ncapacity.\n - NOT_JOURNAL_PRIMARY_BROKER: The present broker is not the assigned primary broker for the journal.\n - NOT_JOURNAL_BROKER: The present broker is not an assigned broker for the journal.\n - INSUFFICIENT_JOURNAL_BROKERS: There are an insufficient number of assigned brokers for the journal\nto meet its required replication.\n - OFFSET_NOT_YET_AVAILABLE: The requested offset is not yet available. This indicates either that the\noffset has not yet been written, or that the broker is not yet aware of a\nwritten fragment covering the offset. Returned only by non-blocking reads.\n - WRONG_ROUTE: The peer disagrees with the Route accompanying a ReplicateRequest.\n - PROPOSAL_MISMATCH: The peer disagrees with the proposal accompanying a ReplicateRequest.\n - ETCD_TRANSACTION_FAILED: The Etcd transaction failed. Returned by Update RPC when an\nexpect_mod_revision of the UpdateRequest differs from the current\nModRevision of the JournalSpec within the store.\n - NOT_ALLOWED: A disallowed journal access was attempted (eg, a write where the\njournal disables writes, or read where journals disable reads).\n - WRONG_APPEND_OFFSET: The Append is refused because its requested offset is not equal\nto the furthest written offset of the journal.\n - INDEX_HAS_GREATER_OFFSET: The Append is refused because the replication pipeline tracks a smaller\njournal offset than that of the remote fragment index. This indicates\nthat journal replication consistency has been lost in the past, due to\ntoo many broker or Etcd failures.\n - REGISTER_MISMATCH: The Append is refused because a registers selector was provided with the\nrequest, but it was not matched by current register values of the journal." - }, - "runtimeError": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "$ref": "#/definitions/protobufAny" - } - } - } - }, - "runtimeStreamError": { - "type": "object", - "properties": { - "grpcCode": { - "type": "integer", - "format": "int32" - }, - "httpCode": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - }, - "httpStatus": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "$ref": "#/definitions/protobufAny" - } - } - } - } - } -} diff --git a/gen/consumer/protocol/protocol.pb.gw.go b/gen/consumer/protocol/protocol.pb.gw.go deleted file mode 100644 index 7c37f0b..0000000 --- a/gen/consumer/protocol/protocol.pb.gw.go +++ /dev/null @@ -1,251 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: consumer/protocol/protocol.proto - -/* -Package protocol is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package gw - -import ( - "context" - ext "go.gazette.dev/core/consumer/protocol" - "io" - "net/http" - - "github.com/golang/protobuf/descriptor" - "github.com/golang/protobuf/proto" - "github.com/grpc-ecosystem/grpc-gateway/runtime" - "github.com/grpc-ecosystem/grpc-gateway/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = descriptor.ForMessage -var _ = metadata.Join - -func request_Shard_Stat_0(ctx context.Context, marshaler runtime.Marshaler, client ext.ShardClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ext.StatRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.Stat(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_Shard_Stat_0(ctx context.Context, marshaler runtime.Marshaler, server ext.ShardServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ext.StatRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.Stat(ctx, &protoReq) - return msg, metadata, err - -} - -func request_Shard_List_0(ctx context.Context, marshaler runtime.Marshaler, client ext.ShardClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ext.ListRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.List(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_Shard_List_0(ctx context.Context, marshaler runtime.Marshaler, server ext.ShardServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq ext.ListRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.List(ctx, &protoReq) - return msg, metadata, err - -} - -// ext.RegisterShardHandlerServer registers the http handlers for service Shard to "mux". -// UnaryRPC :call ext.ShardServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterShardHandlerFromEndpoint instead. -func RegisterShardHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ext.ShardServer) error { - - mux.Handle("POST", pattern_Shard_Stat_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_Shard_Stat_0(rctx, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Shard_Stat_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_Shard_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_Shard_List_0(rctx, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Shard_List_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterShardHandlerFromEndpoint is same as RegisterShardHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterShardHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.Dial(endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterShardHandler(ctx, mux, conn) -} - -// RegisterShardHandler registers the http handlers for service Shard to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterShardHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterShardHandlerClient(ctx, mux, ext.NewShardClient(conn)) -} - -// ext.RegisterShardHandlerClient registers the http handlers for service Shard -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ShardClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ShardClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "ShardClient" to call the correct interceptors. -func RegisterShardHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ext.ShardClient) error { - - mux.Handle("POST", pattern_Shard_Stat_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_Shard_Stat_0(rctx, inboundMarshaler, client, req, pathParams) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Shard_Stat_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_Shard_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_Shard_List_0(rctx, inboundMarshaler, client, req, pathParams) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_Shard_List_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_Shard_Stat_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "shards", "stat"}, "", runtime.AssumeColonVerbOpt(true))) - - pattern_Shard_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "shards", "list"}, "", runtime.AssumeColonVerbOpt(true))) -) - -var ( - forward_Shard_Stat_0 = runtime.ForwardResponseMessage - - forward_Shard_List_0 = runtime.ForwardResponseMessage -) diff --git a/gen/consumer/protocol/protocol.swagger.json b/gen/consumer/protocol/protocol.swagger.json deleted file mode 100644 index 85578f7..0000000 --- a/gen/consumer/protocol/protocol.swagger.json +++ /dev/null @@ -1,670 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "title": "consumer/protocol/protocol.proto", - "version": "version not set" - }, - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "paths": { - "/v1/shards/list": { - "post": { - "summary": "List Shards, their ShardSpecs and their processing status.", - "operationId": "Shard_List", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/consumerListResponse" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/runtimeError" - } - } - }, - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/consumerListRequest" - } - } - ], - "tags": [ - "Shard" - ] - } - }, - "/v1/shards/stat": { - "post": { - "summary": "Stat returns detailed status of a given Shard.", - "operationId": "Shard_Stat", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/consumerStatResponse" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/runtimeError" - } - } - }, - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/consumerStatRequest" - } - } - ], - "tags": [ - "Shard" - ] - } - } - }, - "definitions": { - "GetHintsResponseResponseHints": { - "type": "object", - "properties": { - "hints": { - "$ref": "#/definitions/recoverylogFSMHints", - "description": "If the hints value does not exist Hints will be nil." - } - } - }, - "HeaderEtcd": { - "type": "object", - "properties": { - "clusterId": { - "type": "string", - "format": "uint64", - "description": "cluster_id is the ID of the cluster." - }, - "memberId": { - "type": "string", - "format": "uint64", - "description": "member_id is the ID of the member." - }, - "revision": { - "type": "string", - "format": "int64", - "description": "revision is the Etcd key-value store revision when the request was\napplied." - }, - "raftTerm": { - "type": "string", - "format": "uint64", - "description": "raft_term is the raft term when the request was applied." - } - }, - "description": "Etcd represents the effective Etcd MVCC state under which a Gazette broker\nis operating in its processing of requests and responses. Its inclusion\nallows brokers to reason about relative \"happened before\" Revision ordering\nof apparent routing conflicts in proxied or replicated requests, as well\nas enabling sanity checks over equality of Etcd ClusterId (and precluding,\nfor example, split-brain scenarios where different brokers are backed by\ndifferent Etcd clusters). Etcd is kept in sync with\netcdserverpb.ResponseHeader." - }, - "ListResponseShard": { - "type": "object", - "properties": { - "spec": { - "$ref": "#/definitions/consumerShardSpec" - }, - "modRevision": { - "type": "string", - "format": "int64", - "description": "Current ModRevision of the ShardSpec." - }, - "route": { - "$ref": "#/definitions/protocolRoute", - "description": "Route of the shard, including endpoints." - }, - "status": { - "type": "array", - "items": { - "$ref": "#/definitions/consumerReplicaStatus" - }, - "description": "Status of each replica. Cardinality and ordering matches |route|." - } - }, - "description": "Shards of the response." - }, - "ProcessSpecID": { - "type": "object", - "properties": { - "zone": { - "type": "string", - "description": "\"Zone\" in which the process is running. Zones may be AWS, Azure, or\nGoogle Cloud Platform zone identifiers, or rack locations within a colo,\nor given some other custom meaning. Gazette will replicate across\nmultiple zones, and seeks to minimize traffic which must cross zones (for\nexample, by proxying reads to a broker in the current zone)." - }, - "suffix": { - "type": "string", - "description": "Unique suffix of the process within |zone|. It is permissible for a\nsuffix value to repeat across zones, but never within zones. In practice,\nit's recommended to use a FQDN, Kubernetes Pod name, or comparable unique\nand self-describing value as the ID suffix." - } - }, - "description": "ID composes a zone and a suffix to uniquely identify a ProcessSpec." - }, - "ReplicaStatusCode": { - "type": "string", - "enum": [ - "IDLE", - "BACKFILL", - "STANDBY", - "PRIMARY", - "FAILED" - ], - "default": "IDLE", - "description": " - BACKFILL: The replica is actively playing the historical recovery log.\n - STANDBY: The replica has finished playing the historical recovery log and is\nlive-tailing it to locally mirror recorded operations as they are\nproduced. It can take over as primary at any time.\n\nShards not having recovery logs immediately transition to STANDBY.\n - PRIMARY: The replica is actively serving as primary.\n - FAILED: The replica has encountered an unrecoverable error." - }, - "consumerApplyRequestChange": { - "type": "object", - "properties": { - "expectModRevision": { - "type": "string", - "format": "int64", - "description": "Expected ModRevision of the current ShardSpec. If the shard is being\ncreated, expect_mod_revision is zero." - }, - "upsert": { - "$ref": "#/definitions/consumerShardSpec", - "description": "ShardSpec to be updated (if expect_mod_revision \u003e 0) or created\n(if expect_mod_revision == 0)." - }, - "delete": { - "type": "string", - "description": "Shard to be deleted. expect_mod_revision must not be zero." - } - }, - "description": "Change defines an insertion, update, or deletion to be applied to the set\nof ShardSpecs. Exactly one of |upsert| or |delete| must be set." - }, - "consumerApplyResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/consumerStatus", - "description": "Status of the Apply RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response." - }, - "extension": { - "type": "string", - "format": "byte", - "description": "Optional extension of the ApplyResponse." - } - } - }, - "consumerGetHintsResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/consumerStatus", - "description": "Status of the Hints RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response." - }, - "primaryHints": { - "$ref": "#/definitions/GetHintsResponseResponseHints", - "description": "Primary hints for the shard." - }, - "backupHints": { - "type": "array", - "items": { - "$ref": "#/definitions/GetHintsResponseResponseHints" - }, - "description": "List of backup hints for a shard. The most recent recovery log hints will\nbe first, any subsequent hints are for historical backup. If there is no\nvalue for a hint key the value corresponding hints will be nil." - }, - "extension": { - "type": "string", - "format": "byte", - "description": "Optional extension of the GetHintsResponse." - } - } - }, - "consumerListRequest": { - "type": "object", - "properties": { - "selector": { - "$ref": "#/definitions/protocolLabelSelector", - "description": "Selector optionally refines the set of shards which will be enumerated.\nIf zero-valued, all shards are returned. Otherwise, only ShardSpecs\nmatching the LabelSelector will be returned. One meta-label \"id\" is\nadditionally supported by the selector, where \"id=example-shard-ID\"\nwill match a ShardSpec with ID \"example-shard-ID\"." - }, - "extension": { - "type": "string", - "format": "byte", - "description": "Optional extension of the ListRequest." - } - } - }, - "consumerListResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/consumerStatus", - "description": "Status of the List RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response." - }, - "shards": { - "type": "array", - "items": { - "$ref": "#/definitions/ListResponseShard" - } - }, - "extension": { - "type": "string", - "format": "byte", - "description": "Optional extension of the ListResponse." - } - } - }, - "consumerReplicaStatus": { - "type": "object", - "properties": { - "code": { - "$ref": "#/definitions/ReplicaStatusCode" - }, - "errors": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Errors encountered during replica processing. Set iff |code| is FAILED." - } - }, - "description": "ReplicaStatus is the status of a ShardSpec assigned to a ConsumerSpec.\nIt serves as an allocator AssignmentValue. ReplicaStatus is reduced by taking\nthe maximum enum value among statuses. Eg, if a primary is PRIMARY, one\nreplica is BACKFILL and the other STANDBY, then the status is PRIMARY. If one\nof the replicas transitioned to FAILED, than the status is FAILED. This\nreduction behavior is used to summarize status across all replicas." - }, - "consumerShardSpec": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID of the shard." - }, - "sources": { - "type": "array", - "items": { - "$ref": "#/definitions/consumerShardSpecSource" - }, - "description": "Sources of the shard, uniquely ordered on Source journal." - }, - "recoveryLogPrefix": { - "type": "string", - "description": "Prefix of the Journal into which the shard's recovery log will be recorded.\nThe complete Journal name is built as \"{recovery_log_prefix}/{shard_id}\".\nIf empty, the shard does not use a recovery log." - }, - "hintPrefix": { - "type": "string", - "description": "\"{hint_prefix}/{shard_id}.primary\"\n\nThe primary will regularly produce updated hints into this key, and\nplayers of the log will similarly utilize hints from this key.\nIf |recovery_log_prefix| is set, |hint_prefix| must be also.", - "title": "Prefix of Etcd keys into which recovery log FSMHints are written to and\nread from. FSMHints allow readers of the recovery log to efficiently\ndetermine the minimum fragments of log which must be read to fully recover\nlocal store state. The complete hint key written by the shard primary is:" - }, - "hintBackups": { - "type": "integer", - "format": "int32", - "description": "\"{hints_prefix}/{shard_id}.backup.0\".\n\nIt also move hints previously stored under\n\"{hints_prefix/{shard_id}.backup.0\" to\n\"{hints_prefix/{shard_id}.backup.1\", and so on, keeping at most\n|hint_backups| distinct sets of FSMHints.\n\nIn the case of disaster or data-loss, these copied hints can be an\nimportant fallback for recovering a consistent albeit older version of the\nshard's store, with each relying on only progressively older portions of\nthe recovery log.\n\nWhen pruning the recovery log, log fragments which are older than (and no\nlonger required by) the *oldest* backup are discarded, ensuring that\nall hints remain valid for playback.", - "title": "Backups of verified recovery log FSMHints, retained as a disaster-recovery\nmechanism. On completing playback, a player will write recovered hints to:" - }, - "maxTxnDuration": { - "type": "string", - "description": "Max duration of shard transactions. This duration upper-bounds the amount\nof time during which a transaction may process messages before it must\nflush and commit. It may run for less time if an input message stall occurs\n(eg, no decoded journal message is ready without blocking). A typical value\nwould be `1s`: applications which perform extensive aggregation over\nmessage streams exhibiting locality of \"hot\" keys may benefit from larger\nvalues." - }, - "minTxnDuration": { - "type": "string", - "description": "Min duration of shard transactions. This duration lower-bounds the amount\nof time during which a transaction must process messages before it may\nflush and commit. It may run for more time if additional messages are\navailable (eg, decoded journal messages are ready without blocking). Note\nalso that transactions are pipelined: a current transaction may process\nmessages while a prior transaction's recovery log writes flush to Gazette,\nbut it cannot prepare to commit until the prior transaction writes\ncomplete. In other words even if |min_txn_quantum| is zero, some degree of\nmessage batching is expected due to the network delay inherent in Gazette\nwrites. A typical value of would be `0s`: applications which perform\nextensive aggregation may benefit from larger values." - }, - "disable": { - "type": "boolean", - "description": "Disable processing of the shard." - }, - "hotStandbys": { - "type": "integer", - "format": "int64", - "description": "Hot standbys is the desired number of consumer processes which should be\nreplicating the primary consumer's recovery log. Standbys are allocated in\na separate availability zone of the current primary, and tail the live log\nto continuously mirror the primary's on-disk DB file structure. Should the\nprimary experience failure, one of the hot standbys will be assigned to\ntake over as the new shard primary, which is accomplished by simply opening\nits local copy of the recovered store files.\n\nNote that under regular operation, shard hand-off is zero downtime even if\nstandbys are zero, as the current primary will not cede ownership until the\nreplacement process declares itself ready. However, without standbys a\nprocess failure will leave the shard without an active primary while its\nreplacement starts and completes playback of its recovery log." - }, - "labels": { - "$ref": "#/definitions/protocolLabelSet", - "description": "User-defined Labels of this ShardSpec. The label \"id\" is reserved and may\nnot be used with a ShardSpec's labels." - }, - "disableWaitForAck": { - "type": "boolean", - "description": "Disable waiting for acknowledgements of pending message(s).\n\nIf a consumer transaction reads uncommitted messages, it will by default\nremain open (subject to the max duration) awaiting an acknowledgement of\nthose messages, in the hope that that acknowledgement will be quickly\nforthcoming and, by remaining open, we can process all messages in this\ntransaction. Effectively we're trading a small amount of increased local\nlatency for a global reduction in end-to-end latency.\n\nThis works well for acyclic message flows, but can introduce unnecessary\nstalls if there are message cycles between shards. In the simplest case,\na transaction could block awaiting an ACK of a message that it itself\nproduced -- an ACK which can't arrive until the transaction closes." - }, - "ringBufferSize": { - "type": "integer", - "format": "int64", - "description": "Size of the ring buffer used to sequence read-uncommitted messages\ninto consumed, read-committed ones. The ring buffer is a performance\noptimization only: applications will replay portions of journals as\nneeded when messages aren't available in the buffer.\nIt can remain small if source journal transactions are small,\nbut larger transactions will achieve better performance with a\nlarger ring.\nIf zero, a reasonable default (currently 8192) is used." - }, - "readChannelSize": { - "type": "integer", - "format": "int64", - "description": "Size of the channel used to bridge message read and decode with\nsequencing and consumption. Larger values may reduce data stalls,\nparticularly for larger transactions and/or bursty custom\nMessageProducer implementations.\nIf zero, a reasonable default (currently 8192) is used." - } - }, - "description": "ShardSpec describes a shard and its configuration, and is the long-lived unit\nof work and scaling for a consumer application. Each shard is allocated to a\none \"primary\" at-a-time selected from the current processes of a consumer\napplication, and is re-assigned on process fault or exit.\n\nShardSpecs describe all configuration of the shard and its processing,\nincluding journals to consume, configuration for processing transactions, its\nrecovery log, hot standbys, etc. ShardSpecs may be further extended with\ndomain-specific labels \u0026 values to further define application behavior.\nShardSpec is-a allocator.ItemValue." - }, - "consumerShardSpecSource": { - "type": "object", - "properties": { - "journal": { - "type": "string", - "description": "Journal which this shard is consuming." - }, - "minOffset": { - "type": "string", - "format": "int64", - "description": "Minimum journal byte offset the shard should begin reading from.\nTypically this should be zero, as read offsets are check-pointed and\nrestored from the shard's Store as it processes. |min_offset| can be\nuseful for shard initialization, directing it to skip over historical\nportions of the journal not needed for the application's use case." - } - }, - "description": "Sources define the set of journals which this shard consumes. At least one\nSource must be specified, and in many use cases only one will be needed.\nFor use cases which can benefit, multiple sources may be specified to\nrepresent a \"join\" over messages of distinct journals.\n\nNote the effective mapping of messages to each of the joined journals\nshould align (eg, joining a journal of customer updates with one of orders,\nwhere both are mapped on customer ID). This typically means the\npartitioning of the two event \"topics\" must be the same.\n\nAnother powerful pattern is to shard on partitions of a high-volume event\nstream, and also have each shard join against all events of a low-volume\nstream. For example, a shard might ingest and index \"viewed product\"\nevents, read a comparably low-volume \"purchase\" event stream, and on each\npurchase publish the bundle of its corresponding prior product views." - }, - "consumerStatRequest": { - "type": "object", - "properties": { - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header may be attached by a proxying consumer peer." - }, - "shard": { - "type": "string", - "description": "Shard to Stat." - }, - "readThrough": { - "type": "object", - "additionalProperties": { - "type": "string", - "format": "int64" - }, - "description": "Journals and offsets which must be reflected in a completed consumer\ntransaction before Stat returns, blocking if required. Offsets of journals\nnot read by this shard are ignored." - }, - "extension": { - "type": "string", - "format": "byte", - "description": "Optional extension of the StatRequest." - } - } - }, - "consumerStatResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/consumerStatus", - "description": "Status of the Stat RPC." - }, - "header": { - "$ref": "#/definitions/protocolHeader", - "description": "Header of the response." - }, - "readThrough": { - "type": "object", - "additionalProperties": { - "type": "string", - "format": "int64" - }, - "description": "Journals and offsets read through by the most recent completed consumer\ntransaction." - }, - "publishAt": { - "type": "object", - "additionalProperties": { - "type": "string", - "format": "int64" - }, - "description": "Journals and offsets this shard has published through, including\nacknowledgements, as-of the most recent completed consumer transaction.\n\nFormally, if an acknowledged message A results in this shard publishing\nmessages B, and A falls within |read_through|, then all messages B \u0026 their\nacknowledgements fall within |publish_at|.\n\nThe composition of |read_through| and |publish_at| allow CQRS applications\nto provide read-your-writes consistency, even if written events pass\nthrough multiple intermediate consumers and arbitrary transformations\nbefore arriving at the materialized view which is ultimately queried." - }, - "extension": { - "type": "string", - "format": "byte", - "description": "Optional extension of the StatResponse." - } - } - }, - "consumerStatus": { - "type": "string", - "enum": [ - "OK", - "SHARD_NOT_FOUND", - "NO_SHARD_PRIMARY", - "NOT_SHARD_PRIMARY", - "ETCD_TRANSACTION_FAILED", - "SHARD_STOPPED" - ], - "default": "OK", - "description": "Status is a response status code, used across Gazette Consumer RPC APIs.\n\n - SHARD_NOT_FOUND: The named shard does not exist.\n - NO_SHARD_PRIMARY: There is no current primary consumer process for the shard. This is a\ntemporary condition which should quickly resolve, assuming sufficient\nconsumer capacity.\n - NOT_SHARD_PRIMARY: The present consumer process is not the assigned primary for the shard,\nand was not instructed to proxy the request.\n - ETCD_TRANSACTION_FAILED: The Etcd transaction failed. Returned by Update RPC when an\nexpect_mod_revision of the UpdateRequest differs from the current\nModRevision of the ShardSpec within the store.\n - SHARD_STOPPED: The current primary shard has stopped, either due to reassignment or\nprocessing failure, and will not make further progress toward the\nrequested operation.\nFor example, a Stat RPC will return SHARD_STOPPED if the StatRequest\ncannot be satisfied." - }, - "consumerUnassignResponse": { - "type": "object", - "properties": { - "status": { - "$ref": "#/definitions/consumerStatus", - "description": "Status of the Unassign RPC." - }, - "shards": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Shards which had assignments removed." - } - } - }, - "protobufAny": { - "type": "object", - "properties": { - "typeUrl": { - "type": "string" - }, - "value": { - "type": "string", - "format": "byte" - } - } - }, - "protocolHeader": { - "type": "object", - "properties": { - "processId": { - "$ref": "#/definitions/ProcessSpecID", - "description": "ID of the process responsible for request processing. May be empty iff\nHeader is being used within a proxied request, and that request may be\ndispatched to any member of the Route." - }, - "route": { - "$ref": "#/definitions/protocolRoute", - "description": "Route of processes specifically responsible for this RPC, or an empty Route\nif any process is capable of serving the RPC." - }, - "etcd": { - "$ref": "#/definitions/HeaderEtcd" - } - }, - "description": "Header captures metadata such as the process responsible for processing\nan RPC, and its effective Etcd state." - }, - "protocolLabel": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "description": "Label defines a key \u0026 value pair which can be attached to entities like\nJournalSpecs and BrokerSpecs. Labels may be used to provide identifying\nattributes which do not directly imply semantics to the core system, but\nare meaningful to users or for higher-level Gazette tools." - }, - "protocolLabelSelector": { - "type": "object", - "properties": { - "include": { - "$ref": "#/definitions/protocolLabelSet", - "description": "Include is Labels which must be matched for a LabelSet to be selected. If\nempty, all Labels are included. An include Label with empty (\"\") value is\nmatched by a Label of the same name having any value." - }, - "exclude": { - "$ref": "#/definitions/protocolLabelSet", - "description": "Exclude is Labels which cannot be matched for a LabelSet to be selected. If\nempty, no Labels are excluded. An exclude Label with empty (\"\") value\nexcludes a Label of the same name having any value." - } - }, - "description": "LabelSelector defines a filter over LabelSets." - }, - "protocolLabelSet": { - "type": "object", - "properties": { - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/protocolLabel" - }, - "description": "Labels of the set. Instances must be unique and sorted over (Name, Value)." - } - }, - "description": "LabelSet is a collection of labels and their values." - }, - "protocolRoute": { - "type": "object", - "properties": { - "members": { - "type": "array", - "items": { - "$ref": "#/definitions/ProcessSpecID" - }, - "description": "Members of the Route, ordered on ascending ProcessSpec.ID (zone, suffix)." - }, - "primary": { - "type": "integer", - "format": "int32", - "description": "Index of the ProcessSpec serving as primary within |members|,\nor -1 of no member is currently primary." - }, - "endpoints": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Endpoints of each Route member. If not empty, |endpoints| has the same\nlength and order as |members|, and captures the endpoint of each one." - } - }, - "description": "Route captures the current topology of an item and the processes serving it." - }, - "recoverylogFSMHints": { - "type": "object", - "properties": { - "log": { - "type": "string", - "description": "Log is the implied recovery log of any contained |live_nodes| Segments\nwhich omit a |log| value. This implied behavior is both for backward-\ncompatibility (Segments didn't always have a |log| field) and also for\ncompacting the representation in the common case of Segments mostly or\nentirely addressing a single log." - }, - "liveNodes": { - "type": "array", - "items": { - "$ref": "#/definitions/recoverylogFnodeSegments" - }, - "description": "Live Fnodes and their Segments as-of the generation of these FSMHints." - }, - "properties": { - "type": "array", - "items": { - "$ref": "#/definitions/recoverylogProperty" - }, - "description": "Property files and contents as-of the generation of these FSMHints." - } - }, - "description": "FSMHints represents a manifest of Fnodes which were still live (eg, having\nremaining links) at the time the FSMHints were produced, as well as any\nProperties. It allows a Player of the log to identify minimal Segments which\nmust be read to recover all Fnodes, and also contains sufficient metadata for\na Player to resolve all possible conflicts it could encounter while reading\nthe log, to arrive at a consistent view of file state which exactly matches\nthat of the Recorder producing the FSMHints.\nNext tag: 4." - }, - "recoverylogFnodeSegments": { - "type": "object", - "properties": { - "fnode": { - "type": "string", - "format": "int64", - "description": "Fnode being hinted." - }, - "segments": { - "type": "array", - "items": { - "$ref": "#/definitions/recoverylogSegment" - }, - "description": "Segments of the Fnode in the log. Currently, FSM tracks only a single\nSegment per Fnode per Author \u0026 Log. A specific implication of this is that Fnodes\nmodified over long periods of time will result in Segments spanning large\nchunks of the log. For best performance, Fnodes should be opened \u0026 written\nonce, and then never be modified again (this is RocksDB's behavior).\nIf supporting this case is desired, FSM will have to be a bit smarter about\nnot extending Segments which gap over significant portions of the log\n(eg, there's a trade-off to make over size of the hinted manifest, vs\nsavings incurred on playback by being able to skip portions of the log)." - } - }, - "description": "FnodeSegments captures log Segments containing all RecordedOps of the Fnode." - }, - "recoverylogProperty": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Filesystem path of this property, relative to the common base directory." - }, - "content": { - "type": "string", - "description": "Complete file content of this property." - } - }, - "description": "Property is a small file which rarely changes, and is thus managed\noutside of regular Fnode tracking. See FSM.Properties." - }, - "recoverylogSegment": { - "type": "object", - "properties": { - "author": { - "type": "integer", - "format": "int64", - "description": "Author which wrote RecordedOps of this Segment." - }, - "firstSeqNo": { - "type": "string", - "format": "int64", - "description": "First (lowest) sequence number of RecordedOps within this Segment." - }, - "firstOffset": { - "type": "string", - "format": "int64", - "description": "First byte offset of the Segment, where |first_seq_no| is recorded.\nIf this Segment was produced by a Recorder, this is guaranteed only to be a\nlower-bound (eg, a Player reading at this offset may encounter irrelevant\noperations prior to the RecordedOp indicated by the tuple\n(|author|, |first_seq_no|, |first_checksum|). If a Player produced the Segment,\nfirst_offset is exact." - }, - "firstChecksum": { - "type": "integer", - "format": "int64", - "description": "Checksum of the RecordedOp having |first_seq_no|." - }, - "lastSeqNo": { - "type": "string", - "format": "int64", - "description": "Last (highest, inclusive) sequence number of RecordedOps within this Segment." - }, - "lastOffset": { - "type": "string", - "format": "int64", - "description": "Last offset (exclusive) of the Segment. Zero means the offset is not known\n(eg, because the Segment was produced by a Recorder)." - }, - "log": { - "type": "string", - "description": "Log is the Journal holding this Segment's data, and to which offsets are relative." - } - }, - "description": "Segment is a contiguous chunk of recovery log written by a single Author.\nRecorders track Segments they have written, for use in providing hints to\nfuture readers of the log. A key point to understand is that Gazette append\nsemantics mean that Recorders *cannot know* exactly what offsets their writes\nare applied to in the log, nor guarantee that their operations are not being\ninterleaved with those of other writers. Log Players are aware of these\nlimitations, and use Segments to resolve conflicts of possible interpretation\nof the log. Segments produced by a Player are exact, since Players observe all\nrecorded operations at their exact offsets.\nNext tag: 8." - }, - "runtimeError": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "$ref": "#/definitions/protobufAny" - } - } - } - } - } -} diff --git a/go.mod b/go.mod index aa05073..127de2b 100644 --- a/go.mod +++ b/go.mod @@ -3,48 +3,52 @@ module github.com/estuary/data-plane-gateway go 1.22.2 require ( - github.com/estuary/flow v0.1.9-0.20230303181027-f65a9d7f1a89 + github.com/estuary/flow v0.5.8-0.20241208213014-fa3e7350a837 github.com/gogo/gateway v1.1.0 - github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/golang/protobuf v1.5.3 - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 - github.com/hashicorp/golang-lru/v2 v2.0.1 - github.com/jamiealquiza/envy v1.1.0 - github.com/prometheus/client_golang v1.14.0 + github.com/jessevdk/go-flags v1.5.0 github.com/sirupsen/logrus v1.9.0 - github.com/urfave/negroni v1.0.0 - github.com/valyala/fasthttp v1.48.0 - go.gazette.dev/core v0.89.1-0.20231012132739-dfed675b7fd1 - golang.org/x/net v0.24.0 - golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f - google.golang.org/grpc v1.56.3 + go.gazette.dev/core v0.100.0 + google.golang.org/grpc v1.65.0 ) require ( github.com/DataDog/zstd v1.4.8 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/klauspost/compress v1.16.3 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/afero v1.6.0 // indirect - github.com/spf13/cobra v1.6.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - go.etcd.io/etcd/client/v3 v3.5.4 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/protobuf v1.30.0 // indirect + go.etcd.io/etcd/api/v3 v3.5.17 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect + go.etcd.io/etcd/client/v3 v3.5.17 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.7.0 // indirect + go.uber.org/zap v1.19.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/protobuf v1.34.1 // indirect ) diff --git a/go.sum b/go.sum index 25e4253..084c4a4 100644 --- a/go.sum +++ b/go.sum @@ -13,22 +13,21 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV 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.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= +cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= 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/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 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/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y= +cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= 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= @@ -38,23 +37,23 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo 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.18.2 h1:5NQw6tOn3eMm0oE8vTkfjau18kjL79FlMjy/CHTpmoY= -cloud.google.com/go/storage v1.18.2/go.mod h1:AiIj7BWXyhO5gGVmYJ+S8tbkCx3yb0IMjua8Aw4naVM= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1 h1:SEy2xmstIphdPwNBUi7uhvjyjhVKISfwjfOJmuy7kg4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk= github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 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/DataDog/zstd v1.4.8 h1:Rpmta4xZ/MgZnriKNd24iZMhGpP5dvUcs/uqfBapKZY= @@ -64,52 +63,42 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go v1.40.35 h1:ofWh1LlWaSbOpAsl8EHlg96PZXqgCGKKi8YgrdU2Z+I= github.com/aws/aws-sdk-go v1.40.35/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y= +github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY= +github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= 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.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= -github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/estuary/flow v0.1.9-0.20230303181027-f65a9d7f1a89 h1:qf9Pciyb/k63u9aZLmeBqp6zBP/BZdRQ1fYMeu2IaTs= -github.com/estuary/flow v0.1.9-0.20230303181027-f65a9d7f1a89/go.mod h1:BMnABnmpvpgAVWt8lNlw7LBnwuyLmTd0asVzUFLsKi8= +github.com/estuary/flow v0.5.8-0.20241208213014-fa3e7350a837 h1:ulU8y7ILJwSzvc2Oj7+pUT3R42WK6I8mmic3CQPJJfA= +github.com/estuary/flow v0.5.8-0.20241208213014-fa3e7350a837/go.mod h1:fRYaE62AKCeBcthEs+M+Ofc9L7AQdRaNKjES3+tFziM= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -132,13 +121,14 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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 h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -162,8 +152,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw 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/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -177,8 +167,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -190,15 +180,19 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= -github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= +github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -208,14 +202,12 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= -github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jamiealquiza/envy v1.1.0 h1:Nwh4wqTZ28gDA8zB+wFkhnUpz3CEcO12zotjeqqRoKE= -github.com/jamiealquiza/envy v1.1.0/go.mod h1:MP36BriGCLwEHhi1OU8E9569JNZrjWfCvzG7RsPnHus= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -236,8 +228,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -246,6 +238,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -256,8 +250,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -269,7 +263,6 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= @@ -277,8 +270,8 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1: github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= @@ -295,9 +288,8 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -307,52 +299,41 @@ github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -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.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.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc= -github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= -go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= -go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= -go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.gazette.dev/core v0.89.1-0.20231012132739-dfed675b7fd1 h1:oRtrsDKboiCGeG0B6kJvaJ9KVAn7U797oGMGTqdHnwo= -go.gazette.dev/core v0.89.1-0.20231012132739-dfed675b7fd1/go.mod h1:/fdxqReMWKS26yROKEKDY8JlXjwz8TqeEZggpNwb5I0= +go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= +go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4= +go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw= +go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w= +go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY= +go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo= +go.gazette.dev/core v0.100.0 h1:lOSwhWIuZhYZ+br/4LN/osEfP6yQ+J1qM8pxuXmoD0I= +go.gazette.dev/core v0.100.0/go.mod h1:1rj+daAL/cy+dt9XZGzsLBJl5BeLkeCiRmlcHaSCH/I= 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.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -362,8 +343,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 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= @@ -385,8 +366,8 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl 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 h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -395,7 +376,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB 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.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -426,12 +406,12 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R 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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 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= @@ -439,8 +419,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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= @@ -451,9 +431,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -489,17 +468,15 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w 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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -507,11 +484,10 @@ 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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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= @@ -530,6 +506,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw 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-20191108193012-7d206e10da11/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= @@ -557,12 +534,14 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc 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-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 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= @@ -579,8 +558,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M 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.60.0 h1:eq/zs5WPH4J9undYM9IP1O7dSr7Yh8Y0GtSCpzGzIUk= -google.golang.org/api v0.60.0/go.mod h1:d7rl65NZAkEQ90JFzqBjcRq1TVeG5ZoGV3sSpEnnVb4= +google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= +google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= 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= @@ -619,9 +598,12 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc 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-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= +google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -636,9 +618,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji 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.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 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= @@ -651,8 +632,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj 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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -685,4 +666,3 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 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= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/grpc.go b/grpc.go deleted file mode 100644 index 699fe2a..0000000 --- a/grpc.go +++ /dev/null @@ -1,147 +0,0 @@ -package main - -import ( - context "context" - "fmt" - "io" - "net" - "net/url" - "strings" - "time" - - log "github.com/sirupsen/logrus" - "golang.org/x/sync/errgroup" - - "google.golang.org/grpc" - "google.golang.org/grpc/grpclog" -) - -func logUnaryRPC(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - var start = time.Now().UTC() - // "method" is the gRPC term, which actually means "path" in HTTP terms. - log.WithField("method", method).Trace("starting unary RPC") - var err = invoker(ctx, method, req, reply, cc, opts...) - log.WithFields(log.Fields{ - "method": method, - "timeMillis": time.Now().UTC().Sub(start).Milliseconds(), - "error": err, - }).Debug("finished gRPC client request") - return err -} - -func dialAddress(ctx context.Context, addr string) (*grpc.ClientConn, error) { - dialCtx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - - var logger = grpc.UnaryClientInterceptor(grpc.UnaryClientInterceptor(logUnaryRPC)) - var dialAddr string - opts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithBlock(), grpc.WithChainUnaryInterceptor(logger)} - if strings.HasPrefix(addr, "unix://") { - parsedUrl, err := url.Parse(addr) - if err != nil { - return nil, err - } - dialAddr = parsedUrl.Path - opts = append(opts, grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { - return net.DialTimeout("unix", addr, timeout) - })) - } else { - dialAddr = addr - } - - conn, err := grpc.DialContext(dialCtx, dialAddr, opts...) - if err != nil { - return nil, fmt.Errorf("failed to dial `%v`: %w", dialAddr, err) - } - log.Printf("[dialAddress] dial successful. addr = %s", addr) - - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", addr, cerr) - } - }() - - return conn, err -} - -// / This is a bit reversed from normal operations. We're forwarding messages -// / from the local grpc server to a remote server. Sends messages received by -// / the server to the client and sends responses sent by the client to the -// / server. -func proxyStream(ctx context.Context, streamDesc string, source grpc.ServerStream, destination grpc.ClientStream, req interface{}, resp interface{}) error { - eg, ctx := errgroup.WithContext(ctx) - log.WithField("stream", streamDesc).Trace("starting streaming proxy") - var startTime = time.Now().UTC() - - eg.Go(func() (_err error) { - var msgCount = 0 - defer func() { - log.WithFields(log.Fields{ - "stream": streamDesc, - "error": _err, - "msgCount": msgCount, - }).Trace("finished forwarding messages from client to server") - }() - for { - select { - case <-ctx.Done(): - return nil - default: - // Keep going - } - err := source.RecvMsg(req) - if err == io.EOF { - return nil - } else if err != nil { - return err - } - msgCount++ - err = destination.SendMsg(req) - if err == io.EOF { - return nil - } else if err != nil { - return err - } - } - }) - eg.Go(func() (_err error) { - var msgCount = 0 - defer func() { - log.WithFields(log.Fields{ - "stream": streamDesc, - "error": _err, - "msgCount": msgCount, - }).Trace("finished forwarding messages from server to client") - }() - for { - select { - case <-ctx.Done(): - return nil - default: - // Keep going - } - err := destination.RecvMsg(resp) - if err == io.EOF { - return nil - } else if err != nil { - return err - } - msgCount++ - err = source.SendMsg(resp) - if err == io.EOF { - return nil - } else if err != nil { - return err - } - } - }) - - var err = eg.Wait() - log.WithFields(log.Fields{ - "stream": streamDesc, - "error": err, - "timeMillis": time.Now().UTC().Sub(startTime).Milliseconds(), - }).Debug("finished proxying streaming RPC") - return err -} diff --git a/journal_server.go b/journal_server.go index 62a1e9f..25ef8e6 100644 --- a/journal_server.go +++ b/journal_server.go @@ -1,169 +1,119 @@ package main import ( - "bytes" - context "context" + "context" "fmt" - "slices" + "io" + "time" - "github.com/estuary/data-plane-gateway/auth" - "github.com/estuary/flow/go/labels" - log "github.com/sirupsen/logrus" pb "go.gazette.dev/core/broker/protocol" + "google.golang.org/grpc/metadata" ) -type JournalAuthServer struct { - clientCtx context.Context - journalClient pb.JournalClient - jwtVerificationKey []byte -} +type PassThroughAuthorizer struct{} -func NewJournalAuthServer(ctx context.Context, jwtVerificationKey []byte) *JournalAuthServer { - journalClient, err := newJournalClient(ctx, *brokerAddr) - if err != nil { - log.Fatalf("Failed to connect to broker: %v", err) - } +func (PassThroughAuthorizer) Authorize(ctx context.Context, claims pb.Claims, exp time.Duration) (context.Context, error) { + var md, _ = metadata.FromIncomingContext(ctx) + var token = md.Get("authorization") - authServer := &JournalAuthServer{ - clientCtx: ctx, - journalClient: journalClient, - jwtVerificationKey: jwtVerificationKey, + if len(token) == 0 { + return nil, fmt.Errorf("missing required Authorization header") } - - return authServer + return metadata.AppendToOutgoingContext(ctx, "authorization", token[0]), nil } -func newJournalClient(ctx context.Context, addr string) (pb.JournalClient, error) { - log.Printf("connecting journal client to: %s", addr) - conn, err := dialAddress(ctx, addr) - if err != nil { - return nil, fmt.Errorf("error connecting to server: %w", err) - } - - return pb.NewJournalClient(conn), nil +type JournalProxy struct { + jc pb.JournalClient } -// List implements protocol.JournalServer -func (s *JournalAuthServer) List(ctx context.Context, req *pb.ListRequest) (*pb.ListResponse, error) { - claims, err := auth.AuthenticateGrpcReq(ctx, s.jwtVerificationKey) - if err != nil { - return nil, err - } - ctx = pb.WithDispatchDefault(ctx) +func (s *JournalProxy) List(req *pb.ListRequest, stream pb.Journal_ListServer) error { + var ctx = pb.WithDispatchDefault(stream.Context()) - // Is the user listing (only) ops collections? - var requested = req.Selector.Include.ValuesOf(labels.Collection) - var isOpsListing = len(requested) != 0 - for _, r := range requested { - isOpsListing = isOpsListing && slices.Contains(allOpsCollections, r) + var proxy, err = s.jc.List(ctx, req) + if err != nil { + return err } - // Special-case listings of ops collections. - // We list all journals, and then filter to those that the user may access. - if isOpsListing { - var resp, err = s.journalClient.List(ctx, req) - if err != nil { - return nil, err - } - - // Filter journals to those the user has access to. - var filtered []pb.ListResponse_Journal - for _, j := range resp.Journals { - if isAllowedOpsJournal(claims, j.Spec.Name) { - filtered = append(filtered, j) + for { + if resp, err := proxy.Recv(); err != nil { + if err == io.EOF { + err = nil // Graceful close. } + return err + } else if err = stream.Send(resp); err != nil { + return err } - resp.Journals = filtered - return resp, nil - } - - err = auth.EnforceSelectorPrefix(claims, req.Selector) - if err != nil { - return nil, fmt.Errorf("Unauthorized: %w", err) } +} - return s.journalClient.List(ctx, req) +func (s *JournalProxy) ListFragments(ctx context.Context, req *pb.FragmentsRequest) (*pb.FragmentsResponse, error) { + return s.jc.ListFragments(pb.WithDispatchDefault(ctx), req) } -// ListFragments implements protocol.JournalServer -func (s *JournalAuthServer) ListFragments(ctx context.Context, req *pb.FragmentsRequest) (*pb.FragmentsResponse, error) { - claims, err := auth.AuthenticateGrpcReq(ctx, s.jwtVerificationKey) +func (s *JournalProxy) Read(req *pb.ReadRequest, stream pb.Journal_ReadServer) error { + var ctx = stream.Context() + + var proxy, err = s.jc.Read(pb.WithDispatchDefault(ctx), req) if err != nil { - return nil, err + return err } - err = auth.EnforcePrefix(claims, req.Journal.String()) - if err != nil { - if !isAllowedOpsJournal(claims, req.Journal) { - return nil, fmt.Errorf("Unauthorized: %w", err) + for { + if resp, err := proxy.Recv(); err != nil { + if err == io.EOF { + err = nil // Graceful close. + } + return err + } else if err = stream.Send(resp); err != nil { + return err } } - - return s.journalClient.ListFragments(ctx, req) } -// Read implements protocol.JournalServer -func (s *JournalAuthServer) Read(req *pb.ReadRequest, readServer pb.Journal_ReadServer) error { - ctx := readServer.Context() +func (s *JournalProxy) Append(stream pb.Journal_AppendServer) error { + var ctx = stream.Context() - claims, err := auth.AuthenticateGrpcReq(ctx, s.jwtVerificationKey) + var req, err = stream.Recv() if err != nil { return err } - err = auth.EnforcePrefix(claims, req.Journal.String()) - if err != nil { - if !isAllowedOpsJournal(claims, req.Journal) { - return fmt.Errorf("Unauthorized: %w", err) - } - } + ctx = pb.WithClaims(ctx, pb.Claims{ + Capability: pb.Capability_APPEND, + Selector: pb.LabelSelector{ + Include: pb.MustLabelSet("name", req.Journal.String()), + }, + }) + ctx = pb.WithDispatchDefault(ctx) - readClient, err := s.journalClient.Read(ctx, req) + proxy, err := s.jc.Append(ctx) if err != nil { return err } - return proxyStream(ctx, "/protocol.Journal/Read", readServer, readClient, new(pb.ReadRequest), new(pb.ReadResponse)) -} + for { + if err = proxy.Send(req); err != nil { + return err + } else if req, err = stream.Recv(); err == io.EOF { + break + } else if err != nil { + return err + } + } + resp, err := proxy.CloseAndRecv() -// We're currently only implementing the read-only RPCs for protocol.JournalServer. -func (s *JournalAuthServer) Append(pb.Journal_AppendServer) error { - return fmt.Errorf("Unsupported operation: `Append`") -} -func (s *JournalAuthServer) Apply(context.Context, *pb.ApplyRequest) (*pb.ApplyResponse, error) { - return nil, fmt.Errorf("Unsupported operation: `Apply`") -} -func (s *JournalAuthServer) Replicate(pb.Journal_ReplicateServer) error { - return fmt.Errorf("Unsupported operation: `Replicate`") + if err == nil { + err = stream.SendAndClose(resp) + } + return err } -// TODO(johnny): This authorization check is an encapsulated hack that allows -// ops logs and stats to be read-able by end users. -// It's a placeholder for a missing partition-level authorization feature. -func isAllowedOpsJournal(claims *auth.AuthorizedClaims, journal pb.Journal) bool { - var b = make([]byte, 256) - - for _, oc := range allOpsCollections { - for _, kind := range []string{"capture", "derivation", "materialization"} { - for _, prefix := range claims.Prefixes { - b = append(b[:0], oc...) - b = append(b, "/kind="...) - b = append(b, kind...) - b = append(b, "/name="...) - b = labels.EncodePartitionValue(b, prefix) - - if bytes.HasPrefix([]byte(journal), b) { - return true - } - } - } - } - return false +func (s *JournalProxy) Apply(ctx context.Context, req *pb.ApplyRequest) (*pb.ApplyResponse, error) { + return s.jc.Apply(pb.WithDispatchDefault(ctx), req) } -var allOpsCollections = []string{ - "ops.us-central1.v1/logs", - "ops.us-central1.v1/stats", +func (s *JournalProxy) Replicate(pb.Journal_ReplicateServer) error { + return fmt.Errorf("unsupported operation: `Replicate`") } -var _ pb.JournalServer = &JournalAuthServer{} +var _ pb.JournalServer = &JournalProxy{} diff --git a/main.go b/main.go index 39b8e7f..84eeea6 100644 --- a/main.go +++ b/main.go @@ -3,129 +3,165 @@ package main import ( context "context" "crypto/tls" - "flag" - "fmt" - "net/http" - "net/http/pprof" - "net/url" "os" - "strconv" - "strings" + "os/signal" + "syscall" - "github.com/estuary/data-plane-gateway/proxy" - grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" - "github.com/jamiealquiza/envy" - "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/estuary/flow/go/network" + pf "github.com/estuary/flow/go/protocols/flow" + "github.com/gogo/gateway" + "github.com/jessevdk/go-flags" log "github.com/sirupsen/logrus" + "go.gazette.dev/core/auth" pb "go.gazette.dev/core/broker/protocol" pc "go.gazette.dev/core/consumer/protocol" + mbp "go.gazette.dev/core/mainboilerplate" + "go.gazette.dev/core/server" "go.gazette.dev/core/task" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" - "google.golang.org/grpc" -) -var ( - logLevel = flag.String("log.level", "info", "Verbosity of logging") - brokerAddr = flag.String("broker-address", "localhost:8080", "Target broker address") - consumerAddr = flag.String("consumer-address", "localhost:9000", "Target consumer address") - inferenceAddr = flag.String("inference-address", "localhost:9090", "Target schema inference service address") - corsOrigin = flag.String("cors-origin", "*", "CORS Origin") - controlPlaneAuthUrl = flag.String("control-plane-auth-url", "", "base url to use for redirecting unauthorized requests") - jwtVerificationKey = flag.String("verification-key", "supersecret", "Key used to verify JWTs signed by the Flow Control Plane") - // Plain port is meant to be exposed to the public internet. It serves the REST endpoints, so that - // it's usable for local development without shenanigans for dealing with the self-signed cert. - // It also serves the ACME challenges for provisioning TLS certs. It does not serve gRPC. - plainPort = flag.String("plain-port", "28317", "Port for unencrypted communication") - // TLS port serves the REST endpoints and gRPC. The bread and butter, if you will. - tlsPort = flag.String("port", "28318", "Service port for HTTPS and gRPC requests. Port may also take the form 'unix:///path/to/socket' to use a Unix Domain Socket") - // We listen on 3 separate ports because the "plain-port" needs to be exposed to the public internet, and we - // don't want to serve metrics or debug endpoints to just anyone. This port serves metrics and debug - // endpoints only. It is not intended to ever be exposed to the public internet. - debugPort = flag.String("debug-port", "28316", "Port for serving metrics and debug endpoints") - zone = flag.String("zone", "local", "Availability zone within which this process is running") - - hostname = flag.String("hostname", "localhost", "The hostname that clients use to connect to the gateway") - - // Args for providing the tls certificate the old fashioned way - tls_cert = flag.String("tls-certificate", "", "Path to the TLS certificate (.crt) to use.") - tls_private_key = flag.String("tls-private-key", "", "The private key for the TLS certificate") + "github.com/grpc-ecosystem/grpc-gateway/runtime" ) -var corsConfig *corsSettings +// Config is the top-level configuration object of data-plane-gateway. +var Config = new(struct { + Broker struct { + mbp.AddressConfig + } `group:"Broker" namespace:"broker" env-namespace:"BROKER"` -func main() { - flag.Parse() - envy.Parse("GATEWAY") - var lvl, err = log.ParseLevel(*logLevel) - if err != nil { - panic(fmt.Sprintf("failed to parse log level: '%s', %v", *logLevel, err)) - } - log.SetLevel(lvl) - log.SetFormatter(&log.JSONFormatter{}) + Consumer struct { + mbp.AddressConfig + } `group:"Consumer" namespace:"consumer" env-namespace:"CONSUMER"` - grpc.EnableTracing = true - grpc_prometheus.EnableHandlingTimeHistogram() - grpc_prometheus.EnableClientHandlingTimeHistogram() + Gateway struct { + mbp.ServiceConfig + } `group:"Gateway" namespace:"gateway" env-namespace:"GATEWAY"` - if *tls_cert == "" { - log.Fatal("TLS is required in order to run data-plane-gateway. Missing TLS arguments") - } - if *tls_private_key == "" { - log.Fatal("must supply --tls-private-key with --tls-certificate") - } + Flow struct { + ControlAPI pb.Endpoint `long:"control-api" env:"CONTROL_API" description:"Address of the control-plane API"` + Dashboard pb.Endpoint `long:"dashboard" env:"DASHBOARD" description:"Address of the Estuary dashboard"` + DataPlaneFQDN string `long:"data-plane-fqdn" env:"DATA_PLANE_FQDN" description:"Fully-qualified domain name of the data-plane to which this reactor belongs"` + } `group:"flow" namespace:"flow" env-namespace:"FLOW"` - crt, err := tls.LoadX509KeyPair(*tls_cert, *tls_private_key) - if err != nil { - log.Fatalf("failed to load tls certificate: %v", err) - } - log.Info("loaded tls certificate") - var certificates = []tls.Certificate{crt} + Log mbp.LogConfig `group:"Logging" namespace:"log" env-namespace:"LOG"` + Diagnostics mbp.DiagnosticsConfig `group:"Debug" namespace:"debug" env-namespace:"DEBUG"` +}) - tlsPortNum, err := strconv.ParseUint(*tlsPort, 10, 16) - if err != nil { - log.Fatalf("invalid tls port number: '%s': %w", *tlsPort, err) - } +const iniFilename = "data-plane-gateway.ini" + +type cmdServe struct{} + +func (cmdServe) Execute(args []string) error { + defer mbp.InitDiagnosticsAndRecover(Config.Diagnostics)() + mbp.InitLog(Config.Log) - // TODO: this sets a global that's used in rest.go :( Can we fix that? - corsConfig = NewCorsSettings(*corsOrigin) + log.WithFields(log.Fields{ + "config": Config, + "version": mbp.Version, + "buildDate": mbp.BuildDate, + }).Info("data-plane-gateway configuration") + pb.RegisterGRPCDispatcher(Config.Gateway.Zone) + + var shardKeys, err = auth.NewKeyedAuth(Config.Consumer.AuthKeys) + mbp.Must(err, "failed to parse consumer auth keys") + + var serverTLS *tls.Config + var tap = network.NewTap() + + if Config.Gateway.ServerCertFile != "" { + serverTLS, err = server.BuildTLSConfig( + Config.Gateway.ServerCertFile, Config.Gateway.ServerCertKeyFile, "") + mbp.Must(err, "building server TLS config") + } - pb.RegisterGRPCDispatcher(*zone) - var grpcServer = grpc.NewServer( - grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor), - grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor), + // Bind our server listener, grabbing a random available port if Port is zero. + srv, err := server.New( + "", + Config.Gateway.Host, + Config.Gateway.Port, + serverTLS, + nil, + Config.Gateway.MaxGRPCRecvSize, + tap.Wrap, ) - grpc_prometheus.Register(grpcServer) + mbp.Must(err, "building Server instance") + + var ( + ctx = context.Background() + brokerConn = Config.Broker.MustDial(ctx) + shardConn = Config.Consumer.MustDial(ctx) + signalCh = make(chan os.Signal, 1) + tasks = task.NewGroup(ctx) + + journalClient = pb.NewAuthJournalClient( + pb.NewJournalClient(brokerConn), + PassThroughAuthorizer{}, + ) + shardClient = pc.NewAuthShardClient( + pc.NewShardClient(shardConn), + PassThroughAuthorizer{}, + ) + ) + srv.QueueTasks(tasks) - ctx := pb.WithDispatchDefault(context.Background()) - var tasks = task.NewGroup(ctx) + // Register proxying gRPC server. + pb.RegisterJournalServer(srv.GRPCServer, &JournalProxy{journalClient}) + pc.RegisterShardServer(srv.GRPCServer, &ShardProxy{shardClient}) - journalServer := NewJournalAuthServer(ctx, []byte(*jwtVerificationKey)) - shardServer := NewShardAuthServer(ctx, []byte(*jwtVerificationKey)) - pb.RegisterJournalServer(grpcServer, journalServer) - pc.RegisterShardServer(grpcServer, shardServer) + // Register gRPC web gateway. + var mux *runtime.ServeMux = runtime.NewServeMux( + runtime.WithMarshalerOption(runtime.MIMEWildcard, &gateway.JSONPb{EmitDefaults: true}), + runtime.WithProtoErrorHandler(runtime.DefaultHTTPProtoErrorHandler), + ) + pb.RegisterJournalHandler(tasks.Context(), mux, brokerConn) + pc.RegisterShardHandler(tasks.Context(), mux, shardConn) + srv.HTTPMux.Handle("/v1/", Config.Gateway.CORSWrapper(mux)) + + // Initialize connector networking frontend. + networkProxy, err := network.NewFrontend( + tap, + Config.Gateway.Host, + Config.Flow.ControlAPI.URL(), + Config.Flow.Dashboard.URL(), + pf.NewAuthNetworkProxyClient(pf.NewNetworkProxyClient(shardConn), shardKeys), + pc.NewAuthShardClient(pc.NewShardClient(shardConn), shardKeys), + shardKeys, + ) + mbp.Must(err, "failed to build network proxy") - // Will be used with all listeners. - var healthHandler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("OK\n")) + tasks.Queue("network-proxy-frontend", func() error { + return networkProxy.Serve(tasks.Context()) }) - restHandler := NewRestServer(ctx, fmt.Sprintf("localhost:%s", *tlsPort)) - schemaInferenceHandler := NewSchemaInferenceServer(ctx) + // Install signal handler & start gateway tasks. + signal.Notify(signalCh, syscall.SIGTERM, syscall.SIGINT) + tasks.Queue("handle-signal", func() error { + <-signalCh + log.Info("caught signal; exiting") + return nil + }) - // These routes will be exposed to the public internet and used for handling both http and https requests. - publicMux := http.NewServeMux() - publicMux.Handle("/healthz", healthHandler) - publicMux.Handle("/infer_schema", schemaInferenceHandler) - publicMux.Handle("/", restHandler) + // Block until all tasks complete. Assert none returned an error. + tasks.GoRun() + mbp.Must(tasks.Wait(), "data-plane-gateway task failed") + log.Info("goodbye") - if *controlPlaneAuthUrl == "" { - log.Fatalf("missing required argument control-plane-auth-url") - } - cpAuthUrl, err := url.Parse(*controlPlaneAuthUrl) - if err != nil { - log.Fatalf("invalid control-plane-auth-url: %v", err) - } + return nil +} + +func main() { + var parser = flags.NewParser(Config, flags.Default) + + _, _ = parser.AddCommand("serve", "Serve as Gazette broker", ` +Serve a Gazette broker with the provided configuration, until signaled to +exit (via SIGTERM). Upon receiving a signal, the broker will seek to discharge +its responsible journals and will exit only when it can safely do so. +`, &cmdServe{}) + + mbp.AddPrintConfigCmd(parser, iniFilename) + mbp.MustParseConfig(parser, iniFilename) +} + +/* proxyServer, tappedListener, err := proxy.NewTlsProxyServer(*hostname, uint16(tlsPortNum), certificates, shardServer.shardClient, *cpAuthUrl, []byte(*jwtVerificationKey)) @@ -195,3 +231,4 @@ func main() { log.Println("goodbye") os.Exit(0) } +*/ diff --git a/proxy/connection.go b/proxy/connection.go deleted file mode 100644 index f7c119c..0000000 --- a/proxy/connection.go +++ /dev/null @@ -1,120 +0,0 @@ -package proxy - -import ( - "context" - "net" - "net/http" - "time" - - pf "github.com/estuary/flow/go/protocols/flow" - log "github.com/sirupsen/logrus" -) - -type ProxyConnection struct { - hostname string - taskName string - shardID string - targetPort uint16 - client pf.NetworkProxy_ProxyClient - // readBuf is the remaining Data from the most recent response message. - // We don't do any explicit buffering, per se, at this layer. - // This is just here in case the buffer that's given to `Read` - // is too small to hold all the data from the last response. - readBuf []byte -} - -func (pc *ProxyConnection) singleConnectionTransport(useHttp2 bool) *http.Transport { - // There's potential for significant future improvement here, if we can dynamically - // re-establish upstream connections when they break. For now, we're just handling - // those cases by telling the client to close its connection, so it'll re-establish - // a new one on the next attempt. - return &http.Transport{ - DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) { - log.WithFields(log.Fields{ - "hostname": pc.hostname, - "shardID": pc.shardID, - }).Debug("returning proxy connection from dialer") - return KeepOpenProxyConn{ProxyConnection: pc}, nil - }, - MaxIdleConns: 1, - MaxIdleConnsPerHost: 1, - MaxConnsPerHost: 1, - IdleConnTimeout: 0, - //ResponseHeaderTimeout: 0, - MaxResponseHeaderBytes: 0, - ForceAttemptHTTP2: useHttp2, - } -} - -// KeepOpenProxyConn is returned from the http transport as the upstream -// connection for the proxy handler. It overrides the Close function to -// prevent the upstream connection from being closed until the client -// connection is also closed. -type KeepOpenProxyConn struct { - *ProxyConnection -} - -func (pc KeepOpenProxyConn) Close() error { - log.Debug("keeping open proxy connection") - return nil -} - -// TODO: Do we need to handle deadlines? -func (pc *ProxyConnection) SetDeadline(dl time.Time) error { - return nil -} -func (pc *ProxyConnection) SetReadDeadline(dl time.Time) error { - return nil -} -func (pc *ProxyConnection) SetWriteDeadline(dl time.Time) error { - return nil -} - -func (pc *ProxyConnection) LocalAddr() net.Addr { - return nil -} - -func (pc *ProxyConnection) RemoteAddr() net.Addr { - return nil -} - -func (pc *ProxyConnection) Close() error { - var err = pc.client.CloseSend() - log.WithFields(log.Fields{ - "hostname": pc.hostname, - "error": err, - }).Debug("closed upstream proxy client") - return err -} - -func (pc *ProxyConnection) Read(buf []byte) (int, error) { - if len(pc.readBuf) == 0 { - // We need to read another response - var resp, err = pc.client.Recv() - if err != nil { - return 0, err - } - pc.readBuf = resp.Data - } - var i = copy(buf, pc.readBuf) - if log.IsLevelEnabled(log.TraceLevel) { - log.WithFields(log.Fields{ - "hostname": pc.hostname, - "readBufLen": len(pc.readBuf), - "bufLen": len(buf), - "i": i, - }).Trace("read data from proxy conn") - } - pc.readBuf = pc.readBuf[i:] - return i, nil -} - -func (pc *ProxyConnection) Write(buf []byte) (int, error) { - var err = pc.client.Send(&pf.TaskNetworkProxyRequest{ - Data: buf, - }) - if err != nil { - return 0, err - } - return len(buf), nil -} diff --git a/proxy/http.go b/proxy/http.go deleted file mode 100644 index ab16842..0000000 --- a/proxy/http.go +++ /dev/null @@ -1,255 +0,0 @@ -package proxy - -import ( - "bytes" - "context" - "crypto/tls" - "encoding/json" - "fmt" - "html/template" - "net/http" - "net/http/httputil" - "net/url" - "strconv" - "strings" - "time" - - "github.com/estuary/data-plane-gateway/auth" - "github.com/estuary/flow/go/labels" - log "github.com/sirupsen/logrus" - "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/fasthttpadaptor" - "golang.org/x/net/http2" -) - -func (h *ProxyHandler) proxyHttp(ctx context.Context, clientConn *tls.Conn, proxyConn *ProxyConnection, portConfig *labels.PortConfig) error { - defer proxyConn.Close() - // We generally assume that the upstream connector container wishes to speak http/1.1, unless they explicitly request to use _only_ h2. - var useHttp2Upstream = portConfig != nil && portConfig.Protocol == "h2" - var isPublicPort = portConfig != nil && portConfig.Public - - var targetScheme = "http" - if useHttp2Upstream { - targetScheme = "https" - } - var proxy = httputil.ReverseProxy{ - Transport: proxyConn.singleConnectionTransport(useHttp2Upstream), - FlushInterval: 0, - ErrorHandler: func(w http.ResponseWriter, req *http.Request, err error) { - log.WithFields(log.Fields{ - "hostname": proxyConn.hostname, - "remoteAddr": clientConn.RemoteAddr().String(), - "shardID": proxyConn.shardID, - "error": err, - "URI": req.RequestURI, - }).Debug("proxy error") - handleHttpError(err, w, req) - }, - - Director: func(req *http.Request) { - req.URL.Host = proxyConn.hostname - req.URL.Scheme = targetScheme - if _, ok := req.Header["User-Agent"]; !ok { - // explicitly disable User-Agent so it's not set to default value - req.Header.Set("User-Agent", "") - - } - // if the port is private, then scrub the authentication token from the requests. - if !isPublicPort { - req.Header.Del("Authorization") - // There's no `DeleteCookie` function, so we parse them, delete them all, and - // add them back in while filtering out the flow_auth cookie. - var cookies = req.Cookies() - req.Header.Del("Cookie") - for _, cookie := range cookies { - if cookie.Name != auth.AuthCookieName { - req.AddCookie(cookie) - } - } - } - }, - } - - var handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // If the port is private, then require that each request has a JWT that permits it to - // access the task. We don't check the Authorization header if the port is public, since the header value - // might be meant to be interpreted by the connector itself. - if !isPublicPort { - var claims, authErr = auth.AuthenticateHttpReq(req, h.jwtVerificationKey) - if authErr == nil { - authErr = auth.EnforcePrefix(claims, proxyConn.taskName) - } - var acceptHeader = req.Header.Get("accept") - - // The port is private and the request is unauthorized. - // We might redirect the user to the dashboard so that they can authorize the request and get redirected back here. - // Or, this request might be the _result_ of a successful redirect back from that endpoint, in which case we'll - // redirect _again_, with a `Set-Cookie` header to make sure that the next request is authorized. But this type - // of redirection only makes sense if this request originated from an interactive browser session. For example, - // it wouldn't make sense to respond with a redirect to dashboard if this we a JSON API request. In that case, we'd - // prefer to simply return a JSON response with the error message. Checking if the accept header contains "html" - // just seemed like a cheap and easy way to determine if a redirect is likely to be appreciated, since actually parsing - // the accept header is pretty complicated. - if authErr != nil && strings.Contains(acceptHeader, "html") { - // Note that we only match this path if the request doesn't already contain a valid auth token. Technically, a - // connector could itself expose an `/auth-redirect` endpoint, and that would work as long as the request can be authorized. - if req.URL.Path == "/auth-redirect" { - h.redirectHandler.ServeHTTP(w, req) - return - } else { - // This is just a regular request that's unauthorized, so we'll handle this by redirecting to the dashboard. - var origUrl = "https://" + req.Host + req.URL.Path - var redirectTarget = h.controlPlaneAuthUrl.JoinPath("/data-plane-auth-req") - var query = &url.Values{} - query.Add("orig_url", origUrl) - query.Add("prefix", proxyConn.taskName) - redirectTarget.RawQuery = query.Encode() - - var targetUrl = redirectTarget.String() - log.WithFields(log.Fields{ - "error": authErr, - "host": req.Host, - "clientAddr": req.RemoteAddr, - "reqUrl": origUrl, - "redirectTarget": targetUrl, - }).Info("HTTP proxy request to private port is unauthorized") - - http.Redirect(w, req, targetUrl, 307) - return - } - } else if authErr != nil { - handleHttpError(authErr, w, req) - } - } - proxy.ServeHTTP(w, req) - }) - - // These timeouts seemed like reasonable starting points, and haven't been very - // carefully considered. But better arbitrary timeouts than no timeouts at all! - if clientConn.ConnectionState().NegotiatedProtocol == "h2" { - var h2Server = http2.Server{ - IdleTimeout: 10 * time.Second, - } - // The clientConn will be closed automatically by ServeConn, but we'll need to close the proxyConn ourselves - h2Server.ServeConn(clientConn, &http2.ServeConnOpts{ - Context: ctx, - Handler: handlerFunc, - BaseConfig: &http.Server{ - IdleTimeout: 10 * time.Second, - ReadTimeout: 20 * time.Second, - WriteTimeout: 20 * time.Second, - }, - }) - return nil - } else { - // We'll be speaking http/1.1, which requires a 3rd party library because there's no ServeConn function in Go's http package. - var server = fasthttp.Server{ - Handler: fasthttpadaptor.NewFastHTTPHandler(handlerFunc), - Name: fmt.Sprintf("%s (%s)", proxyConn.hostname, clientConn.RemoteAddr().String()), - IdleTimeout: 10 * time.Second, - ReadTimeout: 20 * time.Second, - WriteTimeout: 20 * time.Second, - } - - return server.ServeConn(clientConn) - } -} - -var errTemplate = template.Must(template.New("proxy-error").Parse(` - - - Error - - - - {{.}} - -`)) - -func handleHttpError(err error, w http.ResponseWriter, r *http.Request) { - var body []byte - var contentType string - var status = httpStatus(err) - - // If the error was caused by an issue with the upstream connection, then we must - // ask the client to close the connection and create a new one. This is important - // because the http proxy handler isn't currently able to re-establish connections - // in response to them breaking. So if the client continues to use this connection, - // then they will continue to get 5xx errors due to the broken upstream connection. - // Note that, while the `Connection` header is not compatible with http2, the Go - // http2 package seems to handle this by removing the header and sending a GOAWAY. - // In fact, this is the _only_ way I can figure out how to send a GOAWAY from a handler. - if status >= 500 { - w.Header().Add("Connection", "close") - } - - var headers = r.Header["Accept"] - var accept string - if len(headers) > 0 { - accept = headers[0] - } - if strings.Contains(accept, "json") { - body, _ = json.Marshal(map[string]interface{}{ - "error": err.Error(), - }) - contentType = "application/json" - } else if strings.Contains(accept, "html") { - var buf bytes.Buffer - if templateErr := errTemplate.Execute(&buf, err.Error()); templateErr != nil { - log.WithFields(log.Fields{ - "origError": err.Error(), - "templateError": templateErr.Error(), - }).Error("error rendering html error template") - } - body = buf.Bytes() - contentType = "text/html" - } else { - // just render as plain text - body = []byte(fmt.Sprintf("Error: %s", err)) - contentType = "text/plain" - } - - w.Header().Add("Content-Type", contentType) - w.Header().Add("Content-Length", strconv.Itoa(len(body))) - w.WriteHeader(status) - var _, writeErr = w.Write(body) - if writeErr != nil { - log.WithFields(log.Fields{ - "origError": err.Error(), - "templateError": writeErr.Error(), - }).Debug("failed to write error response body") - } -} - -func httpStatus(err error) int { - if err == NoMatchingShard { - return 404 - } else if err == auth.InvalidAuthToken || err == auth.UnsupportedAuthType { - return 400 - } else if err == auth.MissingAuthToken { - return 401 - } else if err == auth.Unauthorized { - // In this case, the user provided a valid auth token, which just didn't authorize them to access the shard. We return a 403 - // instead of a 404 because we can have _a little_ more trust in an authenticated user, and thus provide them with more - // specific and helpful information. - return 403 - } else { - return 503 - } -} - -func isHttp(negotiatedProto string) bool { - return negotiatedProto == "h2" || negotiatedProto == "http/1.1" -} diff --git a/proxy/proxy.go b/proxy/proxy.go deleted file mode 100644 index 4f99782..0000000 --- a/proxy/proxy.go +++ /dev/null @@ -1,675 +0,0 @@ -package proxy - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "io" - "math/rand" - "net" - "net/url" - "strconv" - "strings" - "sync" - "time" - - "github.com/estuary/flow/go/labels" - pf "github.com/estuary/flow/go/protocols/flow" - lru "github.com/hashicorp/golang-lru/v2" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - log "github.com/sirupsen/logrus" - pb "go.gazette.dev/core/broker/protocol" - pc "go.gazette.dev/core/consumer/protocol" - "golang.org/x/sync/errgroup" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - -var NoPrimaryShards = errors.New("no primary shards") -var NoMatchingShard = errors.New("no shards matching hostname") -var PortNotPublic = errors.New("port is not public and protocol is not http") -var ProtoNotHttp = errors.New("invalid protocol for port") - -// TappedListener is a net.Listener for all of the connections that are _not_ handled by the ProxyServer. -type TappedListener struct { - closeCh chan<- struct{} - proxyServer *ProxyServer - recv <-chan acceptResult -} - -// Accept waits for and returns the next connection to the listener. -// Connections returned by this listener will always be of type `*tls.Conn`. -func (l *TappedListener) Accept() (net.Conn, error) { - var result = <-l.recv - if result.err != nil { - return nil, result.err - } - if result.conn != nil { - return result.conn, nil - } - // If the result is zero-valued, then it means that the channel has closed - // and we should return an EOF error to signal that this listener is done. - return nil, io.EOF -} - -type acceptResult struct { - conn *tls.Conn - err error -} - -// Close closes the listener. -// Any blocked Accept operations will be unblocked and return errors. -func (l *TappedListener) Close() error { - var err = l.proxyServer.tlsListener.Close() - close(l.closeCh) - return err -} - -// Addr returns the listener's network address. -func (l *TappedListener) Addr() net.Addr { - return l.proxyServer.tlsListener.Addr() -} - -type ProxyServer struct { - closeCh <-chan struct{} - tlsListener net.Listener - overflow chan<- acceptResult - proxyHandler *ProxyHandler - baseConfig *tls.Config -} - -// NewTlsProxyServer returns a ProxyServer, which listens for TLS connections and will handle each connection either by -// proxying to a running task's container or by passing the connection on to the associated `TappedListener`. This -// decision is based on the SNI (Server Name Indicator) in the TLS Client Hello message. Connections that are made to -// subdomains of `hostname` will be proxied to a container of the shard that's indicated by the subdomain labels. -// Connections that are made to `hostname` exactly will be given to the returned `TappedListener` to be handled by -// another server. -func NewTlsProxyServer(hostname string, port uint16, tlsCerts []tls.Certificate, shardClient pc.ShardClient, controlPlaneAuthUrl url.URL, jwtVerificationKey []byte) (*ProxyServer, *TappedListener, error) { - var tcpListener, err = net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - return nil, nil, err - } - var closeCh = make(chan struct{}) - var proxyHandler = newHandler(hostname, shardClient, controlPlaneAuthUrl, jwtVerificationKey) - var tlsConfig = getTlsConfig(tlsCerts, proxyHandler) - - var tlsListener = tls.NewListener(tcpListener, tlsConfig) - - var acceptCh = make(chan acceptResult) - var server = &ProxyServer{ - closeCh: closeCh, - tlsListener: tlsListener, - overflow: acceptCh, - proxyHandler: proxyHandler, - baseConfig: tlsConfig, - } - var tappedListener = &TappedListener{ - closeCh: closeCh, - proxyServer: server, - recv: acceptCh, - } - return server, tappedListener, nil -} - -// Run starts the server accepting and handling new connections, and blocks until -// the server stops with an error (which will never be nil). -func (ps *ProxyServer) Run() error { - var err error - defer func() { - if err == nil { - panic("ProxyServer.Run has nil terminal error") - } - log.Info("proxy server shutting down, sending final error to overflow listener") - select { - case ps.overflow <- acceptResult{err: err}: - case <-ps.closeCh: - } - close(ps.overflow) - log.Info("proxy server shutdown complete") - }() - for { - select { - case <-ps.closeCh: - return fmt.Errorf("shutting down") - default: - } - var conn net.Conn - conn, err = ps.tlsListener.Accept() - if err != nil { - log.WithField("error", err).Error("failed to accept tls connection") - return err - } - // Start a new goroutine to handle this connection, so we don't block the accept loop - go func() { - // Await the completion of the TLS handshake. This is needed in order to - // ensure that ConnectionState is populated with the SNI from the client hello. - if hsErr := conn.(*tls.Conn).Handshake(); hsErr != nil { - log.WithFields(log.Fields{ - "error": hsErr, - "clientAddr": conn.RemoteAddr(), - }).Debug("tls handshake error") - return - } - var state = conn.(*tls.Conn).ConnectionState() - - if ps.proxyHandler.isProxySubdomain(state.ServerName) { - log.WithFields(log.Fields{ - "clientAddr": conn.RemoteAddr(), - "sni": state.ServerName, - }).Debug("handling connection as a proxy") - ps.proxyHandler.handleProxyConnection(context.Background(), conn.(*tls.Conn)) - } else { - log.WithFields(log.Fields{ - "clientAddr": conn.RemoteAddr(), - "sni": state.ServerName, - }).Debug("sending connection to overflow listener") - select { - case ps.overflow <- acceptResult{conn: conn.(*tls.Conn)}: - case <-ps.closeCh: - } - - } - }() - } -} - -func getTlsConfig(certs []tls.Certificate, proxyHandler *ProxyHandler) *tls.Config { - - return &tls.Config{ - // We use the same certs for all connections. We assume that this is a wildcard certificate for `*.$hostname` - Certificates: certs, - // NextProtos is used only for connections that are not to be proxied. - // We need to explicitly configure support for HTTP2 here, or else it won't be offered. - NextProtos: []string{"h2", "http/1.1"}, - - // This function looks up the server's ALPN protocols - // based on the server name given in the TLS client hello message. - GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { - log.WithFields(log.Fields{ - "sni": hello.ServerName, - "clientAddr": hello.Conn.RemoteAddr(), - "clientProtos": hello.SupportedProtos, - }).Debug("got tls client hello") - if proxyHandler.isProxySubdomain(hello.ServerName) { - var protos, proxyErr = proxyHandler.getAlpnProtocols(hello) - if proxyErr != nil { - log.WithFields(log.Fields{ - "sni": hello.ServerName, - "clientAddr": hello.Conn.RemoteAddr(), - "error": proxyErr, - }).Warn("error resolving sni to shard") - ProxyConnectionRejectedCounter.Inc() - return nil, proxyErr - } - return &tls.Config{ - Certificates: certs, - // Go's tls package would normally enable session tickets and automatically rotates the keys. The problem is that - // key rotation is tied to each individual tls config. If we want to enable session resumption that works across mutliple - // `tls.Config` instances, we'd have to implement key rotation ourselves, because that's the way `tls.Config` is designed. - // This might be worth doing at some point, but doesn't seem motivated right now. So we instead just disable session tickets - // altogether for proxied connections. - SessionTicketsDisabled: true, - // This is the main thing we needed to override. - NextProtos: protos, - }, nil - } else { - // nil config here means to use the base config. - // We'll accept any value for SNI for connections that aren't intended to be - // proxied to containers. It would probably also be OK to reject any connections - // with an SNI that doesn't match the configured hostname, but I'm also not really - // seeing a strong reason to do so. - return nil, nil - } - }, - } - -} - -// ProxyHandler proxies TCP traffic to connector containers, though a gRPC service in the reactor. -type ProxyHandler struct { - hostname string - proxyDomainSuffix string - controlPlaneAuthUrl url.URL - mu *sync.Mutex - redirectHandler *authRedirectHandler - - shardResolutionCache *lru.Cache[string, *resolvedShard] - shardClient pc.ShardClient - jwtVerificationKey []byte -} - -func newHandler(gatewayHostname string, shardClient pc.ShardClient, controlPlaneAuthUrl url.URL, jwtVerificationKey []byte) *ProxyHandler { - var cache, err = lru.New[string, *resolvedShard](SHARD_RESOLUTION_CACHE_MAX_SIZE) - if err != nil { - panic(fmt.Sprintf("failed to initialize shardResolutionCache: %v", err)) - } - - return &ProxyHandler{ - hostname: gatewayHostname, - proxyDomainSuffix: "." + gatewayHostname, - mu: &sync.Mutex{}, - shardResolutionCache: cache, - redirectHandler: newRedirectHandler(gatewayHostname), - controlPlaneAuthUrl: controlPlaneAuthUrl, - shardClient: shardClient, - jwtVerificationKey: jwtVerificationKey, - } -} - -func (h *ProxyHandler) isProxySubdomain(sni string) bool { - return strings.HasSuffix(sni, h.proxyDomainSuffix) && len(sni) > len(h.proxyDomainSuffix) -} - -func (h *ProxyHandler) getAlpnProtocols(hello *tls.ClientHelloInfo) ([]string, error) { - if hello.ServerName == "" { - return nil, fmt.Errorf("TLS client hello is missing SNI") - } - var resolved, err = h.getResolvedShard(hello.Context(), hello.ServerName, hello.Conn.RemoteAddr().String()) - if err != nil { - return nil, err - } - if configuredProto := resolved.getAlpnProto(); configuredProto != "" { - // The protocol can be comma-separated in order to allow an h2c server running in the connector - // to specify `h2,http/1.1`. The importance of this is questionable, so it might be something we - // remove if we find it's not necessary. - return strings.Split(configuredProto, ","), nil - } - return hello.SupportedProtos, nil -} - -// This solution kind of sucks, but it's expedient: An important part of exposing -// ports is that you can set the alpn protocol. This is used during the TLS -// handshake to help clients negotiate between the various protocols that all -// operate using TLS. So we need to know which alpn protocols to offer for a given -// hostname as part of the GetConfigForClient callback. Knowing this requires -// resolving the SNI hostname to a specific shard, whose labeling whill be used -// to validate the connection request. We _also_ need to resolve that shard in -// order to know which endpoint to connect to for dialing the grpc.ClientConn. The -// problem is that there's no way to pass state between that GetConfigForClient -// callback and the rest of the process. So we have GetConfigForClient do the -// shard resolution and cache the result. GetConfigForClient will call the -// GetAlpnProtocols function in order to see which alpn protocols should be -// offered for the connection. GetAlpnProtocols will validate the hostname -// and fetch the shard (and selecting from among multiple matched shards, if -// necessary), cacheing the result. HandleProxyConnection will then lookup in the -// cache to continue the connection process. But the shard listing results include -// transient information, such as status, which cannot be cached for very long. -// The TTL on the cache then acts on a limit to the total amount of time required -// to complete the TLS handshake. Probs not a biggie, but worth knowing. The other -// thing is that it's an LRU cache so that we can be reasonably confident that -// the entries for currently handshaking connections won't get evicted in between -// these two phases under heavy load. This means that concurrent connection -// requests for _n_ distinct hosts could start to fail if n > cacheLimit. I see -// this as the failure mode just being load shedding, although it does make this -// number ...complicated. -const SHARD_RESOLUTION_CACHE_MAX_SIZE = 1024 -const SHARD_RESOLUTION_CACHE_MAX_AGE = 30 * time.Second - -type resolvedShard struct { - spec pc.ShardSpec - labeling labels.ShardLabeling - route pb.Route - shardHost string - targetPort uint16 - fetchedAt time.Time -} - -func (rs *resolvedShard) expiration() time.Time { - return rs.fetchedAt.Add(SHARD_RESOLUTION_CACHE_MAX_AGE) -} - -func (rs *resolvedShard) getAlpnProto() string { - if cfg := rs.maybePortConfig(); cfg != nil { - return cfg.Protocol - } else { - return "" - } -} - -func (rs *resolvedShard) maybePortConfig() *labels.PortConfig { - return rs.labeling.Ports[rs.targetPort] -} - -// getResolvedShard returns either a non-nil resolvedShard, or an error. -func (h *ProxyHandler) getResolvedShard(ctx context.Context, sni string, clientAddr string) (*resolvedShard, error) { - var err error - var resolved, ok = h.shardResolutionCache.Get(sni) - if !ok || resolved.expiration().Before(time.Now()) { - resolved, err = h.doResolveShard(ctx, sni, clientAddr) - if err != nil { - // We do _not_ cache failed resolutions - return nil, err - } - h.shardResolutionCache.Add(sni, resolved) - } - return resolved, nil -} - -func (h *ProxyHandler) doResolveShard(ctx context.Context, sni string, clientAddr string) (*resolvedShard, error) { - var query, err = h.parseServerName(sni) - if err != nil { - return nil, err - } - - var queryLabels = []pb.Label{ - {Name: labels.ExposePort, Value: strconv.Itoa(int(query.port))}, - {Name: labels.Hostname, Value: query.hostname}, - } - if query.keyBegin != "" && query.rClockBegin != "" { - // It just so happens that the labels will still be sorted after appending these - queryLabels = append(queryLabels, - pb.Label{Name: labels.KeyBegin, Value: query.keyBegin}, - pb.Label{Name: labels.RClockBegin, Value: query.rClockBegin}, - ) - } - listResp, err := h.shardClient.List(ctx, &pc.ListRequest{ - Selector: pb.LabelSelector{ - Include: pb.LabelSet{ - Labels: queryLabels, - }, - }, - }) - if err != nil { - return nil, fmt.Errorf("listing shards: %w", err) - } - if listResp.Status != pc.Status_OK { - return nil, fmt.Errorf("error status when listing shards: %s", listResp.Status.String()) - } - - // If there's no shards, then immediately return - if len(listResp.Shards) == 0 { - return nil, NoMatchingShard - } - - var shardsWithPrimary []int - for i, shard := range listResp.Shards { - if shard.Route.Primary >= 0 { - shardsWithPrimary = append(shardsWithPrimary, i) - } - } - if len(shardsWithPrimary) == 0 { - return nil, NoPrimaryShards - } - - var shardIndex = shardsWithPrimary[0] - if len(shardsWithPrimary) > 1 { - // If there's more than one matching shard having a primary assignment, then select one of them at random. - shardIndex = shardsWithPrimary[rand.Intn(len(shardsWithPrimary))] - } - var shard = listResp.Shards[shardIndex] - log.WithFields(log.Fields{ - "sni": sni, - "shardID": shard.Spec.Id, - "clientAddr": clientAddr, - }).Debug("resolved proxy host to shard") - - var route = shard.Route - if route.Primary < 0 { - return nil, fmt.Errorf("no primary member available for shard: '%s'", shard.Spec.Id) - } - if len(route.Endpoints) == 0 { - return nil, fmt.Errorf("no endpoints available for shard: '%s'", shard.Spec.Id) - } - labeling, err := labels.ParseShardLabels(shard.Spec.LabelSet) - if err != nil { - return nil, fmt.Errorf("parsing shard labels: %w", err) - } - - return &resolvedShard{ - spec: shard.Spec, - route: shard.Route, - labeling: labeling, - shardHost: query.hostname, - targetPort: query.port, - fetchedAt: time.Now(), - }, nil -} - -func (h *ProxyHandler) handleProxyConnection(ctx context.Context, conn *tls.Conn) { - defer conn.Close() - var state = conn.ConnectionState() - var sni = state.ServerName - var clientAddr = conn.RemoteAddr().String() - var resolved, err = h.getResolvedShard(ctx, sni, clientAddr) - if err != nil { - err = fmt.Errorf("resolving shard for connection attempt: %w", err) - } else if !isHttp(state.NegotiatedProtocol) { - // Check to see if the connection is allowed. If the protocol is http, then - // the port visibility can either be public or private, since the http proxy - // will enforce authZ based on the Authorization request header. But for any - // other protocol, we'll require that the port is public - if portConfig := resolved.maybePortConfig(); portConfig != nil { - if !portConfig.Public { - err = PortNotPublic - } - } else { - err = PortNotPublic - } - } - - if err != nil { - log.WithFields(log.Fields{ - "error": err, - "sni": sni, - "clientAddr": clientAddr, - "proto": state.NegotiatedProtocol, - }).Warn("rejecting connection") - ProxyConnectionRejectedCounter.Inc() - return - } - - var portStr = strconv.Itoa(int(resolved.targetPort)) - shardID := resolved.spec.Id.String() - ProxyConnectionsAcceptedCounter.WithLabelValues(shardID, portStr).Inc() - if err := h.proxyConnection(ctx, conn, sni, clientAddr, resolved); err != nil { - ProxyConnectionsClosedCounter.WithLabelValues(shardID, portStr, "error").Inc() - log.WithFields(log.Fields{ - "error": err, - "sni": sni, - "clientAddr": clientAddr, - "proto": state.NegotiatedProtocol, - "shard": shardID, - }).Debug("failed to proxy connection") - } else { - ProxyConnectionsClosedCounter.WithLabelValues(shardID, portStr, "ok").Inc() - log.WithFields(log.Fields{ - "sni": sni, - "clientAddr": clientAddr, - "proto": state.NegotiatedProtocol, - "shard": shardID, - }).Debug("finished proxy connection") - } -} - -func (h *ProxyHandler) proxyConnection(ctx context.Context, conn *tls.Conn, sni string, clientAddr string, resolved *resolvedShard) error { - shardID := resolved.spec.Id.String() - var endpoint = resolved.route.Endpoints[resolved.route.Primary] - reactorAddr := endpoint.GRPCAddr() - log.WithFields(log.Fields{ - "sni": sni, - "clientAddr": clientAddr, - "reactorAddr": reactorAddr, - }).Debug("starting proxy connection") - // Ideally we'd reuse an existing connection that's cached per reactor. - // I don't think that's necessary at this stage, though. - proxyConn, err := grpc.DialContext(ctx, reactorAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - return fmt.Errorf("connecting to reactor: %w", err) - } - defer proxyConn.Close() - var proxyClient = pf.NewNetworkProxyClient(proxyConn) - - proxyStreaming, err := proxyClient.Proxy(ctx) - if err != nil { - return fmt.Errorf("starting proxy RPC: %w", err) - } - defer proxyStreaming.CloseSend() - - if err = proxyStreaming.Send(&pf.TaskNetworkProxyRequest{ - Open: &pf.TaskNetworkProxyRequest_Open{ - ShardId: resolved.spec.Id, - TargetPort: uint32(resolved.targetPort), - ClientAddr: conn.RemoteAddr().String(), - }, - }); err != nil { - return fmt.Errorf("sending Open message: %w", err) - } - - openResp, err := proxyStreaming.Recv() - if err != nil { - return fmt.Errorf("receiving opened response: %w", err) - } - if err = validateOpenResponse(openResp.OpenResponse); err != nil { - return err - } - - var proxyConnnection = &ProxyConnection{ - hostname: sni, - taskName: resolved.labeling.TaskName, - shardID: shardID, - targetPort: resolved.targetPort, - client: proxyStreaming, - } - var negotiatedProto = conn.ConnectionState().NegotiatedProtocol - - // This logging is a bit too verbose, but too helpful to be removed just yet. - log.WithFields(log.Fields{ - "sni": sni, - "clientAddr": clientAddr, - "reactorAddr": reactorAddr, - "proto": negotiatedProto, - }).Debug("starting to proxy connection data") - - // We're finally ready to copy the data between the connection and our grpc streaming rpc. - if isHttp(negotiatedProto) { - return h.proxyHttp(ctx, conn, proxyConnnection, resolved.maybePortConfig()) - } else { - return proxyTcp(ctx, conn, proxyConnnection) - } -} - -func proxyTcp(ctx context.Context, clientConn *tls.Conn, proxyConn *ProxyConnection) error { - - grp, ctx := errgroup.WithContext(ctx) - // Task that copies data from the client connection to the reactor. - grp.Go(func() error { - if outgoingBytes, e := io.Copy(clientConn, proxyConn); e != nil { - log.WithFields(log.Fields{ - "hostname": proxyConn.hostname, - "error": e, - "outgoingBytes": outgoingBytes, - }).Debug("copyProxyResponseData completed with error") - return e - } else { - log.WithFields(log.Fields{ - "hostname": proxyConn.hostname, - "outgoingBytes": outgoingBytes, - }).Debug("copyProxyResponseData completed successfully") - return nil - } - }) - - // Task that copies data from the reactor to the client - grp.Go(func() error { - defer proxyConn.Close() - if incomingBytes, e := io.Copy(proxyConn, clientConn); e != nil { - log.WithFields(log.Fields{ - "hostname": proxyConn.hostname, - "error": e, - "incomingBytes": incomingBytes, - }).Debug("copyProxyRequestData completed with error") - return e - } else { - log.WithFields(log.Fields{ - "hostname": proxyConn.hostname, - "incomingBytes": incomingBytes, - }).Debug("copyProxyRequestData completed successfully") - return nil - } - }) - - var err = grp.Wait() - log.WithField("error", err).Debug("finished proxy connection") - return err -} - -func validateOpenResponse(resp *pf.TaskNetworkProxyResponse_OpenResponse) error { - if resp == nil { - return fmt.Errorf("missing open response") - } - if resp.Status != pf.TaskNetworkProxyResponse_OK { - return fmt.Errorf("open response status (%s) not OK", resp.Status) - } - return nil -} - -type shardQuery struct { - hostname string - port uint16 - // We don't bother to parse out the keyBegin and rClockBegin fields - // because we'd only need to convert them to strings again to use as - // label selectors. - keyBegin string - rClockBegin string -} - -func (h *ProxyHandler) parseServerName(sni string) (*shardQuery, error) { - var shardAndPort, domainSuffix, ok = strings.Cut(sni, ".") - if !ok { - return nil, fmt.Errorf("sni does not have enough components") - } - if domainSuffix != h.hostname { - return nil, fmt.Errorf("invalid sni does not match domain suffix") - } - if len(shardAndPort) == 0 { - return nil, fmt.Errorf("invalid sni contains empty label") - } - - var query = &shardQuery{} - var parts = strings.Split(shardAndPort, "-") - var portStr string - if len(parts) == 2 { - query.hostname = parts[0] - portStr = parts[1] - } else if len(parts) == 4 { - query.hostname = parts[0] - query.keyBegin = parts[1] - query.rClockBegin = parts[2] - portStr = parts[3] - } else { - return nil, fmt.Errorf("invalid subdomain") - } - - var targetPort, err = strconv.ParseUint(portStr, 10, 16) - if err != nil { - return nil, fmt.Errorf("parsing subdomain port number: %w", err) - } - query.port = uint16(targetPort) - - return query, nil -} - -var ProxyConnectionsAcceptedCounter = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "net_proxy_conns_accept_total", - Help: "counter of proxy connections that have been accepted", -}, []string{"shard", "port"}) -var ProxyConnectionsClosedCounter = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "net_proxy_conns_closed_total", - Help: "counter of proxy connections that have completed and closed", -}, []string{"shard", "port", "status"}) - -var ProxyConnectionRejectedCounter = promauto.NewCounter(prometheus.CounterOpts{ - Name: "net_proxy_conns_reject_total", - Help: "counter of proxy connections that have been rejected due to error or invalid sni", -}) - -var ProxyConnBytesInboundCounter = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "net_proxy_conn_inbound_bytes_total", - Help: "total bytes proxied from client to container", -}, []string{"shard", "port"}) -var ProxyConnBytesOutboundCounter = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "net_proxy_conn_outbound_bytes_total", - Help: "total bytes proxied from container to client", -}, []string{"shard", "port"}) diff --git a/proxy/redirect.go b/proxy/redirect.go deleted file mode 100644 index 40466f5..0000000 --- a/proxy/redirect.go +++ /dev/null @@ -1,98 +0,0 @@ -package proxy - -import ( - "bytes" - "errors" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - - "github.com/estuary/data-plane-gateway/auth" - "github.com/sirupsen/logrus" -) - -// authRedirectHandler is used in the authentication flow for accessing private ports throught the proxy. -// It handles requests to `/auth-redirect` endpoints after users have successfully acquired an auth token -// from the dashboard (control-plane). It expects the request URI to contain `token` and `orig_url` -// parameters. Assuming the request and token are valid, the user will be redirected to `orig_url` -// with a cookie that contains their auth token. -type authRedirectHandler struct { - dpgDomainSuffix string -} - -func newRedirectHandler(dpgHostname string) *authRedirectHandler { - return &authRedirectHandler{ - dpgDomainSuffix: "." + dpgHostname, - } -} - -func (h *authRedirectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - var params = req.URL.Query() - // Note that we don't do any validation of the token here. If it's not valid, then we'll - // catch it when the browser requests the new location, and can handle it then. - var token = params.Get("token") - if token == "" { - renderAuthError(errors.New("url is missing the token parameter"), w, req) - return - } - var origUrl = params.Get("orig_url") - if origUrl == "" { - renderAuthError(errors.New("url is missing the orig_url parameter"), w, req) - return - } - var origUrlParsed, err = url.Parse(origUrl) - if err != nil { - renderAuthError(fmt.Errorf("invalid orig_url parameter: %w", err), w, req) - return - } - - // Check that the hostname of the original url is actually a subdomain of DPG's hostname. - // This isn't technically neccessary for security because the cookie is scoped to a single - // origin. But it makes sense to fail fast if we can. - if !strings.HasSuffix(origUrlParsed.Hostname(), h.dpgDomainSuffix) { - renderAuthError(fmt.Errorf("invalid orig_url parameter: hostname '%s' is not a subdomain of %s", origUrlParsed.Hostname(), h.dpgDomainSuffix), w, req) - return - } - - var cookie = &http.Cookie{ - Name: auth.AuthCookieName, - Value: token, - Secure: true, - HttpOnly: true, - Path: "/", - } - http.SetCookie(w, cookie) - http.Redirect(w, req, origUrl, 307) -} - -// renderAuthError writes an html error response page with a 400 status. We don't redirect -// these back to the dashboard in order to prevent an infinite loop of redirects. -// This doesn't handle multiple content types because this is only expected to be used -// with interactive sessions (html). -func renderAuthError(err error, w http.ResponseWriter, r *http.Request) { - var body []byte - var contentType string - - var buf bytes.Buffer - if templateErr := errTemplate.Execute(&buf, err.Error()); templateErr != nil { - logrus.WithFields(logrus.Fields{ - "origError": err.Error(), - "templateError": templateErr.Error(), - }).Error("error rendering html error template") - } - body = buf.Bytes() - contentType = "text/html" - - w.Header().Add("Content-Type", contentType) - w.Header().Add("Content-Length", strconv.Itoa(len(body))) - w.WriteHeader(400) - var _, writeErr = w.Write(body) - if writeErr != nil { - logrus.WithFields(logrus.Fields{ - "origError": err.Error(), - "templateError": writeErr.Error(), - }).Warn("failed to write error response body") - } -} diff --git a/rest.go b/rest.go deleted file mode 100644 index c7e5f93..0000000 --- a/rest.go +++ /dev/null @@ -1,131 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "net/http" - "regexp" - "strings" - "time" - - "github.com/gogo/gateway" - "github.com/grpc-ecosystem/grpc-gateway/runtime" - log "github.com/sirupsen/logrus" - "github.com/urfave/negroni" - _ "go.gazette.dev/core/broker/protocol" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - - bgw "github.com/estuary/data-plane-gateway/gen/broker/protocol" - cgw "github.com/estuary/data-plane-gateway/gen/consumer/protocol" -) - -// NewRestServer returns an http.Handler that proxies REST requests to broker and consumer gRPC -// services. The `gatewayAddr` parameter, surprisingly, is what it proxies _to_. It establishes a -// connection to `gatewayAddr` immediately, because grpc-gateway requires an established connection -// before it can register the handlers. There's no good reason for that. It's just how it do. So -// because grpc-gateway requires an established connection, we proxy to _ourselves_ instead of the -// actual broker and consumer endpoints, so that we are able to start this thing even when the -// broker or consumer are unavailable. If you know of a way to avoid this nonsense and lazily -// connect to the broker/consumer endpoints, then please feel free to implement that and delete this -// nauseating comment. -func NewRestServer(ctx context.Context, gatewayAddr string) http.Handler { - var err error - - jsonpb := &gateway.JSONPb{ - EmitDefaults: true, - Indent: "", - OrigName: false, - } - var mux *runtime.ServeMux = runtime.NewServeMux( - runtime.WithMarshalerOption(runtime.MIMEWildcard, jsonpb), - runtime.WithProtoErrorHandler(runtime.DefaultHTTPProtoErrorHandler), - ) - - // Since we dial ourselves on the loopback address, the hostname won't match the TLS cert. - opts := []grpc.DialOption{ - grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ - InsecureSkipVerify: true, - })), - grpc.WithChainUnaryInterceptor(grpc.UnaryClientInterceptor(grpc.UnaryClientInterceptor(logRestDelegateRPC))), - } - - err = bgw.RegisterJournalHandlerFromEndpoint(ctx, mux, gatewayAddr, opts) - if err != nil { - log.Fatalf("Failed to initialize journal rest gateway: %v", err) - } - - err = cgw.RegisterShardHandlerFromEndpoint(ctx, mux, gatewayAddr, opts) - if err != nil { - log.Fatalf("Failed to initialize shard rest gateway: %v", err) - } - - n := negroni.Classic() - n.Use(negroni.HandlerFunc(cors)) - n.UseHandler(mux) - - return n -} - -func logRestDelegateRPC(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - var start = time.Now().UTC() - // "method" is the gRPC term, which actually means "path" in HTTP terms. - log.WithField("method", method).Trace("starting REST delegate RPC") - var err = invoker(ctx, method, req, reply, cc, opts...) - log.WithFields(log.Fields{ - "method": method, - "timeMillis": time.Now().UTC().Sub(start).Milliseconds(), - "error": err, - }).Debug("finished REST delegate gRPC") - return err -} - -func cors(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - if corsConfig.IsAllowed(r.Header.Get("Origin")) { - rw.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) - rw.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") - rw.Header().Set("Access-Control-Allow-Headers", "Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma, Authorization") - } - - if r.Method == "OPTIONS" { - return - } - - next(rw, r) -} - -type corsSettings struct { - allowedOrigins []string -} - -func NewCorsSettings(rawOriginFlag string) *corsSettings { - return &corsSettings{ - allowedOrigins: strings.Split(rawOriginFlag, ","), - } -} - -func (c *corsSettings) IsAllowed(origin string) bool { - if c.allowWildcard() { - return true - } - - for _, allowed := range c.allowedOrigins { - if matched, _ := regexp.MatchString(allowed, origin); matched { - return true - } - } - - return false -} - -func (c *corsSettings) allowWildcard() bool { - return len(c.allowedOrigins) == 1 && c.allowedOrigins[0] == "*" -} - -func copyHeader(dst, src http.Header) { - for k, vv := range src { - for _, v := range vv { - dst.Add(k, v) - } - } -} \ No newline at end of file diff --git a/schema_inference.go b/schema_inference.go deleted file mode 100644 index 6ebe506..0000000 --- a/schema_inference.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - context "context" - "fmt" - "io" - "net/http" - - "github.com/estuary/data-plane-gateway/auth" - "github.com/urfave/negroni" -) - -func NewSchemaInferenceServer(ctx context.Context) http.Handler { - inferenceHandler := negroni.Classic() - inferenceHandler.Use(negroni.HandlerFunc(cors)) - inferenceHandler.UseHandler(schemaInferenceHandler) - - return inferenceHandler -} - -// Will be used with both http and https -// Inspired partially by https://gist.github.com/yowu/f7dc34bd4736a65ff28d -var schemaInferenceHandler = http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { - // Do auth - // Pull JWT from authz header - // See auth.go:authorized() - // decodeJWT(that bearer token) -> AuthorizedClaims - claims, err := auth.AuthenticateHttpReq(req, []byte(*jwtVerificationKey)) - if err != nil { - http.Error(writer, err.Error(), http.StatusUnauthorized) - return - } - - collection_name := req.URL.Query().Get("collection") - authorization_error := auth.EnforcePrefix(claims, collection_name) - - // enforcePrefix(claims, collection_name) - // collection_name comes from actual inference request - if authorization_error != nil { - http.Error(writer, authorization_error.Error(), http.StatusForbidden) - return - } - - // TODO: rename the argument to `?collection=...` in the schema inference service, then get rid of this: - args := req.URL.Query() - args.Set("collection_name", args.Get("collection")) - args.Del("collection") - - // Call inference - inference_response, inference_error := http.Get(fmt.Sprintf("http://%s/infer_schema?%s", *inferenceAddr, args.Encode())) - - if inference_error != nil { - // An error is returned if there were too many redirects or if there was an HTTP protocol error. - // A non-2xx response doesn't cause an error. - http.Error(writer, inference_error.Error(), http.StatusInternalServerError) - return - } - - defer inference_response.Body.Close() - // Return result - - copyHeader(writer.Header(), inference_response.Header) - writer.WriteHeader(inference_response.StatusCode) - io.Copy(writer, inference_response.Body) -}) diff --git a/shard_server.go b/shard_server.go index 88a6001..bd191ff 100644 --- a/shard_server.go +++ b/shard_server.go @@ -2,87 +2,31 @@ package main import ( context "context" - "fmt" - "github.com/estuary/data-plane-gateway/auth" - log "github.com/sirupsen/logrus" + pb "go.gazette.dev/core/broker/protocol" pc "go.gazette.dev/core/consumer/protocol" ) -type ShardAuthServer struct { - clientCtx context.Context - shardClient pc.ShardClient - jwtVerificationKey []byte +type ShardProxy struct { + sc pc.ShardClient } -func NewShardAuthServer(ctx context.Context, jwtVerificationKey []byte) *ShardAuthServer { - shardClient, err := newShardClient(ctx, *consumerAddr) - if err != nil { - log.Fatalf("Failed to connect to consumer: %v", err) - } - - authServer := &ShardAuthServer{ - clientCtx: ctx, - shardClient: shardClient, - jwtVerificationKey: jwtVerificationKey, - } - - return authServer +func (s *ShardProxy) List(ctx context.Context, req *pc.ListRequest) (*pc.ListResponse, error) { + return s.sc.List(pb.WithDispatchDefault(ctx), req) } -func newShardClient(ctx context.Context, addr string) (pc.ShardClient, error) { - var entry = log.WithField("address", addr) - entry.Debug("starting to connect shard client") - conn, err := dialAddress(ctx, addr) - if err != nil { - return nil, fmt.Errorf("error connecting to server: %w", err) - } - - entry.Info("successfully connected shard client") - return pc.NewShardClient(conn), nil -} - -// List implements protocol.ShardServer -func (s *ShardAuthServer) List(ctx context.Context, req *pc.ListRequest) (*pc.ListResponse, error) { - claims, err := auth.AuthenticateGrpcReq(ctx, s.jwtVerificationKey) - if err != nil { - return nil, err - } - - err = auth.EnforceSelectorPrefix(claims, req.Selector) - if err != nil { - return nil, fmt.Errorf("Unauthorized: %w", err) - } - - return s.shardClient.List(ctx, req) - -} - -// Stat implements protocol.ShardServer -func (s *ShardAuthServer) Stat(ctx context.Context, req *pc.StatRequest) (*pc.StatResponse, error) { - claims, err := auth.AuthenticateGrpcReq(ctx, s.jwtVerificationKey) - if err != nil { - return nil, err - } - - err = auth.EnforcePrefix(claims, req.Shard.String()) - if err != nil { - return nil, fmt.Errorf("Unauthorized: %w", err) - } - - return s.shardClient.Stat(ctx, req) - +func (s *ShardProxy) Stat(ctx context.Context, req *pc.StatRequest) (*pc.StatResponse, error) { + return s.sc.Stat(pb.WithDispatchDefault(ctx), req) } -// We're currently only implementing the read-only RPCs for protocol.ShardServer. -func (s *ShardAuthServer) Apply(context.Context, *pc.ApplyRequest) (*pc.ApplyResponse, error) { - return nil, fmt.Errorf("Unsupported operation: `Apply`") +func (s *ShardProxy) Apply(ctx context.Context, req *pc.ApplyRequest) (*pc.ApplyResponse, error) { + return s.sc.Apply(pb.WithDispatchDefault(ctx), req) } -func (s *ShardAuthServer) GetHints(context.Context, *pc.GetHintsRequest) (*pc.GetHintsResponse, error) { - return nil, fmt.Errorf("Unsupported operation: `GetHints`") +func (s *ShardProxy) GetHints(ctx context.Context, req *pc.GetHintsRequest) (*pc.GetHintsResponse, error) { + return s.sc.GetHints(pb.WithDispatchDefault(ctx), req) } -func (s *ShardAuthServer) Unassign(context.Context, *pc.UnassignRequest) (*pc.UnassignResponse, error) { - return nil, fmt.Errorf("Unsupported operation: `Unassign`") +func (s *ShardProxy) Unassign(ctx context.Context, req *pc.UnassignRequest) (*pc.UnassignResponse, error) { + return s.sc.Unassign(pb.WithDispatchDefault(ctx), req) } -var _ pc.ShardServer = &ShardAuthServer{} +var _ pc.ShardServer = &ShardProxy{} diff --git a/test.sh b/test.sh deleted file mode 100755 index a8bde62..0000000 --- a/test.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/bash - -set -e - -function bail() { - log "$@" 1>&2 - exit 1 -} - -function log() { - echo "[test.sh] ${1}" -} - -# Ensure we have openssl, which is needed in order to generate the tls certificate. -command -v openssl || bail "This script requires the openssl binary, which was not found on the PATH" - -# The first arg sets the MODE. There are two MODES: -# 1. "server" mode will launch flow, launch the gateway, and deploy a -# source-hello-world capture. It stays running from that point on until the user -# kills the server. This is useful for interactive testing or authoring tests. -# 2. "run" mode will do the same setup as "server" mode, but will then execute -# the typescript tests with deno. When these finish, the test server will -# shutdown. -MODE="${1:-server}" -log "MODE: $MODE" - -# The second arg sets the GATEWAY_BIN. This is the full path to the gateway binary to -# use. This allows local developers to use their local build/installation of -# the gateway, while CI can build a fresh copy immediately before running tests. -GATEWAY_BIN="${2}" -if [ -z "${GATEWAY_BIN}" ]; then - GATEWAY_BIN="$(command -v data-plane-gateway)" -fi - -# The third arg sets the FLOW_BIN. This is the full path to the flow binary to -# use. This allows local developers to use their local build/installation of -# flowctl, while CI can download the flowctl binary to a known location. -FLOW_BIN="${3}" -if [ -z "${FLOW_BIN}" ]; then - FLOW_BIN="$(command -v flowctl-go)" -fi - -ROOT_DIR="$(git rev-parse --show-toplevel)" -cd "${ROOT_DIR}" - -TESTDIR="test/tmp" - -# Ensure we start with an empty dir, since temporary data plane files will go here. -# Remove it, if it exists already. -if [[ -d "${TESTDIR}" ]]; then - rm -r ${TESTDIR} -fi -mkdir -p "${TESTDIR}" - -# Map to an absolute directory. -export TESTDIR=$(realpath ${TESTDIR}) - -# `flowctl` commands which interact with the data plane look for *_ADDRESS -# variables, which are created by the temp-data-plane we're about to start. -export BROKER_ADDRESS=unix://localhost${TESTDIR}/gazette.sock -export CONSUMER_ADDRESS=unix://localhost${TESTDIR}/consumer.sock -export GATEWAY_PORT=28318 - -export BUILD_ID='0000000000000000' -export CATALOG_SOURCE="test/acmeCo/source-hello-world.flow.yaml" -export CATALOG_SOURCE_ARABIC="test/acmeCo/arabic-source-hello-world.flow.yaml" - -# This is needed in order for docker run commands to work on ARM macs. -export DOCKER_DEFAULT_PLATFORM="linux/amd64" - -log "TESTDIR setup: ${TESTDIR}" - -# Start an empty local data plane within our TESTDIR as a background job. -# --poll so that connectors are polled rather than continuously tailed. -# --sigterm to verify we cleanly tear down the test catalog (otherwise it hangs). -# --tempdir to use our known TESTDIR rather than creating a new temporary directory. -# --unix-sockets to create UDS socket files in TESTDIR in well-known locations. -${FLOW_BIN} temp-data-plane \ - --log.level info \ - --tempdir=${TESTDIR} \ - --unix-sockets \ - --sigterm \ - & -DATA_PLANE_PID=$! - -log "Data plane launched: ${DATA_PLANE_PID}" - -# Generate a private key and a self-signed TLS certificate to use for the test. -openssl req -x509 -nodes -days 365 \ - -subj "/C=CA/ST=QC/O=Estuary/CN=localhost:${GATEWAY_PORT}" \ - -newkey rsa:2048 -keyout ${TESTDIR}/tls-private-key.pem \ - -out ${TESTDIR}/tls-self-signed-cert.pem - -# Start the gateway and point it at the data plane -${GATEWAY_BIN} \ - --port=${GATEWAY_PORT} \ - --broker-address=${BROKER_ADDRESS} \ - --consumer-address=${CONSUMER_ADDRESS} \ - --tls-certificate=${TESTDIR}/tls-self-signed-cert.pem \ - --tls-private-key=${TESTDIR}/tls-private-key.pem \ - --control-plane-auth-url=http://localhost:3000/ \ - & -GATEWAY_PID=$! - -log "Gateway launched: ${GATEWAY_PID}" - -# Arrange to stop the background processes on exit. -trap "kill -s SIGTERM ${DATA_PLANE_PID} \ - && kill -s SIGTERM ${GATEWAY_PID} \ - && wait ${DATA_PLANE_PID} \ - && wait ${GATEWAY_PID} \ - && exit 0" \ - EXIT - -# Build the catalog. -${FLOW_BIN} api build \ - --build-db=${TESTDIR}/builds/${BUILD_ID} \ - --build-id=${BUILD_ID} \ - --source=${CATALOG_SOURCE} || bail "Build failed." - -# Build the catalog for some minor multi language testing -${FLOW_BIN} api build \ - --build-db=${TESTDIR}/builds/${BUILD_ID} \ - --build-id=${BUILD_ID} \ - --source=${CATALOG_SOURCE_ARABIC} || bail "Build failed." - -log "Build finished" - -# Activate the catalog. -${FLOW_BIN} api activate \ - --build-id=${BUILD_ID} \ - --all \ - --log.level=info || bail "Activate failed." - - -log "Activation finished" - -if [ "${MODE}" == "run" ]; then - # Wait just a bit longer for the shard to boot up. - sleep 5 - - make test - - # Tests pass, so let's cleanup the test catalog so the data plane exits cleanly. - ${FLOW_BIN} api delete \ - --build-id=${BUILD_ID} \ - --all \ - --log.level=info || bail "Delete failed." - - log "Test Passed" -else - log "Ready" - wait -fi diff --git a/test/acmeCo/arabic-source-hello-world.flow.yaml b/test/acmeCo/arabic-source-hello-world.flow.yaml deleted file mode 100644 index a203385..0000000 --- a/test/acmeCo/arabic-source-hello-world.flow.yaml +++ /dev/null @@ -1,15 +0,0 @@ -collections: - acmeCo/arabic-greetings: - schema: greetings.schema.yaml - key: [/ts] -captures: - acmeCo/arabic-source-hello-world: - endpoint: - connector: - image: ghcr.io/estuary/source-hello-world:dev - config: source-hello-world.config.yaml - bindings: - - resource: - name: greetings - prefix: 'مرحبا #{}' - target: acmeCo/arabic-greetings diff --git a/test/acmeCo/greetings.schema.yaml b/test/acmeCo/greetings.schema.yaml deleted file mode 100644 index 2c3ab38..0000000 --- a/test/acmeCo/greetings.schema.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: object -properties: - ts: { type: string, format: date-time } - message: { type: string } -required: [ts, message] diff --git a/test/acmeCo/source-hello-world.config.yaml b/test/acmeCo/source-hello-world.config.yaml deleted file mode 100644 index 8796d8b..0000000 --- a/test/acmeCo/source-hello-world.config.yaml +++ /dev/null @@ -1 +0,0 @@ -rate: 100 diff --git a/test/acmeCo/source-hello-world.flow.yaml b/test/acmeCo/source-hello-world.flow.yaml deleted file mode 100644 index 2fb6515..0000000 --- a/test/acmeCo/source-hello-world.flow.yaml +++ /dev/null @@ -1,15 +0,0 @@ -collections: - acmeCo/greetings: - schema: greetings.schema.yaml - key: [/ts] -captures: - acmeCo/source-hello-world: - endpoint: - connector: - image: ghcr.io/estuary/source-hello-world:dev - config: source-hello-world.config.yaml - bindings: - - resource: - name: greetings - prefix: 'Hello #{}' - target: acmeCo/greetings