From 7609fae12ebc3f284ca2661a2eb3042053f16981 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Mon, 24 Jun 2019 13:22:42 -0700 Subject: [PATCH 01/27] Add polling support --- packages/apollo-gateway/src/index.ts | 46 ++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 0e43515ed46..7682b54bb5a 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -40,6 +40,7 @@ export interface GatewayConfigBase { __exposeQueryPlanExperimental?: boolean; buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; serviceList?: ServiceEndpointDefinition[]; + onSchemaChange?: SchemaChangeCallback; } export interface LocalGatewayConfig extends GatewayConfigBase { @@ -52,6 +53,11 @@ function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { return 'localServiceList' in config; } +export type SchemaChangeCallback = (options: { + schema: GraphQLSchema; + executor: GraphQLExecutor; +}) => void; + export class ApolloGateway implements GraphQLService { public schema?: GraphQLSchema; public isReady: boolean = false; @@ -59,6 +65,7 @@ export class ApolloGateway implements GraphQLService { protected config: GatewayConfig; protected logger: Logger; protected queryPlanStore?: InMemoryLRUCache; + private pollingTimer?: NodeJS.Timer; constructor(config: GatewayConfig) { this.config = { @@ -85,6 +92,13 @@ export class ApolloGateway implements GraphQLService { } this.initializeQueryPlanStore(); + + if (config.onSchemaChange) { + if (!isLocalConfig(this.config)) { + this.logger.debug('Starting polling for schema changes'); + this.startPollingServices(); + } + } } public async load() { @@ -124,18 +138,37 @@ export class ApolloGateway implements GraphQLService { this.isReady = true; } + private startPollingServices() { + this.pollingTimer = setInterval(async () => { + const [services, isNewSchema] = await this.loadServiceDefinitions( + this.config, + ); + if (!isNewSchema) { + this.logger.debug('No changes to gateway config'); + return; + } + if (this.queryPlanStore) this.queryPlanStore.flush(); + this.logger.debug('Gateway config has changed, updating schema'); + this.createSchema(services); + this.config.onSchemaChange!({ + schema: this.schema!, + executor: this.executor, + }); + }, 10 * 1000); + } + protected createServices(services: ServiceEndpointDefinition[]) { for (const serviceDef of services) { if (!serviceDef.url && !isLocalConfig(this.config)) { throw new Error( - `Service defintion for service ${serviceDef.name} is missing a url`, + `Service definition for service ${serviceDef.name} is missing a url`, ); } this.serviceMap[serviceDef.name] = this.config.buildService ? this.config.buildService(serviceDef) : new RemoteGraphQLDataSource({ - url: serviceDef.url, - }); + url: serviceDef.url, + }); } } @@ -234,6 +267,13 @@ export class ApolloGateway implements GraphQLService { sizeCalculator: approximateObjectSize, }); } + + public async stop() { + if (this.pollingTimer) { + clearInterval(this.pollingTimer); + this.pollingTimer = undefined; + } + } } function approximateObjectSize(obj: T): number { From 03c97402a5b3dfc6b42128c155af860b6c2912cc Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Mon, 24 Jun 2019 13:36:36 -0700 Subject: [PATCH 02/27] Add remote hosted endpoint support (mainly directly copied from enterprise gateway) --- package-lock.json | 93 ++++++++++++ package.json | 1 + .../integration/networkRequests.test.ts | 36 +++++ .../src/__tests__/integration/nockMocks.ts | 114 ++++++++++++++ packages/apollo-gateway/src/index.ts | 65 +++++--- .../src/loadServicesFromStorage.ts | 139 ++++++++++++++++++ .../apollo-gateway/src/utilities/createSHA.ts | 10 ++ .../src/utilities/isNodeLike.ts | 11 ++ 8 files changed, 451 insertions(+), 18 deletions(-) create mode 100644 packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts create mode 100644 packages/apollo-gateway/src/__tests__/integration/nockMocks.ts create mode 100644 packages/apollo-gateway/src/loadServicesFromStorage.ts create mode 100644 packages/apollo-gateway/src/utilities/createSHA.ts create mode 100644 packages/apollo-gateway/src/utilities/isNodeLike.ts diff --git a/package-lock.json b/package-lock.json index 41f3834e8ea..f628ca60b2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3935,6 +3935,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -4517,6 +4523,20 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.0.tgz", @@ -4533,6 +4553,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "chownr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", @@ -5298,6 +5324,15 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", @@ -7198,6 +7233,12 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-own-enumerable-property-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", @@ -12098,6 +12139,40 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "node-fetch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", @@ -12835,6 +12910,12 @@ "pify": "^3.0.0" } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -13067,6 +13148,12 @@ "read": "1" } }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, "property-expr": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-1.5.1.tgz", @@ -14834,6 +14921,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz", diff --git a/package.json b/package.json index d26f27b40b0..912acf70141 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "meteor-promise": "0.8.7", "mock-req": "0.2.0", "multer": "1.4.1", + "nock": "^10.0.6", "node-fetch": "2.3.0", "prettier": "1.17.1", "prettier-check": "2.0.0", diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts new file mode 100644 index 00000000000..5f4571ff2cf --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts @@ -0,0 +1,36 @@ +import nock from 'nock'; +import { createGateway } from '../..'; + +import { + mockGetRawPartialSchema, + mockFetchStorageSecret, + mockGetCompositionConfigLink, + mockGetCompositionConfigs, + mockGetImplementingServices, + mockLocalhostSDLQuery, +} from './nockMocks'; + +it('Queries remote endpoints for their SDLs', async () => { + mockLocalhostSDLQuery(); + + await createGateway({ + serviceList: [{ name: 'accounts', url: 'http://localhost:4001/graphql' }], + }); + + expect(nock.isDone()).toBeTruthy(); +}); + +// This test is maybe a bit terrible, but IDK a better way to mock all the requests +it('Extracts service definitions from remote storage', async () => { + mockFetchStorageSecret(); + mockGetCompositionConfigLink(); + mockGetCompositionConfigs(); + mockGetImplementingServices(); + mockGetRawPartialSchema(); + + await createGateway({ + apiKey: 'service:mdg-private-6077:EgWp3sa01FhGuMJSKIfMVQ', + }); + + expect(nock.isDone()).toBeTruthy(); +}); \ No newline at end of file diff --git a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts new file mode 100644 index 00000000000..9746547347c --- /dev/null +++ b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts @@ -0,0 +1,114 @@ +import nock from 'nock'; + +export const mockLocalhostSDLQuery = () => + nock('http://localhost:4001', { encodedQueryParams: true }) + .post('/graphql', { + query: 'query GetServiceDefinition { _service { sdl } }', + }) + .reply( + 200, + { + data: { + _service: { + sdl: + 'extend type Query {\n me: User\n everyone: [User]\n}\n\ntype User @key(fields: "id") {\n id: ID!\n name: String\n username: String\n}\n', + }, + }, + }, + { + 'Content-Type': 'application/json', + }, + ); + +export const mockFetchStorageSecret = () => + nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) + .get( + '/engine-partial-schema-prod/mdg-private-6077/storage-secret/232d3a78a4392a756d37613458805dd830b517453729dc4945c24ca4b575cfc270b08e763cea5a8da49c784b31272d1c79b1d598cdc6e4d6913700a5505cd2f7.json', + ) + .reply( + 200, + [ + '1f', + '8b', + '08', + '00', + '00', + '00', + '000000035332334831b434b734d43548b434d53531324ad44d3437b3d04d4e4e3435333348b34c4c365702008b81631826000000', + ], + { + 'Content-Type': 'application/octet-stream', + 'Content-Encoding': 'gzip', + }, + ); + +// get composition config link, using received storage secret +export const mockGetCompositionConfigLink = () => + nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) + .get( + '/engine-partial-schema-prod/60d19791-0a95-422a-a768-cca5660f9ac7/current/v1/composition-config-link', + ) + .reply( + 200, + [ + '1f', + '8b', + '08', + '0000000000000325cccb0d02211000d05e383bf21106d82a3c791f6741315930805e8cbdbb89af80f711dc6a2eb733cdbb5804aa55471f35288a0eac3104e431003339449523b197fcea3dd529df5a72db9e6d94595a857f34a43348579503f813ee876684603d815b4352c94467391f1fa3557110b9f58de625f5b10f62d1df1fc566aa6190000000', + ], + { + 'Content-Type': 'application/json', + 'Content-Encoding': 'gzip', + }, + ); + +// get composition configs, using received composition config link +export const mockGetCompositionConfigs = () => + nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) + .get( + '/engine-partial-schema-prod/60d19791-0a95-422a-a768-cca5660f9ac7/current/v1/composition-configs/526ab0f8-7365-41c6-847a-5d8e0e2954cf.json', + ) + .reply( + 200, + [ + '1f8b08000000000000034d90b16e1c310c44ff45b5d74b521225dd17a44867204d9082a2c8f882ec9eb17b7663f8dfa324089096e0cc9b99f7e0b76393fb173bceeb6d0f177c08d7112e21134b07af4b899c9784ca4b4d45963caa8151cb493dccdfede5a76db6dfaffbf7273bdeae6a9f6f2af7e97586cbd7f7b0cb66d34e546faffbfd9c9217b93fcf0bc3c0561a2e206d02886491c2755195cc0cde44cbaaafc731cdd7375cff272de75fd4b9fef35d4b14e251910bc694dcab756a92e3a8ad7bd64a390fc5d4a289b30b38f634d0005a01ee4263986949997b823ef92c80031a241d9db2387a258ad63c9177058796236365cd02d6011f7f9c73be8f6f0fe1d467dbe4939cbf5b6a6632191d39c642313115436e55b520688b831be706b3784f9e51b37713ced68620c90cdc63751932d5510169823512cef1726c5838691ec9e78449128d3f199dac51d3e415ba848f5f8438ec47e2010000', + ], + { + 'Content-Type': 'application/octet-stream', + 'Content-Encoding': 'gzip', + }, + ); + +// get implementing service reference, using received composition-config +export const mockGetImplementingServices = () => + nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) + .get( + '/engine-partial-schema-prod/60d19791-0a95-422a-a768-cca5660f9ac7/current/v1/implementing-services/accounts/73a26d81671344ff8eb29a53d89bf5c8255dc1493eaf6fa0f1b4d1e009706ba2ddeec7456b40b6606a01d0904cdb25af1f8223e9f42fbc0f09536186c5a0eb01.json', + ) + .reply( + 200, + [ + '1f8b08000000000000032d90bb6ec3300c45ff45730c536f2973976e050a642725b136e01764251d8afe7bd5361b710fef01c82fc17b5db1dd4a3de77d135779111f158fe9f5455cc59a3f86a3ce0f6c6570e0bd78c21bd619b7d637d2bdd6d2a78bd8702d3dc094f6fbd6ce9e1c58db8ccb7b9aca8a6fd8a68e1d64197d940360b483510a07f42e0c29a1750e3862f2e3533a56fc1c9e92e1fcb39ca3f2402918a7d9446f0c98643c922d96c979e6eca256a4341a451a8882b221781d038728ad2607d25291cae6a894e29c0299c84912ab64bdd63143e71e14b0b42e6196d25896a4bd72e4289a7e572d8ff9ff590280ad94110c1449c04c6831b352803658f4143449a6fcdbbad7a517a6d68eeb382e7bc265dacf763500527cff00fb367ce587010000', + ], + { + 'Content-Type': 'application/octet-stream', + 'Content-Encoding': 'gzip', + }, + ); + +// get raw-partial-schema, using received composition-config +export const mockGetRawPartialSchema = () => + nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) + .get( + '/engine-partial-schema-prod/60d19791-0a95-422a-a768-cca5660f9ac7/current/raw-partial-schemas/270bc8463f4974404c47ab5e5fb67ffd6932b23a42b30bb825887398f89153b6015be125d9222fdc8b49fc1bf2c57339d05be7020f156cad1145f1b3726b6b94', + ) + .reply( + 200, + [ + '1f8b08000000000000034bad2849cd4b5128a92c4855082c4d2daa54a8e65250c84db552082d4e2d023253cb8082f97940816890482c572d1717583588a7e0909d5aa99196999a93526ca5a09499a2a409d69f9962a5e0e9a20864e52582cc0a2e29cacc4b07724b819a50846ab90003647a4182000000', + ], + { + 'Content-Type': 'application/octet-stream', + 'Content-Encoding': 'gzip', + }, + ); \ No newline at end of file diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 0e43515ed46..e8a9d74ebc8 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -19,6 +19,7 @@ import { } from './executeQueryPlan'; import { getServiceDefinitionsFromRemoteEndpoint } from './loadServicesFromRemoteEndpoint'; +import { getServiceDefinitionsFromStorage } from './loadServicesFromStorage'; import { serializeQueryPlan, QueryPlan } from './QueryPlan'; import { GraphQLDataSource } from './datasources/types'; @@ -32,26 +33,47 @@ export interface GraphQLService { export type ServiceEndpointDefinition = Pick; -export interface GatewayConfigBase { +interface GatewayConfigBase { debug?: boolean; // TODO: expose the query plan in a more flexible JSON format in the future // and remove this config option in favor of `exposeQueryPlan`. Playground // should cutover to use the new option when it's built. __exposeQueryPlanExperimental?: boolean; buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; - serviceList?: ServiceEndpointDefinition[]; } +export interface ConcreteGatewayConfig extends GatewayConfigBase { + serviceList: ServiceEndpointDefinition[]; +} + +export interface HostedGatewayConfig extends GatewayConfigBase { + apiKey?: string; + tag?: string; + federationVersion?: number; +} export interface LocalGatewayConfig extends GatewayConfigBase { localServiceList: ServiceDefinition[]; } -export type GatewayConfig = GatewayConfigBase | LocalGatewayConfig; +export type GatewayConfig = + | ConcreteGatewayConfig + | LocalGatewayConfig + | HostedGatewayConfig; function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { return 'localServiceList' in config; } +function isHostedConfig(config: GatewayConfig): config is HostedGatewayConfig { + return !(isLocalConfig(config) || isConcreteConfig(config)); +} + +function isConcreteConfig( + config: GatewayConfig, +): config is ConcreteGatewayConfig { + return 'serviceList' in config; +} + export class ApolloGateway implements GraphQLService { public schema?: GraphQLSchema; public isReady: boolean = false; @@ -84,6 +106,15 @@ export class ApolloGateway implements GraphQLService { this.createSchema(config.localServiceList); } + if (isHostedConfig(config)) { + const apiKey = config.apiKey || process.env['ENGINE_API_KEY']; + if (!apiKey) { + throw new Error( + 'The gateway requires either a serviceList, localServiceList, or apiKey to be provided in the config, or ENGINE_API_KEY to be defined in the environment', + ); + } + } + this.initializeQueryPlanStore(); } @@ -101,7 +132,7 @@ export class ApolloGateway implements GraphQLService { protected createSchema(services: ServiceDefinition[]) { this.logger.debug( `Composing schema from service list: \n${services - .map(({ name }) => ` ${name}`) + .map(({ name, url }) => ` ${url || 'local'} : ${name}`) .join('\n')}`, ); @@ -134,8 +165,8 @@ export class ApolloGateway implements GraphQLService { this.serviceMap[serviceDef.name] = this.config.buildService ? this.config.buildService(serviceDef) : new RemoteGraphQLDataSource({ - url: serviceDef.url, - }); + url: serviceDef.url, + }); } } @@ -143,20 +174,18 @@ export class ApolloGateway implements GraphQLService { config: GatewayConfig, ): Promise<[ServiceDefinition[], boolean]> { if (isLocalConfig(config)) return [config.localServiceList, false]; - if (!config.serviceList) - throw new Error( - 'The gateway requires a service list to be provided in the config', - ); - - const [ - remoteServices, - isNewService, - ] = await getServiceDefinitionsFromRemoteEndpoint({ - serviceList: config.serviceList, - }); - this.createServices(remoteServices); + const [remoteServices, isNewService] = isConcreteConfig(config) + ? await getServiceDefinitionsFromRemoteEndpoint({ + serviceList: config.serviceList, + }) + : await getServiceDefinitionsFromStorage({ + apiKey: config.apiKey || process.env['ENGINE_API_KEY']!, + graphVariant: config.tag || 'current', + federationVersion: config.federationVersion!, + }); + this.createServices(remoteServices); return [remoteServices, isNewService]; } diff --git a/packages/apollo-gateway/src/loadServicesFromStorage.ts b/packages/apollo-gateway/src/loadServicesFromStorage.ts new file mode 100644 index 00000000000..5d885e6d4a7 --- /dev/null +++ b/packages/apollo-gateway/src/loadServicesFromStorage.ts @@ -0,0 +1,139 @@ +import { CachedFetcher } from './cachedFetcher'; +import { ServiceDefinition } from '@apollo/federation'; +import { parse } from 'graphql'; +import createSHA from './utilities/createSHA'; + +export interface LinkFileResult { + configPath: string; + formatVersion: number; +} + +export interface ImplementingService { + formatVersion: number; + graphID: string; + graphVariant: string; + name: string; + revision: string; + url: string; + partialSchemaPath: string; +} + +export interface ImplementingServiceLocation { + name: string; + path: string; +} + +export interface ConfigFileResult { + formatVersion: number; + id: string; + implementingServiceLocations: ImplementingServiceLocation[]; + schemaHash: string; +} + +export const envOverrideOperationManifest = 'APOLLO_PARTIAL_SCHEMA_BASE_URL'; +export const envOverrideStorageSecretBaseUrl = 'APOLLO_STORAGE_SECRET_BASE_URL'; + +const urlFromEnvOrDefault = (envKey: string, fallback: string) => + (process.env[envKey] || fallback).replace(/\/$/, ''); + +// Generate and cache our desired operation manifest URL. +const urlPartialSchemaBase = urlFromEnvOrDefault( + envOverrideOperationManifest, + 'https://storage.googleapis.com/engine-partial-schema-prod/', +); + +export const urlStorageSecretBase: string = urlFromEnvOrDefault( + envOverrideStorageSecretBaseUrl, + 'https://storage.googleapis.com/engine-partial-schema-prod/', +); + +const fetcher = new CachedFetcher(); +let serviceDefinitionList: ServiceDefinition[] = []; + +function getStorageSecretUrl(apiKey: string): string { + const graphId = apiKey.split(':', 2)[1]; + const apiKeyHash = createSHA('sha512') + .update(apiKey) + .digest('hex'); + return `${urlStorageSecretBase}/${graphId}/storage-secret/${apiKeyHash}.json`; +} + +async function fetchStorageSecret(apiKey: string): Promise { + const storageSecretUrl = getStorageSecretUrl(apiKey); + const response = await fetcher.fetch(storageSecretUrl); + return JSON.parse(response.result); +} + +export async function getServiceDefinitionsFromStorage({ + apiKey, + graphVariant, + federationVersion = 1, +}: { + apiKey: string; + graphVariant: string; + federationVersion: number; +}): Promise<[ServiceDefinition[], boolean]> { + const secret = await fetchStorageSecret(apiKey); + + if (!graphVariant) { + console.warn('No graphVariant specified, defaulting to "current".'); + graphVariant = 'current'; + } + + const baseUrl = `${urlPartialSchemaBase}/${secret}/${graphVariant}/v${federationVersion}`; + + const { + isCacheHit: linkFileCacheHit, + result: linkFileResult, + } = await fetchLinkFile(baseUrl); + + // If the link file is a cache hit, no further work is needed + if (linkFileCacheHit) return [serviceDefinitionList, false]; + + const parsedLink = JSON.parse(linkFileResult) as LinkFileResult; + + const { result: configFileResult } = await fetcher.fetch( + `${urlPartialSchemaBase}/${parsedLink.configPath}`, + ); + + const parsedConfig = JSON.parse(configFileResult) as ConfigFileResult; + return fetchPartialSchemaFiles(parsedConfig.implementingServiceLocations); +} + +async function fetchLinkFile(baseUrl: string) { + return fetcher.fetch(`${baseUrl}/composition-config-link`); +} + +// The order of implementingServices is IMPORTANT +async function fetchPartialSchemaFiles( + implementingServices: ImplementingServiceLocation[], +): Promise<[ServiceDefinition[], boolean]> { + let isDirty = false; + const fetchPartialSchemasPromises = implementingServices.map( + async ({ name, path }) => { + const serviceLocation = await fetcher.fetch( + `${urlPartialSchemaBase}/${path}`, + ); + + const { url, partialSchemaPath } = JSON.parse( + serviceLocation.result, + ) as ImplementingService; + + const { isCacheHit, result } = await fetcher.fetch( + `${urlPartialSchemaBase}/${partialSchemaPath}`, + ); + + // Cache miss === dirty service, will need to be recomposed + if (!isCacheHit) { + isDirty = true; + } + + return { name, url, typeDefs: parse(result) }; + }, + ); + + // Respect the order here + const services = await Promise.all(fetchPartialSchemasPromises); + + return [services, isDirty]; +} \ No newline at end of file diff --git a/packages/apollo-gateway/src/utilities/createSHA.ts b/packages/apollo-gateway/src/utilities/createSHA.ts new file mode 100644 index 00000000000..d37d83519aa --- /dev/null +++ b/packages/apollo-gateway/src/utilities/createSHA.ts @@ -0,0 +1,10 @@ +import isNodeLike from './isNodeLike'; + +export default function (kind: string): import('crypto').Hash { + if (isNodeLike) { + // Use module.require instead of just require to avoid bundling whatever + // crypto polyfills a non-Node bundler might fall back to. + return module.require('crypto').createHash(kind); + } + return require('sha.js')(kind); +} diff --git a/packages/apollo-gateway/src/utilities/isNodeLike.ts b/packages/apollo-gateway/src/utilities/isNodeLike.ts new file mode 100644 index 00000000000..e5fa3a2221b --- /dev/null +++ b/packages/apollo-gateway/src/utilities/isNodeLike.ts @@ -0,0 +1,11 @@ +export default typeof process === 'object' && + process && + // We used to check `process.release.name === "node"`, however that doesn't + // account for certain forks of Node.js which are otherwise identical to + // Node.js. For example, NodeSource's N|Solid reports itself as "nsolid", + // though it's mostly the same build of Node.js with an extra addon. + process.release && + process.versions && + // The one thing which is present on both Node.js and N|Solid (a fork of + // Node.js), is `process.versions.node` being defined. + typeof process.versions.node === 'string'; From c6513dea035b6b1c29d4d304351e04cdfd178217 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Mon, 24 Jun 2019 13:46:42 -0700 Subject: [PATCH 03/27] Lint --- packages/apollo-gateway/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 7682b54bb5a..6d9a2d3bb54 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -167,8 +167,8 @@ export class ApolloGateway implements GraphQLService { this.serviceMap[serviceDef.name] = this.config.buildService ? this.config.buildService(serviceDef) : new RemoteGraphQLDataSource({ - url: serviceDef.url, - }); + url: serviceDef.url, + }); } } From 72f866a027c16929fa219b631f45bc20f769975c Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Mon, 24 Jun 2019 13:52:39 -0700 Subject: [PATCH 04/27] Lint --- .../integration/networkRequests.test.ts | 2 +- .../src/__tests__/integration/nockMocks.ts | 2 +- packages/apollo-gateway/src/index.ts | 16 ++++++++-------- .../src/loadServicesFromStorage.ts | 2 +- .../apollo-gateway/src/utilities/createSHA.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts index 5f4571ff2cf..b8d4854b23f 100644 --- a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts +++ b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts @@ -33,4 +33,4 @@ it('Extracts service definitions from remote storage', async () => { }); expect(nock.isDone()).toBeTruthy(); -}); \ No newline at end of file +}); diff --git a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts index 9746547347c..f85d9c09726 100644 --- a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts +++ b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts @@ -111,4 +111,4 @@ export const mockGetRawPartialSchema = () => 'Content-Type': 'application/octet-stream', 'Content-Encoding': 'gzip', }, - ); \ No newline at end of file + ); diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index e8a9d74ebc8..920ecc0c6f4 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -165,8 +165,8 @@ export class ApolloGateway implements GraphQLService { this.serviceMap[serviceDef.name] = this.config.buildService ? this.config.buildService(serviceDef) : new RemoteGraphQLDataSource({ - url: serviceDef.url, - }); + url: serviceDef.url, + }); } } @@ -177,13 +177,13 @@ export class ApolloGateway implements GraphQLService { const [remoteServices, isNewService] = isConcreteConfig(config) ? await getServiceDefinitionsFromRemoteEndpoint({ - serviceList: config.serviceList, - }) + serviceList: config.serviceList, + }) : await getServiceDefinitionsFromStorage({ - apiKey: config.apiKey || process.env['ENGINE_API_KEY']!, - graphVariant: config.tag || 'current', - federationVersion: config.federationVersion!, - }); + apiKey: config.apiKey || process.env['ENGINE_API_KEY']!, + graphVariant: config.tag || 'current', + federationVersion: config.federationVersion!, + }); this.createServices(remoteServices); return [remoteServices, isNewService]; diff --git a/packages/apollo-gateway/src/loadServicesFromStorage.ts b/packages/apollo-gateway/src/loadServicesFromStorage.ts index 5d885e6d4a7..07ba76fabd2 100644 --- a/packages/apollo-gateway/src/loadServicesFromStorage.ts +++ b/packages/apollo-gateway/src/loadServicesFromStorage.ts @@ -136,4 +136,4 @@ async function fetchPartialSchemaFiles( const services = await Promise.all(fetchPartialSchemasPromises); return [services, isDirty]; -} \ No newline at end of file +} diff --git a/packages/apollo-gateway/src/utilities/createSHA.ts b/packages/apollo-gateway/src/utilities/createSHA.ts index d37d83519aa..d7362148969 100644 --- a/packages/apollo-gateway/src/utilities/createSHA.ts +++ b/packages/apollo-gateway/src/utilities/createSHA.ts @@ -1,6 +1,6 @@ import isNodeLike from './isNodeLike'; -export default function (kind: string): import('crypto').Hash { +export default function(kind: string): import('crypto').Hash { if (isNodeLike) { // Use module.require instead of just require to avoid bundling whatever // crypto polyfills a non-Node bundler might fall back to. From f52a803999e32246ed6b0f69e60ff32a0a2770db Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Mon, 24 Jun 2019 13:59:07 -0700 Subject: [PATCH 05/27] Oops cant use createGateway in this branch --- .../src/__tests__/integration/networkRequests.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts index b8d4854b23f..2ff4a55e9b9 100644 --- a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts +++ b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts @@ -1,5 +1,5 @@ import nock from 'nock'; -import { createGateway } from '../..'; +import { ApolloGateway } from '../..'; import { mockGetRawPartialSchema, @@ -13,10 +13,10 @@ import { it('Queries remote endpoints for their SDLs', async () => { mockLocalhostSDLQuery(); - await createGateway({ + let gateway = new ApolloGateway({ serviceList: [{ name: 'accounts', url: 'http://localhost:4001/graphql' }], }); - + await gateway.load(); expect(nock.isDone()).toBeTruthy(); }); @@ -28,9 +28,9 @@ it('Extracts service definitions from remote storage', async () => { mockGetImplementingServices(); mockGetRawPartialSchema(); - await createGateway({ + let gateway = new ApolloGateway({ apiKey: 'service:mdg-private-6077:EgWp3sa01FhGuMJSKIfMVQ', }); - + await gateway.load(); expect(nock.isDone()).toBeTruthy(); }); From 6e947db74f8a564dcff808994c0889e5de81d829 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Tue, 25 Jun 2019 11:18:55 -0700 Subject: [PATCH 06/27] Naming --- packages/apollo-gateway/src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 920ecc0c6f4..b7e05843c8c 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -42,11 +42,11 @@ interface GatewayConfigBase { buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; } -export interface ConcreteGatewayConfig extends GatewayConfigBase { +export interface RemoteGatewayConfig extends GatewayConfigBase { serviceList: ServiceEndpointDefinition[]; } -export interface HostedGatewayConfig extends GatewayConfigBase { +export interface ManagedGatewayConfig extends GatewayConfigBase { apiKey?: string; tag?: string; federationVersion?: number; @@ -56,21 +56,21 @@ export interface LocalGatewayConfig extends GatewayConfigBase { } export type GatewayConfig = - | ConcreteGatewayConfig + | RemoteGatewayConfig | LocalGatewayConfig - | HostedGatewayConfig; + | ManagedGatewayConfig; function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { return 'localServiceList' in config; } -function isHostedConfig(config: GatewayConfig): config is HostedGatewayConfig { +function isHostedConfig(config: GatewayConfig): config is ManagedGatewayConfig { return !(isLocalConfig(config) || isConcreteConfig(config)); } function isConcreteConfig( config: GatewayConfig, -): config is ConcreteGatewayConfig { +): config is RemoteGatewayConfig { return 'serviceList' in config; } From 823b246f5f247bf6a7686a59346d871886e30ae7 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Tue, 25 Jun 2019 11:43:01 -0700 Subject: [PATCH 07/27] Remove extrenous call to create services --- packages/apollo-gateway/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index b7e05843c8c..617f6f01b0a 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -185,7 +185,6 @@ export class ApolloGateway implements GraphQLService { federationVersion: config.federationVersion!, }); - this.createServices(remoteServices); return [remoteServices, isNewService]; } From 950d8e40c22f388bb742ebfc228a91c18ffa3d2a Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Tue, 25 Jun 2019 14:25:37 -0700 Subject: [PATCH 08/27] Make mock process more explicit --- package-lock.json | 9 + package.json | 1 + .../integration/networkRequests.test.ts | 71 +++++++- .../src/__tests__/integration/nockMocks.ts | 168 ++++++++---------- 4 files changed, 150 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index f628ca60b2c..972c2b04a4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2993,6 +2993,15 @@ "@types/express": "*" } }, + "@types/nock": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.1.3.tgz", + "integrity": "sha512-S8rJ+SaW82ICX87pZP62UcMifrMfjEdqNzSp+llx4YcvKw6bO650Ye6HwTqER1Dar3S40GIZECQisOrAICDCjA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "8.10.49", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.49.tgz", diff --git a/package.json b/package.json index 912acf70141..168101979b2 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@types/micro": "7.3.3", "@types/multer": "1.3.7", "@types/node": "8.10.49", + "@types/nock": "9.1.3", "@types/node-fetch": "2.3.2", "@types/request": "2.48.1", "@types/request-promise": "4.1.44", diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts index 2ff4a55e9b9..37c0601cc18 100644 --- a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts +++ b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts @@ -1,5 +1,6 @@ import nock from 'nock'; import { ApolloGateway } from '../..'; +import createSHA from '../../utilities/createSHA'; import { mockGetRawPartialSchema, @@ -11,10 +12,23 @@ import { } from './nockMocks'; it('Queries remote endpoints for their SDLs', async () => { - mockLocalhostSDLQuery(); + let url = 'localhost:4001'; + let sdl = `extend type Query { + me: User + everyone: [User] + } + + type User @key(fields: "id") { + id: ID! + name: String + username: String + } + `; + + mockLocalhostSDLQuery({ url, sdl }); let gateway = new ApolloGateway({ - serviceList: [{ name: 'accounts', url: 'http://localhost:4001/graphql' }], + serviceList: [{ name: 'accounts', url: 'http://localhost:4001/' }], }); await gateway.load(); expect(nock.isDone()).toBeTruthy(); @@ -22,15 +36,54 @@ it('Queries remote endpoints for their SDLs', async () => { // This test is maybe a bit terrible, but IDK a better way to mock all the requests it('Extracts service definitions from remote storage', async () => { - mockFetchStorageSecret(); - mockGetCompositionConfigLink(); - mockGetCompositionConfigs(); - mockGetImplementingServices(); - mockGetRawPartialSchema(); + let apiKey = 'service:jacksons-service:AABBCCDDEEFFGG'; + let apiKeyHash = createSHA('sha512') + .update(apiKey) + .digest('hex'); - let gateway = new ApolloGateway({ - apiKey: 'service:mdg-private-6077:EgWp3sa01FhGuMJSKIfMVQ', + let storageSecret = 'secret'; + let serviceName = 'jacksons-service'; + let implementingServicePath = 'path-to-implementing-service-definition.json'; + let partialSchemaPath = 'path-to-accounts-partial-schema.json'; + let federatedServiceName = 'accounts'; + let federatedServiceURL = 'http://localhost:4001'; + let federatedServiceSchema = ` + extend type Query { + me: User + everyone: [User] + } + + type User @key(fields: "id") { + id: ID! + name: String + username: String + }`; + + mockFetchStorageSecret({ apiKeyHash, storageSecret, serviceName }); + + mockGetCompositionConfigLink(storageSecret); + + mockGetCompositionConfigs({ + storageSecret, + implementingServicePath, + federatedServiceName, + }); + + mockGetImplementingServices({ + storageSecret, + implementingServicePath, + partialSchemaPath, + federatedServiceName, + federatedServiceURL, }); + + mockGetRawPartialSchema({ + storageSecret, + partialSchemaPath, + federatedServiceSchema, + }); + + let gateway = new ApolloGateway({ apiKey }); await gateway.load(); expect(nock.isDone()).toBeTruthy(); }); diff --git a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts index f85d9c09726..191b575c748 100644 --- a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts +++ b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts @@ -1,114 +1,102 @@ import nock from 'nock'; -export const mockLocalhostSDLQuery = () => - nock('http://localhost:4001', { encodedQueryParams: true }) - .post('/graphql', { +export const mockLocalhostSDLQuery = ({ + url, + sdl, +}: { + url: string; + sdl: string; +}) => + nock(url) + .post('/', { query: 'query GetServiceDefinition { _service { sdl } }', }) - .reply( - 200, - { - data: { - _service: { - sdl: - 'extend type Query {\n me: User\n everyone: [User]\n}\n\ntype User @key(fields: "id") {\n id: ID!\n name: String\n username: String\n}\n', - }, - }, - }, - { - 'Content-Type': 'application/json', - }, - ); + .reply(200, { data: { _service: { sdl: sdl } } }); -export const mockFetchStorageSecret = () => - nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) +export const mockFetchStorageSecret = ({ + apiKeyHash, + storageSecret, + serviceName, +}: { + apiKeyHash: string; + storageSecret: string; + serviceName: string; +}) => + nock('https://storage.googleapis.com:443') .get( - '/engine-partial-schema-prod/mdg-private-6077/storage-secret/232d3a78a4392a756d37613458805dd830b517453729dc4945c24ca4b575cfc270b08e763cea5a8da49c784b31272d1c79b1d598cdc6e4d6913700a5505cd2f7.json', + `/engine-partial-schema-prod/${serviceName}/storage-secret/${apiKeyHash}.json`, ) - .reply( - 200, - [ - '1f', - '8b', - '08', - '00', - '00', - '00', - '000000035332334831b434b734d43548b434d53531324ad44d3437b3d04d4e4e3435333348b34c4c365702008b81631826000000', - ], - { - 'Content-Type': 'application/octet-stream', - 'Content-Encoding': 'gzip', - }, - ); + .reply(200, `"${storageSecret}"`); // get composition config link, using received storage secret -export const mockGetCompositionConfigLink = () => - nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) +export const mockGetCompositionConfigLink = (storageSecret: string) => + nock('https://storage.googleapis.com:443') .get( - '/engine-partial-schema-prod/60d19791-0a95-422a-a768-cca5660f9ac7/current/v1/composition-config-link', + `/engine-partial-schema-prod/${storageSecret}/current/v1/composition-config-link`, ) - .reply( - 200, - [ - '1f', - '8b', - '08', - '0000000000000325cccb0d02211000d05e383bf21106d82a3c791f6741315930805e8cbdbb89af80f711dc6a2eb733cdbb5804aa55471f35288a0eac3104e431003339449523b197fcea3dd529df5a72db9e6d94595a857f34a43348579503f813ee876684603d815b4352c94467391f1fa3557110b9f58de625f5b10f62d1df1fc566aa6190000000', - ], - { - 'Content-Type': 'application/json', - 'Content-Encoding': 'gzip', - }, - ); + .reply(200, { + configPath: `${storageSecret}/current/v1/composition-configs/composition-config-path.json`, + }); // get composition configs, using received composition config link -export const mockGetCompositionConfigs = () => - nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) +export const mockGetCompositionConfigs = ({ + storageSecret, + implementingServicePath, + federatedServiceName, +}: { + storageSecret: string; + implementingServicePath: string; + federatedServiceName: string; +}) => + nock('https://storage.googleapis.com:443') .get( - '/engine-partial-schema-prod/60d19791-0a95-422a-a768-cca5660f9ac7/current/v1/composition-configs/526ab0f8-7365-41c6-847a-5d8e0e2954cf.json', + `/engine-partial-schema-prod/${storageSecret}/current/v1/composition-configs/composition-config-path.json`, ) - .reply( - 200, - [ - '1f8b08000000000000034d90b16e1c310c44ff45b5d74b521225dd17a44867204d9082a2c8f882ec9eb17b7663f8dfa324089096e0cc9b99f7e0b76393fb173bceeb6d0f177c08d7112e21134b07af4b899c9784ca4b4d45963caa8151cb493dccdfede5a76db6dfaffbf7273bdeae6a9f6f2af7e97586cbd7f7b0cb66d34e546faffbfd9c9217b93fcf0bc3c0561a2e206d02886491c2755195cc0cde44cbaaafc731cdd7375cff272de75fd4b9fef35d4b14e251910bc694dcab756a92e3a8ad7bd64a390fc5d4a289b30b38f634d0005a01ee4263986949997b823ef92c80031a241d9db2387a258ad63c9177058796236365cd02d6011f7f9c73be8f6f0fe1d467dbe4939cbf5b6a6632191d39c642313115436e55b520688b831be706b3784f9e51b37713ced68620c90cdc63751932d5510169823512cef1726c5838691ec9e78449128d3f199dac51d3e415ba848f5f8438ec47e2010000', + .reply(200, { + implementingServiceLocations: [ + { + name: federatedServiceName, + path: `${storageSecret}/current/v1/implementing-services/${federatedServiceName}/${implementingServicePath}.json`, + }, ], - { - 'Content-Type': 'application/octet-stream', - 'Content-Encoding': 'gzip', - }, - ); + }); // get implementing service reference, using received composition-config -export const mockGetImplementingServices = () => - nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) +export const mockGetImplementingServices = ({ + storageSecret, + implementingServicePath, + partialSchemaPath, + federatedServiceName, + federatedServiceURL, +}: { + storageSecret: string; + implementingServicePath: string; + partialSchemaPath: string; + federatedServiceName: string; + federatedServiceURL: string; +}) => + nock('https://storage.googleapis.com:443') .get( - '/engine-partial-schema-prod/60d19791-0a95-422a-a768-cca5660f9ac7/current/v1/implementing-services/accounts/73a26d81671344ff8eb29a53d89bf5c8255dc1493eaf6fa0f1b4d1e009706ba2ddeec7456b40b6606a01d0904cdb25af1f8223e9f42fbc0f09536186c5a0eb01.json', + `/engine-partial-schema-prod/${storageSecret}/current/v1/implementing-services/${federatedServiceName}/${implementingServicePath}.json`, ) - .reply( - 200, - [ - '1f8b08000000000000032d90bb6ec3300c45ff45730c536f2973976e050a642725b136e01764251d8afe7bd5361b710fef01c82fc17b5db1dd4a3de77d135779111f158fe9f5455cc59a3f86a3ce0f6c6570e0bd78c21bd619b7d637d2bdd6d2a78bd8702d3dc094f6fbd6ce9e1c58db8ccb7b9aca8a6fd8a68e1d64197d940360b483510a07f42e0c29a1750e3862f2e3533a56fc1c9e92e1fcb39ca3f2402918a7d9446f0c98643c922d96c979e6eca256a4341a451a8882b221781d038728ad2607d25291cae6a894e29c0299c84912ab64bdd63143e71e14b0b42e6196d25896a4bd72e4289a7e572d8ff9ff590280ad94110c1449c04c6831b352803658f4143449a6fcdbbad7a517a6d68eeb382e7bc265dacf763500527cff00fb367ce587010000', - ], - { - 'Content-Type': 'application/octet-stream', - 'Content-Encoding': 'gzip', - }, - ); + .reply(200, { + name: federatedServiceName, + partialSchemaPath: `${storageSecret}/current/raw-partial-schemas/${partialSchemaPath}`, + url: federatedServiceURL, + }); // get raw-partial-schema, using received composition-config -export const mockGetRawPartialSchema = () => - nock('https://storage.googleapis.com:443', { encodedQueryParams: true }) +export const mockGetRawPartialSchema = ({ + storageSecret, + partialSchemaPath, + federatedServiceSchema, +}: { + storageSecret: string; + partialSchemaPath: string; + federatedServiceSchema: string; +}) => + nock('https://storage.googleapis.com:443') .get( - '/engine-partial-schema-prod/60d19791-0a95-422a-a768-cca5660f9ac7/current/raw-partial-schemas/270bc8463f4974404c47ab5e5fb67ffd6932b23a42b30bb825887398f89153b6015be125d9222fdc8b49fc1bf2c57339d05be7020f156cad1145f1b3726b6b94', + `/engine-partial-schema-prod/${storageSecret}/current/raw-partial-schemas/${partialSchemaPath}`, ) - .reply( - 200, - [ - '1f8b08000000000000034bad2849cd4b5128a92c4855082c4d2daa54a8e65250c84db552082d4e2d023253cb8082f97940816890482c572d1717583588a7e0909d5aa99196999a93526ca5a09499a2a409d69f9962a5e0e9a20864e52582cc0a2e29cacc4b07724b819a50846ab90003647a4182000000', - ], - { - 'Content-Type': 'application/octet-stream', - 'Content-Encoding': 'gzip', - }, - ); + .reply(200, federatedServiceSchema); From 11137f080e49aa632c8a30227681e07d8ba13f02 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Tue, 25 Jun 2019 14:32:55 -0700 Subject: [PATCH 09/27] Test for something more ~real~ --- .../src/__tests__/integration/networkRequests.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts index 37c0601cc18..92ca3d898d8 100644 --- a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts +++ b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts @@ -36,13 +36,13 @@ it('Queries remote endpoints for their SDLs', async () => { // This test is maybe a bit terrible, but IDK a better way to mock all the requests it('Extracts service definitions from remote storage', async () => { - let apiKey = 'service:jacksons-service:AABBCCDDEEFFGG'; + let serviceName = 'jacksons-service'; + let apiKey = `service:${serviceName}:AABBCCDDEEFFGG`; let apiKeyHash = createSHA('sha512') .update(apiKey) .digest('hex'); let storageSecret = 'secret'; - let serviceName = 'jacksons-service'; let implementingServicePath = 'path-to-implementing-service-definition.json'; let partialSchemaPath = 'path-to-accounts-partial-schema.json'; let federatedServiceName = 'accounts'; @@ -53,6 +53,7 @@ it('Extracts service definitions from remote storage', async () => { everyone: [User] } + "This is my User" type User @key(fields: "id") { id: ID! name: String @@ -84,6 +85,8 @@ it('Extracts service definitions from remote storage', async () => { }); let gateway = new ApolloGateway({ apiKey }); + await gateway.load(); expect(nock.isDone()).toBeTruthy(); + expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); }); From de238e5e859823aa2e2edf4cb0e39abe7214d4ab Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Tue, 25 Jun 2019 14:42:44 -0700 Subject: [PATCH 10/27] Additional ~real~ tests. --- .../__tests__/integration/networkRequests.test.ts | 13 ++++++++----- .../src/__tests__/integration/nockMocks.ts | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts index 92ca3d898d8..305a873817b 100644 --- a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts +++ b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts @@ -12,14 +12,16 @@ import { } from './nockMocks'; it('Queries remote endpoints for their SDLs', async () => { - let url = 'localhost:4001'; - let sdl = `extend type Query { + let url = 'http://localhost:4001'; + let sdl = ` + extend type Query { me: User - everyone: [User] + everyone: [User] } + "My User." type User @key(fields: "id") { - id: ID! + id: ID! name: String username: String } @@ -28,10 +30,11 @@ it('Queries remote endpoints for their SDLs', async () => { mockLocalhostSDLQuery({ url, sdl }); let gateway = new ApolloGateway({ - serviceList: [{ name: 'accounts', url: 'http://localhost:4001/' }], + serviceList: [{ name: 'accounts', url: `${url}/graphql` }], }); await gateway.load(); expect(nock.isDone()).toBeTruthy(); + expect(gateway.schema!.getType('User')!.description).toBe('My User.'); }); // This test is maybe a bit terrible, but IDK a better way to mock all the requests diff --git a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts index 191b575c748..5b3af8c4828 100644 --- a/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts +++ b/packages/apollo-gateway/src/__tests__/integration/nockMocks.ts @@ -8,10 +8,10 @@ export const mockLocalhostSDLQuery = ({ sdl: string; }) => nock(url) - .post('/', { + .post('/graphql', { query: 'query GetServiceDefinition { _service { sdl } }', }) - .reply(200, { data: { _service: { sdl: sdl } } }); + .reply(200, { data: { _service: { sdl } } }); export const mockFetchStorageSecret = ({ apiKeyHash, From 7fc8583254ab8900cebacb82601f9f1e2b74562a Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 26 Jun 2019 13:41:52 -0700 Subject: [PATCH 11/27] Move to onSchemaChange as a public field This enables config after construction. --- packages/apollo-gateway/src/index.ts | 34 ++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 6d9a2d3bb54..8e45ed7a637 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -40,7 +40,6 @@ export interface GatewayConfigBase { __exposeQueryPlanExperimental?: boolean; buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; serviceList?: ServiceEndpointDefinition[]; - onSchemaChange?: SchemaChangeCallback; } export interface LocalGatewayConfig extends GatewayConfigBase { @@ -53,10 +52,7 @@ function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { return 'localServiceList' in config; } -export type SchemaChangeCallback = (options: { - schema: GraphQLSchema; - executor: GraphQLExecutor; -}) => void; +export type SchemaChangeCallback = (options: { schema: GraphQLSchema }) => void; export class ApolloGateway implements GraphQLService { public schema?: GraphQLSchema; @@ -67,6 +63,22 @@ export class ApolloGateway implements GraphQLService { protected queryPlanStore?: InMemoryLRUCache; private pollingTimer?: NodeJS.Timer; + private _onSchemaChange?: SchemaChangeCallback; + public get onSchemaChange(): SchemaChangeCallback | undefined { + return this._onSchemaChange; + } + + public set onSchemaChange(value: SchemaChangeCallback | undefined) { + // TODO: if (!isRemoteGatewayConfig(this.config)) { throw new Error('onSchemaChange requires an Apollo Engine hosted service list definition.'); } + this._onSchemaChange = value; + if (!this.onSchemaChange) { + clearInterval(this.pollingTimer!); + this.pollingTimer = undefined; + } else if (!this.pollingTimer) { + this.startPollingServices(); + } + } + constructor(config: GatewayConfig) { this.config = { // TODO: expose the query plan in a more flexible JSON format in the future @@ -139,6 +151,9 @@ export class ApolloGateway implements GraphQLService { } private startPollingServices() { + if (this.pollingTimer) { + clearInterval(this.pollingTimer); + } this.pollingTimer = setInterval(async () => { const [services, isNewSchema] = await this.loadServiceDefinitions( this.config, @@ -150,10 +165,11 @@ export class ApolloGateway implements GraphQLService { if (this.queryPlanStore) this.queryPlanStore.flush(); this.logger.debug('Gateway config has changed, updating schema'); this.createSchema(services); - this.config.onSchemaChange!({ - schema: this.schema!, - executor: this.executor, - }); + if (this.onSchemaChange) { + this.onSchemaChange({ + schema: this.schema!, + }); + } }, 10 * 1000); } From 31ec6ebe713e3c7035e7793ced9bd750a35ef557 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 26 Jun 2019 13:45:03 -0700 Subject: [PATCH 12/27] Oops build error --- packages/apollo-gateway/src/index.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 8e45ed7a637..6979e3213c8 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -104,13 +104,6 @@ export class ApolloGateway implements GraphQLService { } this.initializeQueryPlanStore(); - - if (config.onSchemaChange) { - if (!isLocalConfig(this.config)) { - this.logger.debug('Starting polling for schema changes'); - this.startPollingServices(); - } - } } public async load() { From a15697f81dd1a0284c65dcee763e1265a993367e Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 26 Jun 2019 13:45:51 -0700 Subject: [PATCH 13/27] Style --- packages/apollo-gateway/src/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 6979e3213c8..8a5da664cab 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -144,9 +144,8 @@ export class ApolloGateway implements GraphQLService { } private startPollingServices() { - if (this.pollingTimer) { - clearInterval(this.pollingTimer); - } + if (this.pollingTimer) clearInterval(this.pollingTimer); + this.pollingTimer = setInterval(async () => { const [services, isNewSchema] = await this.loadServiceDefinitions( this.config, From 8b510e107338baf08945e4b1ea08e955cdcd0129 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 26 Jun 2019 14:08:21 -0700 Subject: [PATCH 14/27] Allow many onSchemaChange listeners --- packages/apollo-gateway/src/index.ts | 34 ++++++++++++---------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 8a5da664cab..1298c50851e 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -52,7 +52,8 @@ function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { return 'localServiceList' in config; } -export type SchemaChangeCallback = (options: { schema: GraphQLSchema }) => void; +export type SchemaChangeCallback = (schema: GraphQLSchema) => void; +export type Unsubscriber = () => void; export class ApolloGateway implements GraphQLService { public schema?: GraphQLSchema; @@ -62,21 +63,20 @@ export class ApolloGateway implements GraphQLService { protected logger: Logger; protected queryPlanStore?: InMemoryLRUCache; private pollingTimer?: NodeJS.Timer; + private onSchemaChangeListeners = new Set(); - private _onSchemaChange?: SchemaChangeCallback; - public get onSchemaChange(): SchemaChangeCallback | undefined { - return this._onSchemaChange; - } - - public set onSchemaChange(value: SchemaChangeCallback | undefined) { + public onSchemaChange(value: SchemaChangeCallback): Unsubscriber { // TODO: if (!isRemoteGatewayConfig(this.config)) { throw new Error('onSchemaChange requires an Apollo Engine hosted service list definition.'); } - this._onSchemaChange = value; - if (!this.onSchemaChange) { - clearInterval(this.pollingTimer!); - this.pollingTimer = undefined; - } else if (!this.pollingTimer) { - this.startPollingServices(); - } + this.onSchemaChangeListeners.add(value); + if (!this.pollingTimer) this.startPollingServices(); + + return () => { + this.onSchemaChangeListeners.delete(value); + if (this.onSchemaChangeListeners.size === 0 && this.pollingTimer) { + clearInterval(this.pollingTimer!); + this.pollingTimer = undefined; + } + }; } constructor(config: GatewayConfig) { @@ -157,11 +157,7 @@ export class ApolloGateway implements GraphQLService { if (this.queryPlanStore) this.queryPlanStore.flush(); this.logger.debug('Gateway config has changed, updating schema'); this.createSchema(services); - if (this.onSchemaChange) { - this.onSchemaChange({ - schema: this.schema!, - }); - } + this.onSchemaChangeListeners.forEach(listener => listener(this.schema!)); }, 10 * 1000); } From 84eb21d1c394fb49eaf2b7e13fcb070cc33dc4d8 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 26 Jun 2019 14:10:31 -0700 Subject: [PATCH 15/27] Move function --- packages/apollo-gateway/src/index.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 1298c50851e..15cc351087e 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -65,20 +65,6 @@ export class ApolloGateway implements GraphQLService { private pollingTimer?: NodeJS.Timer; private onSchemaChangeListeners = new Set(); - public onSchemaChange(value: SchemaChangeCallback): Unsubscriber { - // TODO: if (!isRemoteGatewayConfig(this.config)) { throw new Error('onSchemaChange requires an Apollo Engine hosted service list definition.'); } - this.onSchemaChangeListeners.add(value); - if (!this.pollingTimer) this.startPollingServices(); - - return () => { - this.onSchemaChangeListeners.delete(value); - if (this.onSchemaChangeListeners.size === 0 && this.pollingTimer) { - clearInterval(this.pollingTimer!); - this.pollingTimer = undefined; - } - }; - } - constructor(config: GatewayConfig) { this.config = { // TODO: expose the query plan in a more flexible JSON format in the future @@ -143,6 +129,20 @@ export class ApolloGateway implements GraphQLService { this.isReady = true; } + public onSchemaChange(value: SchemaChangeCallback): Unsubscriber { + // TODO: if (!isRemoteGatewayConfig(this.config)) { throw new Error('onSchemaChange requires an Apollo Engine hosted service list definition.'); } + this.onSchemaChangeListeners.add(value); + if (!this.pollingTimer) this.startPollingServices(); + + return () => { + this.onSchemaChangeListeners.delete(value); + if (this.onSchemaChangeListeners.size === 0 && this.pollingTimer) { + clearInterval(this.pollingTimer!); + this.pollingTimer = undefined; + } + }; + } + private startPollingServices() { if (this.pollingTimer) clearInterval(this.pollingTimer); From 3e769f17feb810be8ee84eaeb7f230c4ab369659 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 26 Jun 2019 15:14:27 -0700 Subject: [PATCH 16/27] Add note on why todo is a todo --- packages/apollo-gateway/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 15cc351087e..806d8a7bb70 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -130,7 +130,7 @@ export class ApolloGateway implements GraphQLService { } public onSchemaChange(value: SchemaChangeCallback): Unsubscriber { - // TODO: if (!isRemoteGatewayConfig(this.config)) { throw new Error('onSchemaChange requires an Apollo Engine hosted service list definition.'); } + // TODO: if (!isRemoteGatewayConfig(this.config)) { throw new Error('onSchemaChange requires an Apollo Engine hosted service list definition.'); } (dependant on #2915) this.onSchemaChangeListeners.add(value); if (!this.pollingTimer) this.startPollingServices(); From cf5abb1667f8d6ccc448ee95eba97d6c6eba5a84 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 26 Jun 2019 15:51:24 -0700 Subject: [PATCH 17/27] Naming --- packages/apollo-gateway/src/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 67c2f018adb..8db577de67a 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -65,12 +65,10 @@ function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { } function isHostedConfig(config: GatewayConfig): config is ManagedGatewayConfig { - return !(isLocalConfig(config) || isConcreteConfig(config)); + return !(isLocalConfig(config) || isRemoteConfig(config)); } -function isConcreteConfig( - config: GatewayConfig, -): config is RemoteGatewayConfig { +function isRemoteConfig(config: GatewayConfig): config is RemoteGatewayConfig { return 'serviceList' in config; } @@ -212,7 +210,7 @@ export class ApolloGateway implements GraphQLService { ): Promise<[ServiceDefinition[], boolean]> { if (isLocalConfig(config)) return [config.localServiceList, false]; - const [remoteServices, isNewService] = isConcreteConfig(config) + const [remoteServices, isNewService] = isRemoteConfig(config) ? await getServiceDefinitionsFromRemoteEndpoint({ serviceList: config.serviceList, }) From e11a1fc12e34643dff9f867044e193da4fe0f41e Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Wed, 26 Jun 2019 16:12:46 -0700 Subject: [PATCH 18/27] Revert "Merge with polling as the features are intertwined" This reverts commit 46294656c3ee8ead6c9fb9f4b3542d721a5f70df, reversing changes made to de238e5e859823aa2e2edf4cb0e39abe7214d4ab. --- packages/apollo-gateway/src/index.ts | 46 +--------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 8db577de67a..f865b02cc4b 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -72,9 +72,6 @@ function isRemoteConfig(config: GatewayConfig): config is RemoteGatewayConfig { return 'serviceList' in config; } -export type SchemaChangeCallback = (schema: GraphQLSchema) => void; -export type Unsubscriber = () => void; - export class ApolloGateway implements GraphQLService { public schema?: GraphQLSchema; public isReady: boolean = false; @@ -82,8 +79,6 @@ export class ApolloGateway implements GraphQLService { protected config: GatewayConfig; protected logger: Logger; protected queryPlanStore?: InMemoryLRUCache; - private pollingTimer?: NodeJS.Timer; - private onSchemaChangeListeners = new Set(); constructor(config: GatewayConfig) { this.config = { @@ -158,43 +153,11 @@ export class ApolloGateway implements GraphQLService { this.isReady = true; } - public onSchemaChange(value: SchemaChangeCallback): Unsubscriber { - // TODO: if (!isRemoteGatewayConfig(this.config)) { throw new Error('onSchemaChange requires an Apollo Engine hosted service list definition.'); } (dependant on #2915) - this.onSchemaChangeListeners.add(value); - if (!this.pollingTimer) this.startPollingServices(); - - return () => { - this.onSchemaChangeListeners.delete(value); - if (this.onSchemaChangeListeners.size === 0 && this.pollingTimer) { - clearInterval(this.pollingTimer!); - this.pollingTimer = undefined; - } - }; - } - - private startPollingServices() { - if (this.pollingTimer) clearInterval(this.pollingTimer); - - this.pollingTimer = setInterval(async () => { - const [services, isNewSchema] = await this.loadServiceDefinitions( - this.config, - ); - if (!isNewSchema) { - this.logger.debug('No changes to gateway config'); - return; - } - if (this.queryPlanStore) this.queryPlanStore.flush(); - this.logger.debug('Gateway config has changed, updating schema'); - this.createSchema(services); - this.onSchemaChangeListeners.forEach(listener => listener(this.schema!)); - }, 10 * 1000); - } - protected createServices(services: ServiceEndpointDefinition[]) { for (const serviceDef of services) { if (!serviceDef.url && !isLocalConfig(this.config)) { throw new Error( - `Service definition for service ${serviceDef.name} is missing a url`, + `Service defintion for service ${serviceDef.name} is missing a url`, ); } this.serviceMap[serviceDef.name] = this.config.buildService @@ -297,13 +260,6 @@ export class ApolloGateway implements GraphQLService { sizeCalculator: approximateObjectSize, }); } - - public async stop() { - if (this.pollingTimer) { - clearInterval(this.pollingTimer); - this.pollingTimer = undefined; - } - } } function approximateObjectSize(obj: T): number { From 0654857684049552870deb98e283d4f0a043a5df Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 07:48:59 -0700 Subject: [PATCH 19/27] Update packages/apollo-gateway/src/index.ts Co-Authored-By: Jesse Rosenberger --- packages/apollo-gateway/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index f865b02cc4b..8d253aee6c5 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -108,7 +108,7 @@ export class ApolloGateway implements GraphQLService { const apiKey = config.apiKey || process.env['ENGINE_API_KEY']; if (!apiKey) { throw new Error( - 'The gateway requires either a serviceList, localServiceList, or apiKey to be provided in the config, or ENGINE_API_KEY to be defined in the environment', + 'Apollo Gateway requires either a `serviceList`, `localServiceList`, or `apiKey` to be provided in the config, or `ENGINE_API_KEY` to be defined in the environment.', ); } } From 2174b226a13ee84d1650cb5ecb1e8103f0311e1a Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 10:37:16 -0700 Subject: [PATCH 20/27] Use afterEach, remove 'export's, use `const`. --- .../integration/networkRequests.test.ts | 33 ++++++++++--------- .../src/loadServicesFromStorage.ts | 14 ++++---- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts index 305a873817b..7f781a756dc 100644 --- a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts +++ b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts @@ -11,9 +11,13 @@ import { mockLocalhostSDLQuery, } from './nockMocks'; +afterEach(() => { + expect(nock.isDone()).toBeTruthy(); +}); + it('Queries remote endpoints for their SDLs', async () => { - let url = 'http://localhost:4001'; - let sdl = ` + const url = 'http://localhost:4001'; + const sdl = ` extend type Query { me: User everyone: [User] @@ -29,28 +33,28 @@ it('Queries remote endpoints for their SDLs', async () => { mockLocalhostSDLQuery({ url, sdl }); - let gateway = new ApolloGateway({ + const gateway = new ApolloGateway({ serviceList: [{ name: 'accounts', url: `${url}/graphql` }], }); await gateway.load(); - expect(nock.isDone()).toBeTruthy(); expect(gateway.schema!.getType('User')!.description).toBe('My User.'); }); // This test is maybe a bit terrible, but IDK a better way to mock all the requests it('Extracts service definitions from remote storage', async () => { - let serviceName = 'jacksons-service'; - let apiKey = `service:${serviceName}:AABBCCDDEEFFGG`; - let apiKeyHash = createSHA('sha512') + const serviceName = 'jacksons-service'; + const apiKey = `service:${serviceName}:AABBCCDDEEFFGG`; + const apiKeyHash = createSHA('sha512') .update(apiKey) .digest('hex'); - let storageSecret = 'secret'; - let implementingServicePath = 'path-to-implementing-service-definition.json'; - let partialSchemaPath = 'path-to-accounts-partial-schema.json'; - let federatedServiceName = 'accounts'; - let federatedServiceURL = 'http://localhost:4001'; - let federatedServiceSchema = ` + const storageSecret = 'secret'; + const implementingServicePath = + 'path-to-implementing-service-definition.json'; + const partialSchemaPath = 'path-to-accounts-partial-schema.json'; + const federatedServiceName = 'accounts'; + const federatedServiceURL = 'http://localhost:4001'; + const federatedServiceSchema = ` extend type Query { me: User everyone: [User] @@ -87,9 +91,8 @@ it('Extracts service definitions from remote storage', async () => { federatedServiceSchema, }); - let gateway = new ApolloGateway({ apiKey }); + const gateway = new ApolloGateway({ apiKey }); await gateway.load(); - expect(nock.isDone()).toBeTruthy(); expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); }); diff --git a/packages/apollo-gateway/src/loadServicesFromStorage.ts b/packages/apollo-gateway/src/loadServicesFromStorage.ts index 07ba76fabd2..667c008934b 100644 --- a/packages/apollo-gateway/src/loadServicesFromStorage.ts +++ b/packages/apollo-gateway/src/loadServicesFromStorage.ts @@ -3,12 +3,12 @@ import { ServiceDefinition } from '@apollo/federation'; import { parse } from 'graphql'; import createSHA from './utilities/createSHA'; -export interface LinkFileResult { +interface LinkFileResult { configPath: string; formatVersion: number; } -export interface ImplementingService { +interface ImplementingService { formatVersion: number; graphID: string; graphVariant: string; @@ -18,20 +18,20 @@ export interface ImplementingService { partialSchemaPath: string; } -export interface ImplementingServiceLocation { +interface ImplementingServiceLocation { name: string; path: string; } -export interface ConfigFileResult { +interface ConfigFileResult { formatVersion: number; id: string; implementingServiceLocations: ImplementingServiceLocation[]; schemaHash: string; } -export const envOverrideOperationManifest = 'APOLLO_PARTIAL_SCHEMA_BASE_URL'; -export const envOverrideStorageSecretBaseUrl = 'APOLLO_STORAGE_SECRET_BASE_URL'; +const envOverrideOperationManifest = 'APOLLO_PARTIAL_SCHEMA_BASE_URL'; +const envOverrideStorageSecretBaseUrl = 'APOLLO_STORAGE_SECRET_BASE_URL'; const urlFromEnvOrDefault = (envKey: string, fallback: string) => (process.env[envKey] || fallback).replace(/\/$/, ''); @@ -42,7 +42,7 @@ const urlPartialSchemaBase = urlFromEnvOrDefault( 'https://storage.googleapis.com/engine-partial-schema-prod/', ); -export const urlStorageSecretBase: string = urlFromEnvOrDefault( +const urlStorageSecretBase: string = urlFromEnvOrDefault( envOverrideStorageSecretBaseUrl, 'https://storage.googleapis.com/engine-partial-schema-prod/', ); From 82b18ea1e503e910140d12839272c8832c7b0265 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 10:48:31 -0700 Subject: [PATCH 21/27] Reduce config.apiKey duplication --- packages/apollo-gateway/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 8d253aee6c5..fa6780f9078 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -105,8 +105,8 @@ export class ApolloGateway implements GraphQLService { } if (isHostedConfig(config)) { - const apiKey = config.apiKey || process.env['ENGINE_API_KEY']; - if (!apiKey) { + config.apiKey = config.apiKey || process.env['ENGINE_API_KEY']; + if (!config.apiKey) { throw new Error( 'Apollo Gateway requires either a `serviceList`, `localServiceList`, or `apiKey` to be provided in the config, or `ENGINE_API_KEY` to be defined in the environment.', ); @@ -178,7 +178,7 @@ export class ApolloGateway implements GraphQLService { serviceList: config.serviceList, }) : await getServiceDefinitionsFromStorage({ - apiKey: config.apiKey || process.env['ENGINE_API_KEY']!, + apiKey: config.apiKey!, graphVariant: config.tag || 'current', federationVersion: config.federationVersion!, }); From 9ad2250c4839edb16cea6a6bc127f1c3671fc716 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 10:51:38 -0700 Subject: [PATCH 22/27] Naming --- packages/apollo-gateway/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index fa6780f9078..798ba447f73 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -64,7 +64,9 @@ function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { return 'localServiceList' in config; } -function isHostedConfig(config: GatewayConfig): config is ManagedGatewayConfig { +function isManagedConfig( + config: GatewayConfig, +): config is ManagedGatewayConfig { return !(isLocalConfig(config) || isRemoteConfig(config)); } @@ -104,7 +106,7 @@ export class ApolloGateway implements GraphQLService { this.createSchema(config.localServiceList); } - if (isHostedConfig(config)) { + if (isManagedConfig(config)) { config.apiKey = config.apiKey || process.env['ENGINE_API_KEY']; if (!config.apiKey) { throw new Error( From 2b4ca6666760c00ed23999f975fdbee72f94dd61 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 13:49:47 -0700 Subject: [PATCH 23/27] Make apiKeyHash and graphId passed to load() --- packages/apollo-gateway/src/index.ts | 56 +++++++++---------- .../src/loadServicesFromStorage.ts | 24 ++++---- .../apollo-gateway/src/utilities/createSHA.ts | 10 ---- .../src/utilities/isNodeLike.ts | 11 ---- 4 files changed, 39 insertions(+), 62 deletions(-) delete mode 100644 packages/apollo-gateway/src/utilities/createSHA.ts delete mode 100644 packages/apollo-gateway/src/utilities/isNodeLike.ts diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 798ba447f73..b3e433291ab 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -42,16 +42,14 @@ interface GatewayConfigBase { buildService?: (definition: ServiceEndpointDefinition) => GraphQLDataSource; } -export interface RemoteGatewayConfig extends GatewayConfigBase { +interface RemoteGatewayConfig extends GatewayConfigBase { serviceList: ServiceEndpointDefinition[]; } -export interface ManagedGatewayConfig extends GatewayConfigBase { - apiKey?: string; - tag?: string; +interface ManagedGatewayConfig extends GatewayConfigBase { federationVersion?: number; } -export interface LocalGatewayConfig extends GatewayConfigBase { +interface LocalGatewayConfig extends GatewayConfigBase { localServiceList: ServiceDefinition[]; } @@ -60,16 +58,12 @@ export type GatewayConfig = | LocalGatewayConfig | ManagedGatewayConfig; +type EngineConfig = { apiKeyHash: string; graphId: string; graphTag?: string }; + function isLocalConfig(config: GatewayConfig): config is LocalGatewayConfig { return 'localServiceList' in config; } -function isManagedConfig( - config: GatewayConfig, -): config is ManagedGatewayConfig { - return !(isLocalConfig(config) || isRemoteConfig(config)); -} - function isRemoteConfig(config: GatewayConfig): config is RemoteGatewayConfig { return 'serviceList' in config; } @@ -81,6 +75,7 @@ export class ApolloGateway implements GraphQLService { protected config: GatewayConfig; protected logger: Logger; protected queryPlanStore?: InMemoryLRUCache; + private engineConfig: EngineConfig | undefined; constructor(config: GatewayConfig) { this.config = { @@ -106,21 +101,14 @@ export class ApolloGateway implements GraphQLService { this.createSchema(config.localServiceList); } - if (isManagedConfig(config)) { - config.apiKey = config.apiKey || process.env['ENGINE_API_KEY']; - if (!config.apiKey) { - throw new Error( - 'Apollo Gateway requires either a `serviceList`, `localServiceList`, or `apiKey` to be provided in the config, or `ENGINE_API_KEY` to be defined in the environment.', - ); - } - } - this.initializeQueryPlanStore(); } - public async load() { + public async load(engineConfig?: EngineConfig) { + this.engineConfig = engineConfig; if (!this.isReady) { this.logger.debug('Loading configuration for Gateway'); + const [services] = await this.loadServiceDefinitions(this.config); this.logger.debug('Configuration loaded for Gateway'); this.createSchema(services); @@ -175,17 +163,27 @@ export class ApolloGateway implements GraphQLService { ): Promise<[ServiceDefinition[], boolean]> { if (isLocalConfig(config)) return [config.localServiceList, false]; - const [remoteServices, isNewService] = isRemoteConfig(config) - ? await getServiceDefinitionsFromRemoteEndpoint({ + const getServiceDefinitions = async () => { + if (isRemoteConfig(config)) { + return getServiceDefinitionsFromRemoteEndpoint({ serviceList: config.serviceList, - }) - : await getServiceDefinitionsFromStorage({ - apiKey: config.apiKey!, - graphVariant: config.tag || 'current', - federationVersion: config.federationVersion!, }); + } else { + if (!this.engineConfig) { + throw new Error( + 'Must supply engineConfig to ApolloGateway#load() when no serviceList is provided.', + ); + } + return getServiceDefinitionsFromStorage({ + graphId: this.engineConfig.graphId, + apiKeyHash: this.engineConfig.apiKeyHash, + graphVariant: this.engineConfig.graphTag || 'current', + federationVersion: config.federationVersion || 1, + }); + } + }; - return [remoteServices, isNewService]; + return await getServiceDefinitions(); } public executor = async ( diff --git a/packages/apollo-gateway/src/loadServicesFromStorage.ts b/packages/apollo-gateway/src/loadServicesFromStorage.ts index 667c008934b..c42dcb65b22 100644 --- a/packages/apollo-gateway/src/loadServicesFromStorage.ts +++ b/packages/apollo-gateway/src/loadServicesFromStorage.ts @@ -1,7 +1,6 @@ import { CachedFetcher } from './cachedFetcher'; import { ServiceDefinition } from '@apollo/federation'; import { parse } from 'graphql'; -import createSHA from './utilities/createSHA'; interface LinkFileResult { configPath: string; @@ -50,30 +49,31 @@ const urlStorageSecretBase: string = urlFromEnvOrDefault( const fetcher = new CachedFetcher(); let serviceDefinitionList: ServiceDefinition[] = []; -function getStorageSecretUrl(apiKey: string): string { - const graphId = apiKey.split(':', 2)[1]; - const apiKeyHash = createSHA('sha512') - .update(apiKey) - .digest('hex'); +function getStorageSecretUrl(graphId: string, apiKeyHash: string): string { return `${urlStorageSecretBase}/${graphId}/storage-secret/${apiKeyHash}.json`; } -async function fetchStorageSecret(apiKey: string): Promise { - const storageSecretUrl = getStorageSecretUrl(apiKey); +async function fetchStorageSecret( + graphId: string, + apiKeyHash: string, +): Promise { + const storageSecretUrl = getStorageSecretUrl(graphId, apiKeyHash); const response = await fetcher.fetch(storageSecretUrl); return JSON.parse(response.result); } export async function getServiceDefinitionsFromStorage({ - apiKey, + graphId, + apiKeyHash, graphVariant, - federationVersion = 1, + federationVersion, }: { - apiKey: string; + graphId: string; + apiKeyHash: string; graphVariant: string; federationVersion: number; }): Promise<[ServiceDefinition[], boolean]> { - const secret = await fetchStorageSecret(apiKey); + const secret = await fetchStorageSecret(graphId, apiKeyHash); if (!graphVariant) { console.warn('No graphVariant specified, defaulting to "current".'); diff --git a/packages/apollo-gateway/src/utilities/createSHA.ts b/packages/apollo-gateway/src/utilities/createSHA.ts deleted file mode 100644 index d7362148969..00000000000 --- a/packages/apollo-gateway/src/utilities/createSHA.ts +++ /dev/null @@ -1,10 +0,0 @@ -import isNodeLike from './isNodeLike'; - -export default function(kind: string): import('crypto').Hash { - if (isNodeLike) { - // Use module.require instead of just require to avoid bundling whatever - // crypto polyfills a non-Node bundler might fall back to. - return module.require('crypto').createHash(kind); - } - return require('sha.js')(kind); -} diff --git a/packages/apollo-gateway/src/utilities/isNodeLike.ts b/packages/apollo-gateway/src/utilities/isNodeLike.ts deleted file mode 100644 index e5fa3a2221b..00000000000 --- a/packages/apollo-gateway/src/utilities/isNodeLike.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default typeof process === 'object' && - process && - // We used to check `process.release.name === "node"`, however that doesn't - // account for certain forks of Node.js which are otherwise identical to - // Node.js. For example, NodeSource's N|Solid reports itself as "nsolid", - // though it's mostly the same build of Node.js with an extra addon. - process.release && - process.versions && - // The one thing which is present on both Node.js and N|Solid (a fork of - // Node.js), is `process.versions.node` being defined. - typeof process.versions.node === 'string'; From ddc24bda1f879346af3eb400c0b92bf324762167 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 13:58:16 -0700 Subject: [PATCH 24/27] Fix tests (maybe) --- .../__tests__/integration/networkRequests.test.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts index 7f781a756dc..7e7e477de82 100644 --- a/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts +++ b/packages/apollo-gateway/src/__tests__/integration/networkRequests.test.ts @@ -1,6 +1,5 @@ import nock from 'nock'; import { ApolloGateway } from '../..'; -import createSHA from '../../utilities/createSHA'; import { mockGetRawPartialSchema, @@ -43,10 +42,9 @@ it('Queries remote endpoints for their SDLs', async () => { // This test is maybe a bit terrible, but IDK a better way to mock all the requests it('Extracts service definitions from remote storage', async () => { const serviceName = 'jacksons-service'; - const apiKey = `service:${serviceName}:AABBCCDDEEFFGG`; - const apiKeyHash = createSHA('sha512') - .update(apiKey) - .digest('hex'); + // hash of `service:${serviceName}:AABBCCDDEEFFGG`, but we don't want to depend on createSHA. + const apiKeyHash = // createSHA('sha512').update(apiKey).digest('hex'); + 'fa54332e7e8f1522edf721cea7ada93dc7c736f9343f4483989268a956b9101fabd18a53b62d837072abd87018b0b67e052ff732a98b3999e15d39d66d8e56dc'; const storageSecret = 'secret'; const implementingServicePath = @@ -91,8 +89,8 @@ it('Extracts service definitions from remote storage', async () => { federatedServiceSchema, }); - const gateway = new ApolloGateway({ apiKey }); + const gateway = new ApolloGateway({}); - await gateway.load(); + await gateway.load({ apiKeyHash, graphId: serviceName }); expect(gateway.schema!.getType('User')!.description).toBe('This is my User'); }); From d2772337f59e840ea74c1f3c4674c2e738a0c8f3 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 14:03:06 -0700 Subject: [PATCH 25/27] Make the diff smaller --- packages/apollo-gateway/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index b3e433291ab..0d2e3631a2e 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -108,7 +108,6 @@ export class ApolloGateway implements GraphQLService { this.engineConfig = engineConfig; if (!this.isReady) { this.logger.debug('Loading configuration for Gateway'); - const [services] = await this.loadServiceDefinitions(this.config); this.logger.debug('Configuration loaded for Gateway'); this.createSchema(services); From a1fce6582622ac776c1a6cd3da6c48e70ec5c5bc Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 14:04:53 -0700 Subject: [PATCH 26/27] Remove space in service list log message --- packages/apollo-gateway/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 0d2e3631a2e..a9c651c20a0 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -119,7 +119,7 @@ export class ApolloGateway implements GraphQLService { protected createSchema(services: ServiceDefinition[]) { this.logger.debug( `Composing schema from service list: \n${services - .map(({ name, url }) => ` ${url || 'local'} : ${name}`) + .map(({ name, url }) => ` ${url || 'local'}: ${name}`) .join('\n')}`, ); From 583c437b97c114bd4b3488fdb31e8acf0db88fe3 Mon Sep 17 00:00:00 2001 From: Jackson Kearl Date: Thu, 27 Jun 2019 14:07:42 -0700 Subject: [PATCH 27/27] Make config optional (Managed gateways dont need it) --- packages/apollo-gateway/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index a9c651c20a0..014f8c83ad6 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -77,7 +77,7 @@ export class ApolloGateway implements GraphQLService { protected queryPlanStore?: InMemoryLRUCache; private engineConfig: EngineConfig | undefined; - constructor(config: GatewayConfig) { + constructor(config?: GatewayConfig) { this.config = { // TODO: expose the query plan in a more flexible JSON format in the future // and remove this config option in favor of `exposeQueryPlan`. Playground @@ -93,12 +93,12 @@ export class ApolloGateway implements GraphQLService { loglevelDebug(this.logger); // And also support the `debug` option, if it's truthy. - if (config.debug === true) { + if (this.config.debug === true) { this.logger.enableAll(); } - if (isLocalConfig(config)) { - this.createSchema(config.localServiceList); + if (isLocalConfig(this.config)) { + this.createSchema(this.config.localServiceList); } this.initializeQueryPlanStore();