diff --git a/.github/workflows/federation-v1.workflow.yaml b/.github/workflows/federation-v1.workflow.yaml index 33552446..39ca1288 100644 --- a/.github/workflows/federation-v1.workflow.yaml +++ b/.github/workflows/federation-v1.workflow.yaml @@ -34,6 +34,7 @@ jobs: - cosmo - mercurius - grafbase + - inigo uses: ./.github/workflows/benchmark.template.yaml with: gateway: ${{ matrix.directory }} @@ -71,6 +72,7 @@ jobs: - cosmo - mercurius - grafbase + - inigo uses: ./.github/workflows/benchmark.template.yaml with: gateway: ${{ matrix.directory }} @@ -109,6 +111,7 @@ jobs: - cosmo - mercurius - grafbase + - inigo uses: ./.github/workflows/benchmark.template.yaml with: gateway: ${{ matrix.directory }} @@ -147,6 +150,7 @@ jobs: - cosmo - mercurius - grafbase + - inigo uses: ./.github/workflows/benchmark.template.yaml with: gateway: ${{ matrix.directory }} diff --git a/federation-v1/gateways/inigo/Dockerfile b/federation-v1/gateways/inigo/Dockerfile new file mode 100644 index 00000000..eafe17ea --- /dev/null +++ b/federation-v1/gateways/inigo/Dockerfile @@ -0,0 +1,14 @@ +# we need curl to perform health checks +# and that's why we start with alpine image +# and copy the gateway binary from the original docker image +FROM alpine:3.19 +RUN apk add --no-cache curl + +COPY --from=inigohub/gateway:v0.30.15 /usr/bin/gateway . + +COPY supergraph.graphql ./ +COPY config.yaml ./ + +EXPOSE 4000 + +CMD ["./gateway", "--schema", "supergraph.graphql", "--config", "config.yaml"] diff --git a/federation-v1/gateways/inigo/config.yaml b/federation-v1/gateways/inigo/config.yaml new file mode 100644 index 00000000..543be13e --- /dev/null +++ b/federation-v1/gateways/inigo/config.yaml @@ -0,0 +1 @@ +listen_port: 4000 \ No newline at end of file diff --git a/federation-v1/gateways/inigo/docker-compose.yaml b/federation-v1/gateways/inigo/docker-compose.yaml new file mode 100644 index 00000000..cda86655 --- /dev/null +++ b/federation-v1/gateways/inigo/docker-compose.yaml @@ -0,0 +1,35 @@ +version: "3.8" + +services: + gateway: + image: gateway/inigo + container_name: gateway + build: + context: ${BASE_DIR:-.}/../../gateways/inigo + dockerfile: ./Dockerfile + networks: + - test + ports: + - "0.0.0.0:4000:4000" + depends_on: + accounts: + condition: service_healthy + inventory: + condition: service_healthy + products: + condition: service_healthy + reviews: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "-X", "GET", "http://localhost:4000/health"] + interval: 3s + timeout: 5s + retries: 10 + deploy: + resources: + limits: + cpus: ${CPU_LIMIT:-1} + memory: ${MEM_LIMIT:-1gb} +networks: + test: + name: test diff --git a/federation-v1/gateways/inigo/supergraph.graphql b/federation-v1/gateways/inigo/supergraph.graphql new file mode 100644 index 00000000..43dc2879 --- /dev/null +++ b/federation-v1/gateways/inigo/supergraph.graphql @@ -0,0 +1,103 @@ +# Generated by Inigo CLI + +schema + @link(for: EXECUTION, url: "https://specs.apollo.dev/join/v0.2") + @link(url: "https://specs.apollo.dev/link/v1.0") { + query: Query +} + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://inventory:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql") +} +enum link__Purpose { + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY +} + +type Product + @join__type(extension: true, graph: INVENTORY, key: "upc") + @join__type(extension: true, graph: REVIEWS, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") { + inStock: Boolean @join__field(graph: INVENTORY) + name: String @join__field(graph: PRODUCTS) + price: Int + @join__field(external: true, graph: INVENTORY) + @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! + weight: Int + @join__field(external: true, graph: INVENTORY) + @join__field(graph: PRODUCTS) +} +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) + user(id: ID!): User @join__field(graph: ACCOUNTS) + users: [User] @join__field(graph: ACCOUNTS) +} +type Review @join__type(graph: REVIEWS, key: "id") { + author: User @join__field(graph: REVIEWS, provides: "username") + body: String + id: ID! + product: Product +} +type User + @join__type(extension: true, graph: REVIEWS, key: "id") + @join__type(graph: ACCOUNTS, key: "id") { + birthday: Int @join__field(graph: ACCOUNTS) + id: ID! + name: String @join__field(graph: ACCOUNTS) + reviews: [Review] @join__field(graph: REVIEWS) + username: String + @join__field(external: true, graph: REVIEWS) + @join__field(graph: ACCOUNTS) +} + +directive @join__field( + external: Boolean + graph: join__Graph! + override: String + provides: join__FieldSet + requires: join__FieldSet + type: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + extension: Boolean! = false + graph: join__Graph! + key: join__FieldSet + resolvable: Boolean! = true +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @link( + as: String + for: link__Purpose + import: [link__Import] + url: String +) repeatable on SCHEMA diff --git a/federation/gateways/grafbase/Dockerfile b/federation/gateways/grafbase/Dockerfile index a68528da..1b66c056 100644 --- a/federation/gateways/grafbase/Dockerfile +++ b/federation/gateways/grafbase/Dockerfile @@ -1,7 +1,8 @@ FROM ghcr.io/grafbase/gateway:0.30.2 COPY supergraph.graphql ./ +COPY grafbase.toml ./ EXPOSE 4000 -CMD ["--schema", "supergraph.graphql", "--listen-address", "0.0.0.0:4000"] +CMD ["--schema", "supergraph.graphql", "--listen-address", "0.0.0.0:4000", "-c", "grafbase.toml"] diff --git a/federation/gateways/grafbase/grafbase.toml b/federation/gateways/grafbase/grafbase.toml new file mode 100644 index 00000000..be2f2d38 --- /dev/null +++ b/federation/gateways/grafbase/grafbase.toml @@ -0,0 +1,2 @@ + +request_body_limit = "2MiB" diff --git a/federation/gateways/hive-gateway/docker-compose.yaml b/federation/gateways/hive-gateway/docker-compose.yaml index cc7d15e9..e6a802d5 100644 --- a/federation/gateways/hive-gateway/docker-compose.yaml +++ b/federation/gateways/hive-gateway/docker-compose.yaml @@ -24,11 +24,18 @@ services: source: federation/gateways/hive-gateway/supergraph.graphql target: /serve/supergraph.graphql healthcheck: - test: [ "CMD", "/usr/lib/apt/apt-helper", "download-file", "http://127.0.0.1:4000/graphql?query=%7B+users+%7B+reviews+%7B+product+%7B+reviews+%7B+author+%7B+reviews+%7B+product+%7B+name+%7D+%7D+%7D+%7D+%7D+%7D+%7D+%7D", "/tmp/health" ] + test: + [ + "CMD", + "/usr/lib/apt/apt-helper", + "download-file", + "http://127.0.0.1:4000/graphql?query=%7B+users+%7B+reviews+%7B+product+%7B+reviews+%7B+author+%7B+reviews+%7B+product+%7B+name+%7D+%7D+%7D+%7D+%7D+%7D+%7D+%7D", + "/tmp/health", + ] interval: 3s timeout: 5s retries: 10 - command: [ "supergraph", "--jit" ] + command: ["supergraph", "--jit"] deploy: resources: limits: diff --git a/federation/gateways/inigo/Dockerfile b/federation/gateways/inigo/Dockerfile new file mode 100644 index 00000000..162a1318 --- /dev/null +++ b/federation/gateways/inigo/Dockerfile @@ -0,0 +1,14 @@ +# we need curl to perform health checks +# and that's why we start with alpine image +# and copy the gateway binary from the original docker image +FROM alpine:3.19 +RUN apk add --no-cache curl + +COPY --from=inigohub/gateway:1.1.0 /usr/bin/gateway . + +COPY supergraph.graphql ./ +COPY config.yaml ./ + +EXPOSE 4000 + +CMD ["./gateway", "--schema", "supergraph.graphql", "--config", "config.yaml"] \ No newline at end of file diff --git a/federation/gateways/inigo/config.yaml b/federation/gateways/inigo/config.yaml new file mode 100644 index 00000000..543be13e --- /dev/null +++ b/federation/gateways/inigo/config.yaml @@ -0,0 +1 @@ +listen_port: 4000 \ No newline at end of file diff --git a/federation/gateways/inigo/docker-compose.yaml b/federation/gateways/inigo/docker-compose.yaml new file mode 100644 index 00000000..41d95816 --- /dev/null +++ b/federation/gateways/inigo/docker-compose.yaml @@ -0,0 +1,35 @@ +version: "3.8" + +services: + gateway: + image: gateway/inigo + container_name: gateway + build: + context: ${BASE_DIR:-.}/../../gateways/inigo + dockerfile: ./Dockerfile + networks: + - test + ports: + - "0.0.0.0:4000:4000" + depends_on: + accounts: + condition: service_healthy + inventory: + condition: service_healthy + products: + condition: service_healthy + reviews: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "-X", "GET", "http://localhost:4000/health"] + interval: 3s + timeout: 5s + retries: 10 + deploy: + resources: + limits: + cpus: ${CPU_LIMIT:-1} + memory: ${MEM_LIMIT:-1gb} +networks: + test: + name: test \ No newline at end of file diff --git a/federation/gateways/inigo/supergraph.graphql b/federation/gateways/inigo/supergraph.graphql new file mode 100644 index 00000000..43dc2879 --- /dev/null +++ b/federation/gateways/inigo/supergraph.graphql @@ -0,0 +1,103 @@ +# Generated by Inigo CLI + +schema + @link(for: EXECUTION, url: "https://specs.apollo.dev/join/v0.2") + @link(url: "https://specs.apollo.dev/link/v1.0") { + query: Query +} + +scalar join__FieldSet +scalar link__Import + +enum join__Graph { + ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql") + INVENTORY + @join__graph(name: "inventory", url: "http://inventory:4002/graphql") + PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql") + REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql") +} +enum link__Purpose { + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY +} + +type Product + @join__type(extension: true, graph: INVENTORY, key: "upc") + @join__type(extension: true, graph: REVIEWS, key: "upc") + @join__type(graph: PRODUCTS, key: "upc") { + inStock: Boolean @join__field(graph: INVENTORY) + name: String @join__field(graph: PRODUCTS) + price: Int + @join__field(external: true, graph: INVENTORY) + @join__field(graph: PRODUCTS) + reviews: [Review] @join__field(graph: REVIEWS) + shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight") + upc: String! + weight: Int + @join__field(external: true, graph: INVENTORY) + @join__field(graph: PRODUCTS) +} +type Query + @join__type(graph: ACCOUNTS) + @join__type(graph: INVENTORY) + @join__type(graph: PRODUCTS) + @join__type(graph: REVIEWS) { + me: User @join__field(graph: ACCOUNTS) + topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS) + user(id: ID!): User @join__field(graph: ACCOUNTS) + users: [User] @join__field(graph: ACCOUNTS) +} +type Review @join__type(graph: REVIEWS, key: "id") { + author: User @join__field(graph: REVIEWS, provides: "username") + body: String + id: ID! + product: Product +} +type User + @join__type(extension: true, graph: REVIEWS, key: "id") + @join__type(graph: ACCOUNTS, key: "id") { + birthday: Int @join__field(graph: ACCOUNTS) + id: ID! + name: String @join__field(graph: ACCOUNTS) + reviews: [Review] @join__field(graph: REVIEWS) + username: String + @join__field(external: true, graph: REVIEWS) + @join__field(graph: ACCOUNTS) +} + +directive @join__field( + external: Boolean + graph: join__Graph! + override: String + provides: join__FieldSet + requires: join__FieldSet + type: String + usedOverridden: Boolean +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + extension: Boolean! = false + graph: join__Graph! + key: join__FieldSet + resolvable: Boolean! = true +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @link( + as: String + for: link__Purpose + import: [link__Import] + url: String +) repeatable on SCHEMA diff --git a/federation/scenarios/constant-vus-over-time/generate-report.ts b/federation/scenarios/constant-vus-over-time/generate-report.ts index f1a6607d..a8005638 100644 --- a/federation/scenarios/constant-vus-over-time/generate-report.ts +++ b/federation/scenarios/constant-vus-over-time/generate-report.ts @@ -217,6 +217,27 @@ async function generateReport(artifactsRootPath: string) { (c) => c.name === "valid response structure" ); + function logRawReport() { + console.log("Raw report for:", v.name); + console.log("--"); + console.log(JSON.stringify(checks, null, 2)); + console.log("--"); + } + + if (!http200Check) { + logRawReport(); + throw new Error("Could not find 'response code was 200' check!"); + } + + if (!graphqlErrors) { + logRawReport(); + throw new Error("Could not find 'no graphql errors' check!"); + } + + if (!responseStructure) { + logRawReport(); + throw new Error("Could not find 'valid response structure' check!"); + } if (http200Check.fails > 0) { notes.push(`${http200Check.fails} non-200 responses`);