diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 414d846e354..7209c6f73d6 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -22,6 +22,7 @@ import { GraphQLRequestContextDidEncounterErrors, GraphQLRequestContextDidResolveOperation, Logger, + ApolloConfig, } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; @@ -30,8 +31,6 @@ import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; import { createHash } from 'crypto'; -let warnedOnDeprecatedApiKey = false; - export interface ClientInfo { clientName?: string; clientVersion?: string; @@ -69,75 +68,6 @@ export type GenerateClientInfo = ( requestContext: GraphQLRequestContext, ) => ClientInfo; -// AS3: Drop support for deprecated `ENGINE_API_KEY`. -export function getEngineApiKey({ - engine, - skipWarn = false, - logger = console, -}: { - engine: EngineReportingOptions | boolean | undefined; - skipWarn?: boolean; - logger?: Logger; -}) { - if (typeof engine === 'object') { - if (engine.apiKey) { - return engine.apiKey; - } - } - const legacyApiKeyFromEnv = process.env.ENGINE_API_KEY; - const apiKeyFromEnv = process.env.APOLLO_KEY; - - if (legacyApiKeyFromEnv && apiKeyFromEnv && !skipWarn) { - logger.warn( - 'Using `APOLLO_KEY` since `ENGINE_API_KEY` (deprecated) is also set in the environment.', - ); - } - if (legacyApiKeyFromEnv && !warnedOnDeprecatedApiKey && !skipWarn) { - logger.warn( - '[deprecated] The `ENGINE_API_KEY` environment variable has been renamed to `APOLLO_KEY`.', - ); - warnedOnDeprecatedApiKey = true; - } - return apiKeyFromEnv || legacyApiKeyFromEnv || ''; -} - -// AS3: Drop support for deprecated `ENGINE_SCHEMA_TAG`. -export function getEngineGraphVariant( - engine: EngineReportingOptions | boolean | undefined, - logger: Logger = console, -): string | undefined { - if (engine === false) { - return; - } else if ( - typeof engine === 'object' && - (engine.graphVariant || engine.schemaTag) - ) { - if (engine.graphVariant && engine.schemaTag) { - throw new Error( - 'Cannot set both engine.graphVariant and engine.schemaTag. Please use engine.graphVariant.', - ); - } - if (engine.schemaTag) { - logger.warn( - '[deprecated] The `schemaTag` property within `engine` configuration has been renamed to `graphVariant`.', - ); - } - return engine.graphVariant || engine.schemaTag; - } else { - if (process.env.ENGINE_SCHEMA_TAG) { - logger.warn( - '[deprecated] The `ENGINE_SCHEMA_TAG` environment variable has been renamed to `APOLLO_GRAPH_VARIANT`.', - ); - } - if (process.env.ENGINE_SCHEMA_TAG && process.env.APOLLO_GRAPH_VARIANT) { - throw new Error( - '`APOLLO_GRAPH_VARIANT` and `ENGINE_SCHEMA_TAG` (deprecated) environment variables must not both be set.', - ); - } - return process.env.APOLLO_GRAPH_VARIANT || process.env.ENGINE_SCHEMA_TAG; - } -} - export interface EngineReportingOptions { /** * API key for the service. Get this from @@ -429,9 +359,8 @@ class ReportData { // to the Engine server. export class EngineReportingAgent { private readonly options: EngineReportingOptions; - private readonly apiKey: string; + private readonly apolloConfig: ApolloConfig; private readonly logger: Logger = console; - private readonly graphVariant: string; private readonly reportDataByExecutableSchemaId: { [executableSchemaId: string]: ReportData | undefined; @@ -452,29 +381,25 @@ export class EngineReportingAgent { private readonly tracesEndpointUrl: string; readonly schemaReport: boolean; - public constructor(options: EngineReportingOptions = {}) { + public constructor( + options: EngineReportingOptions = {}, + apolloConfig: ApolloConfig, + ) { this.options = options; - this.apiKey = getEngineApiKey({ - engine: this.options, - skipWarn: false, - logger: this.logger, - }); + this.apolloConfig = apolloConfig; if (options.logger) this.logger = options.logger; this.bootId = uuidv4(); - this.graphVariant = getEngineGraphVariant(options, this.logger) || ''; - if (!this.apiKey) { - throw new Error( - `To use EngineReportingAgent, you must specify an API key via the apiKey option or the APOLLO_KEY environment variable.`, - ); + if (!this.apolloConfig.key) { + throw new Error('Missing API key.'); } if (options.experimental_schemaReporting !== undefined) { this.logger.warn( [ '[deprecated] The "experimental_schemaReporting" option has been', - 'renamed to "reportSchema"' - ].join(' ') + 'renamed to "reportSchema"', + ].join(' '), ); if (options.reportSchema === undefined) { options.reportSchema = options.experimental_schemaReporting; @@ -485,11 +410,12 @@ export class EngineReportingAgent { this.logger.warn( [ '[deprecated] The "experimental_overrideReportedSchema" option has', - 'been renamed to "overrideReportedSchema"' - ].join(' ') + 'been renamed to "overrideReportedSchema"', + ].join(' '), ); if (options.overrideReportedSchema === undefined) { - options.overrideReportedSchema = options.experimental_overrideReportedSchema; + options.overrideReportedSchema = + options.experimental_overrideReportedSchema; } } @@ -497,18 +423,19 @@ export class EngineReportingAgent { this.logger.warn( [ '[deprecated] The "experimental_schemaReportingInitialDelayMaxMs"', - 'option has been renamed to "schemaReportingInitialDelayMaxMs"' - ].join(' ') + 'option has been renamed to "schemaReportingInitialDelayMaxMs"', + ].join(' '), ); if (options.schemaReportingInitialDelayMaxMs === undefined) { - options.schemaReportingInitialDelayMaxMs = options.experimental_schemaReportingInitialDelayMaxMs; + options.schemaReportingInitialDelayMaxMs = + options.experimental_schemaReportingInitialDelayMaxMs; } } if (options.reportSchema !== undefined) { this.schemaReport = options.reportSchema; } else { - this.schemaReport = process.env.APOLLO_SCHEMA_REPORTING === "true" + this.schemaReport = process.env.APOLLO_SCHEMA_REPORTING === 'true'; } // Since calculating the signature for Engine reporting is potentially an @@ -567,7 +494,7 @@ export class EngineReportingAgent { if (existing) { return existing; } - const reportData = new ReportData(executableSchemaId, this.graphVariant); + const reportData = new ReportData(executableSchemaId, this.apolloConfig.graphVariant); this.reportDataByExecutableSchemaId[executableSchemaId] = reportData; return reportData; } @@ -636,7 +563,7 @@ export class EngineReportingAgent { public async sendAllReports(): Promise { await Promise.all( - Object.keys(this.reportDataByExecutableSchemaId).map(id => + Object.keys(this.reportDataByExecutableSchemaId).map((id) => this.sendReport(id), ), ); @@ -702,7 +629,7 @@ export class EngineReportingAgent { method: 'POST', headers: { 'user-agent': 'apollo-engine-reporting', - 'x-api-key': this.apiKey, + 'x-api-key': this.apolloConfig.key!, 'content-encoding': 'gzip', }, body: compressed, @@ -711,8 +638,9 @@ export class EngineReportingAgent { if (curResponse.status >= 500 && curResponse.status < 600) { throw new Error( - `HTTP status ${curResponse.status}, ${(await curResponse.text()) || - '(no body)'}`, + `HTTP status ${curResponse.status}, ${ + (await curResponse.text()) || '(no body)' + }`, ); } else { return curResponse; @@ -763,14 +691,14 @@ export class EngineReportingAgent { this.logger.info('Schema to report has been overridden'); } if (this.options.schemaReportingInitialDelayMaxMs !== undefined) { - this.logger.info(`Schema reporting max initial delay override: ${ - this.options.schemaReportingInitialDelayMaxMs - } ms`); + this.logger.info( + `Schema reporting max initial delay override: ${this.options.schemaReportingInitialDelayMaxMs} ms`, + ); } if (this.options.schemaReportingUrl !== undefined) { - this.logger.info(`Schema reporting URL override: ${ - this.options.schemaReportingUrl - }`); + this.logger.info( + `Schema reporting URL override: ${this.options.schemaReportingUrl}`, + ); } if (this.currentSchemaReporter) { this.currentSchemaReporter.stop(); @@ -778,7 +706,7 @@ export class EngineReportingAgent { const serverInfo = { bootId: this.bootId, - graphVariant: this.graphVariant, + graphVariant: this.apolloConfig.graphVariant, // The infra environment in which this edge server is running, e.g. localhost, Kubernetes // Length must be <= 256 characters. platform: process.env.APOLLO_SERVER_PLATFORM || 'local', @@ -796,21 +724,20 @@ export class EngineReportingAgent { }; this.logger.info( - `Schema reporting EdgeServerInfo: ${JSON.stringify(serverInfo)}` - ) + `Schema reporting EdgeServerInfo: ${JSON.stringify(serverInfo)}`, + ); // Jitter the startup between 0 and 10 seconds const delay = Math.floor( - Math.random() * - (this.options.schemaReportingInitialDelayMaxMs || 10_000), + Math.random() * (this.options.schemaReportingInitialDelayMaxMs || 10_000), ); const schemaReporter = new SchemaReporter( serverInfo, executableSchema, - this.apiKey, + this.apolloConfig.key!, this.options.schemaReportingUrl, - this.logger + this.logger, ); const fallbackReportingDelayInMs = 20_000; @@ -818,7 +745,7 @@ export class EngineReportingAgent { this.currentSchemaReporter = schemaReporter; const logger = this.logger; - setTimeout(function() { + setTimeout(function () { reportingLoop(schemaReporter, logger, false, fallbackReportingDelayInMs); }, delay); } @@ -896,7 +823,7 @@ export class EngineReportingAgent { // either the request-specific logger on the request context (if available) // or to the `logger` that was passed into `EngineReportingOptions` which // is provided in the `EngineReportingAgent` constructor options. - this.signatureCache.set(cacheKey, generatedSignature).catch(err => { + this.signatureCache.set(cacheKey, generatedSignature).catch((err) => { logger.warn( 'Could not store signature cache. ' + (err && err.message) || err, ); @@ -907,14 +834,16 @@ export class EngineReportingAgent { public async sendAllReportsAndReportErrors(): Promise { await Promise.all( - Object.keys(this.reportDataByExecutableSchemaId).map(executableSchemaId => + Object.keys( + this.reportDataByExecutableSchemaId, + ).map((executableSchemaId) => this.sendReportAndReportErrors(executableSchemaId), ), ); } private sendReportAndReportErrors(executableSchemaId: string): Promise { - return this.sendReport(executableSchemaId).catch(err => { + return this.sendReport(executableSchemaId).catch((err) => { // This catch block is primarily intended to catch network errors from // the retried request itself, which include network errors and non-2xx // HTTP errors. diff --git a/packages/apollo-gateway/src/__tests__/gateway/reporting.test.ts b/packages/apollo-gateway/src/__tests__/gateway/reporting.test.ts index 9fc331b0680..bba972369b9 100644 --- a/packages/apollo-gateway/src/__tests__/gateway/reporting.test.ts +++ b/packages/apollo-gateway/src/__tests__/gateway/reporting.test.ts @@ -118,6 +118,7 @@ describe('reporting', () => { executor, engine: { apiKey: 'service:foo:bar', + graphVariant: 'current', sendReportsImmediately: true, }, }); diff --git a/packages/apollo-gateway/src/index.ts b/packages/apollo-gateway/src/index.ts index 278f7102d8c..5567846dc0b 100644 --- a/packages/apollo-gateway/src/index.ts +++ b/packages/apollo-gateway/src/index.ts @@ -8,6 +8,7 @@ import { GraphQLExecutionResult, Logger, GraphQLRequestContextExecutionDidStart, + ApolloConfig, } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { @@ -192,7 +193,7 @@ export class ApolloGateway implements GraphQLService { protected config: GatewayConfig; private logger: Logger; protected queryPlanStore?: InMemoryLRUCache; - private engineConfig: GraphQLServiceEngineConfig | undefined; + private apolloConfig?: ApolloConfig; private pollingTimer?: NodeJS.Timer; private onSchemaChangeListeners = new Set(); private serviceDefinitions: ServiceDefinition[] = []; @@ -299,11 +300,16 @@ export class ApolloGateway implements GraphQLService { } } - public async load(options?: { engine?: GraphQLServiceEngineConfig }) { - if (options && options.engine) { - if (!options.engine.graphVariant) - this.logger.warn('No graph variant provided. Defaulting to `current`.'); - this.engineConfig = options.engine; + public async load(options?: { apollo?: ApolloConfig; engine?: GraphQLServiceEngineConfig }) { + if (options?.apollo) { + this.apolloConfig = options.apollo; + } else if (options?.engine) { + // Older version of apollo-server-core that isn't passing 'apollo' yet. + this.apolloConfig = { + keyHash: options.engine.apiKeyHash, + graphId: options.engine.graphId, + graphVariant: options.engine.graphVariant || 'current', + } } await this.updateComposition(); @@ -314,7 +320,7 @@ export class ApolloGateway implements GraphQLService { this.pollServices(); } - const { graphId, graphVariant } = (options && options.engine) || {}; + const { graphId, graphVariant } = this.apolloConfig || {}; const mode = isManagedConfig(this.config) ? 'managed' : 'unmanaged'; this.logger.info( @@ -563,15 +569,16 @@ export class ApolloGateway implements GraphQLService { protected async loadServiceDefinitions( config: GatewayConfig, ): ReturnType { + const canUseManagedConfig = + this.apolloConfig?.graphId && this.apolloConfig?.keyHash; // This helper avoids the repetition of options in the two cases this method - // is invoked below. It is a helper, rather than an options object, since it - // depends on the presence of `this.engineConfig`, which is guarded against - // further down in this method in two separate places. - const getManagedConfig = (engineConfig: GraphQLServiceEngineConfig) => { + // is invoked below. Only call it if canUseManagedConfig is true + // (which makes its uses of ! safe) + const getManagedConfig = () => { return getServiceDefinitionsFromStorage({ - graphId: engineConfig.graphId, - apiKeyHash: engineConfig.apiKeyHash, - graphVariant: engineConfig.graphVariant, + graphId: this.apolloConfig!.graphId!, + apiKeyHash: this.apolloConfig!.keyHash!, + graphVariant: this.apolloConfig!.graphVariant, federationVersion: (config as ManagedGatewayConfig).federationVersion || 1, fetcher: this.fetcher, @@ -579,13 +586,13 @@ export class ApolloGateway implements GraphQLService { }; if (isLocalConfig(config) || isRemoteConfig(config)) { - if (this.engineConfig && !this.warnedStates.remoteWithLocalConfig) { + if (canUseManagedConfig && !this.warnedStates.remoteWithLocalConfig) { // Only display this warning once per start-up. this.warnedStates.remoteWithLocalConfig = true; // This error helps avoid common misconfiguration. // We don't await this because a local configuration should assume // remote is unavailable for one reason or another. - getManagedConfig(this.engineConfig).then(() => { + getManagedConfig().then(() => { this.logger.warn( "A local gateway service list is overriding an Apollo Graph " + "Manager managed configuration. To use the managed " + @@ -614,13 +621,13 @@ export class ApolloGateway implements GraphQLService { }); } - if (!this.engineConfig) { + if (!canUseManagedConfig) { throw new Error( - 'When `serviceList` is not set, an Apollo Engine configuration must be provided. See https://www.apollographql.com/docs/apollo-server/federation/managed-federation/ for more information.', + 'When `serviceList` is not set, an Apollo configuration must be provided. See https://www.apollographql.com/docs/apollo-server/federation/managed-federation/ for more information.', ); } - return getManagedConfig(this.engineConfig); + return getManagedConfig(); } // XXX Nothing guarantees that the only errors thrown or returned in diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 82faeb25e7e..e29528f2698 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -62,7 +62,6 @@ import { import { generateSchemaHash } from './utils/schemaHash'; import { isDirectiveDefined } from './utils/isDirectiveDefined'; -import createSHA from './utils/createSHA'; import { processGraphQLRequest, GraphQLRequestContext, @@ -73,13 +72,14 @@ import { import { Headers } from 'apollo-server-env'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; import { plugin as pluginTracing } from "apollo-tracing"; -import { Logger, SchemaHash, ValueOrPromise } from "apollo-server-types"; +import { Logger, SchemaHash, ValueOrPromise, ApolloConfig } from "apollo-server-types"; import { plugin as pluginCacheControl, CacheControlExtensionOptions, } from 'apollo-cache-control'; -import { getEngineApiKey, getEngineGraphVariant } from "apollo-engine-reporting/dist/agent"; import { cloneObject } from "./runHttpQuery"; +import { determineApolloConfig } from './determineApolloConfig'; +import { federatedPlugin, EngineReportingAgent } from 'apollo-engine-reporting'; const NoIntrospection = (context: ValidationContext) => ({ Field(node: FieldDefinitionNode) { @@ -94,15 +94,6 @@ const NoIntrospection = (context: ValidationContext) => ({ }, }); -function getEngineServiceId(engine: Config['engine'], logger: Logger): string | undefined { - const engineApiKey = getEngineApiKey({engine, skipWarn: true, logger} ); - if (engineApiKey) { - return engineApiKey.split(':', 2)[1]; - } - - return; -} - const forbidUploadsForTesting = process && process.env.NODE_ENV === 'test' && !runtimeSupportsUploads; @@ -127,9 +118,8 @@ export class ApolloServerBase { public requestOptions: Partial> = Object.create(null); private context?: Context | ContextFunction; - private engineReportingAgent?: import('apollo-engine-reporting').EngineReportingAgent; - private engineServiceId?: string; - private engineApiKeyHash?: string; + private engineReportingAgent?: EngineReportingAgent; + private apolloConfig: ApolloConfig; protected plugins: ApolloServerPlugin[] = []; protected subscriptionServerOptions?: SubscriptionServerOptions; @@ -166,7 +156,6 @@ export class ApolloServerBase { mocks, mockEntireSchema, extensions, - engine, subscriptions, uploads, playground, @@ -175,9 +164,20 @@ export class ApolloServerBase { cacheControl, experimental_approximateDocumentStoreMiB, handleSignals, + apollo, + engine, ...requestOptions } = config; + if (engine !== undefined) { + // FIXME(no-engine): finish implementing backwards-compatibility `engine` + // mode and warn about deprecation + if (apollo) { + throw new Error("You cannot provide both `engine` and `apollo` to `new ApolloServer()`. " + + "For details on how to migrate all of your options out of `engine`, see FIXME(no-engine) URL MISSING"); + } + } + // Setup logging facilities if (config.logger) { this.logger = config.logger; @@ -200,6 +200,8 @@ export class ApolloServerBase { this.logger = loglevelLogger; } + this.apolloConfig = determineApolloConfig(apollo, engine, this.logger); + if (gateway && (modules || schema || typeDefs || resolvers)) { throw new Error( 'Cannot define both `gateway` and any of: `modules`, `schema`, `typeDefs`, or `resolvers`', @@ -294,24 +296,17 @@ export class ApolloServerBase { } } - // In an effort to avoid over-exposing the API key itself, extract the - // service ID from the API key for plugins which only needs service ID. - // The truthiness of this value can also be used in other forks of logic - // related to Engine, as is the case with EngineReportingAgent just below. - this.engineServiceId = getEngineServiceId(engine, this.logger); - const apiKey = getEngineApiKey({engine, skipWarn: true, logger: this.logger}); - if (apiKey) { - this.engineApiKeyHash = createSHA('sha512') - .update(apiKey) - .digest('hex'); - } - - if (this.engineServiceId) { - const { EngineReportingAgent } = require('apollo-engine-reporting'); + if (this.apolloConfig.key) { + // FIXME(no-engine) Eliminate EngineReportingAgent entirely. this.engineReportingAgent = new EngineReportingAgent( typeof engine === 'object' ? engine : Object.create({ logger: this.logger, }), + // This isn't part of EngineReportingOptions because it's generated + // internally by ApolloServer, not part of the end-user options... and + // hopefully EngineReportingAgent will be eliminated before this code + // is shipped anyway. + this.apolloConfig, ); // Don't add the extension here (we want to add it later in generateSchemaDerivedData). } @@ -420,7 +415,6 @@ export class ApolloServerBase { private initSchema(): GraphQLSchema | Promise { const { gateway, - engine, schema, modules, typeDefs, @@ -440,13 +434,13 @@ export class ApolloServerBase { ), ); - const graphVariant = getEngineGraphVariant(engine, this.logger); + // For backwards compatibility with old versions of @apollo/gateway. const engineConfig = - this.engineApiKeyHash && this.engineServiceId + this.apolloConfig.keyHash && this.apolloConfig.graphId ? { - apiKeyHash: this.engineApiKeyHash, - graphId: this.engineServiceId, - ...(graphVariant && { graphVariant }), + apiKeyHash: this.apolloConfig.keyHash, + graphId: this.apolloConfig.graphId, + graphVariant: this.apolloConfig.graphVariant, } : undefined; @@ -456,7 +450,7 @@ export class ApolloServerBase { // a federated schema! this.requestOptions.executor = gateway.executor; - return gateway.load({ engine: engineConfig }) + return gateway.load({ apollo: this.apolloConfig, engine: engineConfig }) .then(config => config.schema) .catch(err => { // We intentionally do not re-throw the exact error from the gateway @@ -592,9 +586,10 @@ export class ApolloServerBase { logger: this.logger, schema: schema, schemaHash: schemaHash, + apollo: this.apolloConfig, engine: { - serviceID: this.engineServiceId, - apiKeyHash: this.engineApiKeyHash, + serviceID: this.apolloConfig.graphId, + apiKeyHash: this.apolloConfig.keyHash, }, }; @@ -833,7 +828,6 @@ export class ApolloServerBase { // We haven't configured this app to use Engine directly. But it looks like // we are a federated service backend, so we should be capable of including // our trace in a response extension if we are asked to by the gateway. - const { federatedPlugin } = require('apollo-engine-reporting'); const rewriteError = engine && typeof engine === 'object' ? engine.rewriteError : undefined; pluginsToInit.push(federatedPlugin({ rewriteError })); diff --git a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts index 52013e02fe6..f7df01739e3 100644 --- a/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts +++ b/packages/apollo-server-core/src/__tests__/ApolloServerBase.test.ts @@ -1,6 +1,7 @@ import { ApolloServerBase } from '../ApolloServer'; import { buildServiceDefinition } from '@apollographql/apollo-tools'; import gql from 'graphql-tag'; +import { Logger } from 'apollo-server-types'; const typeDefs = gql` type Query { @@ -45,7 +46,10 @@ describe('ApolloServerBase construction', () => { ).not.toThrow(); }); - it('spits out a deprecation warning when passed a schemaTag in construction', () => { + // FIXME(no-engine): This should be changed to check for a deprecation + // warning for any use of `engine` (which we can't really do until splitting + // out the plugins). + it.skip('spits out a deprecation warning when passed a schemaTag in construction', () => { const spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); expect( () => @@ -112,32 +116,47 @@ describe('environment variables', () => { it('constructs a reporting agent with the ENGINE_API_KEY (deprecated) environment variable and warns', async () => { // set the variables process.env.ENGINE_API_KEY = 'just:fake:stuff'; - const spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + const warn = jest.fn(); + const mockLogger: Logger = { + debug: jest.fn(), + info: jest.fn(), + warn, + error: jest.fn(), + }; const server = new ApolloServerBase({ typeDefs, - resolvers + resolvers, + apollo: { graphVariant: 'xxx' }, + logger: mockLogger, }); await server.stop(); - expect(spyConsoleWarn).toHaveBeenCalledTimes(1); - spyConsoleWarn.mockReset(); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0][0]).toMatch(/deprecated.*ENGINE_API_KEY/); }); it('warns with both the legacy env var and new env var set', async () => { // set the variables process.env.ENGINE_API_KEY = 'just:fake:stuff'; process.env.APOLLO_KEY = 'also:fake:stuff'; - const spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(); + const warn = jest.fn(); + const mockLogger: Logger = { + debug: jest.fn(), + info: jest.fn(), + warn, + error: jest.fn(), + }; const server = new ApolloServerBase({ typeDefs, - resolvers + resolvers, + apollo: { graphVariant: 'xxx' }, + logger: mockLogger, }); await server.stop(); - // Once for deprecation, once for double-set - expect(spyConsoleWarn).toHaveBeenCalledTimes(2); - spyConsoleWarn.mockReset(); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0][0]).toMatch(/Using.*APOLLO_KEY.*ENGINE_API_KEY/); }); }); diff --git a/packages/apollo-server-core/src/determineApolloConfig.ts b/packages/apollo-server-core/src/determineApolloConfig.ts new file mode 100644 index 00000000000..1148057eb7f --- /dev/null +++ b/packages/apollo-server-core/src/determineApolloConfig.ts @@ -0,0 +1,109 @@ +import { ApolloConfig, ApolloConfigInput, Logger } from 'apollo-server-types'; +import { EngineReportingOptions } from 'apollo-engine-reporting'; +import createSHA from './utils/createSHA'; + +// This function combines the newer `apollo` constructor argument, the older +// `engine` constructor argument, and some environment variables to come up +// with a full ApolloConfig. +// +// The caller ensures that only one of the two constructor arguments is actually +// provided and warns if `engine` was provided, but it is this function's job +// to warn if old environment variables are used. +export function determineApolloConfig( + input: ApolloConfigInput | undefined, + // For backwards compatibility. + // AS3: Drop support for deprecated 'engine'. + engine: EngineReportingOptions | boolean | undefined, + logger: Logger, +): ApolloConfig { + if (input && engine !== undefined) { + // There's a more helpful error in the actual ApolloServer constructor. + throw Error('Cannot pass both `apollo` and `engine`'); + } + const apolloConfig: ApolloConfig = { graphVariant: 'current' }; + + const { + APOLLO_KEY, + APOLLO_GRAPH_ID, + APOLLO_GRAPH_VARIANT, + // AS3: Drop support for deprecated `ENGINE_API_KEY` and `ENGINE_SCHEMA_TAG`. + ENGINE_API_KEY, + ENGINE_SCHEMA_TAG, + } = process.env; + + // Determine key. + if (input?.key) { + apolloConfig.key = input.key; + } else if (typeof engine === 'object' && engine.apiKey) { + apolloConfig.key = engine.apiKey; + } else if (APOLLO_KEY) { + if (ENGINE_API_KEY) { + logger.warn( + 'Using `APOLLO_KEY` since `ENGINE_API_KEY` (deprecated) is also set in the environment.', + ); + } + apolloConfig.key = APOLLO_KEY; + } else if (ENGINE_API_KEY) { + logger.warn( + '[deprecated] The `ENGINE_API_KEY` environment variable has been renamed to `APOLLO_KEY`.', + ); + apolloConfig.key = ENGINE_API_KEY; + } + + // Determine key hash. + if (apolloConfig.key) { + apolloConfig.keyHash = createSHA('sha512') + .update(apolloConfig.key) + .digest('hex'); + } + + // Determine graph id. + if (input?.graphId) { + apolloConfig.graphId = input.graphId; + } else if (APOLLO_GRAPH_ID) { + apolloConfig.graphId = APOLLO_GRAPH_ID; + } else if (apolloConfig.key) { + // This is the common case: if the given key is a graph token (starts with 'service:'), + // then use the service name written in the key. + const parts = apolloConfig.key.split(':', 2); + if (parts[0] === 'service') { + apolloConfig.graphId = parts[1]; + } + } + + // Determine variant. + if (input?.graphVariant) { + apolloConfig.graphVariant = input.graphVariant; + } else if (typeof engine === 'object' && engine.graphVariant) { + if (engine.schemaTag) { + throw new Error( + 'Cannot set more than one of apollo.graphVariant, ' + + 'engine.graphVariant, and engine.schemaTag. Please use apollo.graphVariant.', + ); + } + apolloConfig.graphVariant = engine.graphVariant; + } else if (typeof engine === 'object' && engine.schemaTag) { + // No need to warn here, because ApolloServer's constructor should warn about + // the existence of `engine` at all. + apolloConfig.graphVariant = engine.schemaTag; + } else if (APOLLO_GRAPH_VARIANT) { + if (ENGINE_SCHEMA_TAG) { + throw new Error( + '`APOLLO_GRAPH_VARIANT` and `ENGINE_SCHEMA_TAG` (deprecated) environment variables must not both be set.', + ); + } + apolloConfig.graphVariant = APOLLO_GRAPH_VARIANT; + } else if (ENGINE_SCHEMA_TAG) { + logger.warn( + '[deprecated] The `ENGINE_SCHEMA_TAG` environment variable has been renamed to `APOLLO_GRAPH_VARIANT`.', + ); + apolloConfig.graphVariant = ENGINE_SCHEMA_TAG; + } else if (apolloConfig.key) { + // Leave the value 'current' in apolloConfig.graphVariant. + // We warn if it looks like they're trying to use Apollo registry features, but there's + // no reason to warn if there's no key. + logger.warn('No graph variant provided. Defaulting to `current`.'); + } + + return apolloConfig; +} diff --git a/packages/apollo-server-core/src/types.ts b/packages/apollo-server-core/src/types.ts index 454561c3dc5..27a95d0e308 100644 --- a/packages/apollo-server-core/src/types.ts +++ b/packages/apollo-server-core/src/types.ts @@ -6,10 +6,12 @@ import { GraphQLParseOptions, } from 'graphql-tools'; import { + ApolloConfig, ValueOrPromise, GraphQLExecutor, GraphQLExecutionResult, GraphQLRequestContextExecutionDidStart, + ApolloConfigInput, } from 'apollo-server-types'; import { ConnectionContext } from 'subscriptions-transport-ws'; // The types for `ws` use `export = WebSocket`, so we'll use the @@ -90,6 +92,7 @@ export type GraphQLServiceEngineConfig = { export interface GraphQLService { load(options: { + apollo?: ApolloConfig, engine?: GraphQLServiceEngineConfig; }): Promise; onSchemaChange(callback: SchemaChangeCallback): Unsubscriber; @@ -113,7 +116,6 @@ export interface Config extends BaseConfig { introspection?: boolean; mocks?: boolean | IMocks; mockEntireSchema?: boolean; - engine?: boolean | EngineReportingOptions; extensions?: Array<() => GraphQLExtension>; cacheControl?: CacheControlExtensionOptions | boolean; plugins?: PluginDefinition[]; @@ -125,8 +127,11 @@ export interface Config extends BaseConfig { gateway?: GraphQLService; experimental_approximateDocumentStoreMiB?: number; handleSignals?: boolean; + apollo?: ApolloConfigInput; + engine?: boolean | EngineReportingOptions; } +// Configuration for how Apollo Server talks to the Apollo registry. export interface FileUploadOptions { //Max allowed non-file multipart form field size in bytes; enough for your queries (default: 1 MB). maxFieldSize?: number; diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index 50d92d41b52..0687a56b49e 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -105,6 +105,9 @@ export default async function pluginTestHarness({ logger: logger || console, schema, schemaHash, + apollo: { + graphVariant: 'current', + }, engine: {}, }); if (maybeListener && maybeListener.serverWillStop) { diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index 0e21e50b257..476bdde0a83 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -1001,6 +1001,7 @@ export function testApolloServer( validationRules: [validationRule], extensions: [() => new Extension()], engine: { + graphVariant: 'current', ...engineServer.engineOptions(), apiKey: 'service:my-app:secret', maxUncompressedReportSize: 1, @@ -1076,6 +1077,7 @@ export function testApolloServer( }, }, engine: { + graphVariant: 'current', ...engineServer.engineOptions(), apiKey: 'service:my-app:secret', maxUncompressedReportSize: 1, @@ -2379,6 +2381,7 @@ export function testApolloServer( resolvers: { Query: { something: () => 'hello' } }, engine: { apiKey: 'service:my-app:secret', + graphVariant: 'current', tracesEndpointUrl: fakeEngineUrl, reportIntervalMs: 1, maxAttempts: 3, @@ -3132,6 +3135,13 @@ export function testApolloServer( }); expect(optionsSpy).toHaveBeenLastCalledWith({ + apollo: { + key: 'service:tester:1234abc', + keyHash: + '0ca858e7fe8cffc01c5f1db917d2463b348b50d267427e54c1c8c99e557b242f4145930b949905ec430642467613610e471c40bb7a251b1e2248c399bb0498c4', + graphId: 'tester', + graphVariant: 'staging', + }, engine: { apiKeyHash: '0ca858e7fe8cffc01c5f1db917d2463b348b50d267427e54c1c8c99e557b242f4145930b949905ec430642467613610e471c40bb7a251b1e2248c399bb0498c4', diff --git a/packages/apollo-server-plugin-operation-registry/src/__tests__/ApolloServerPluginOperationRegistry.test.ts b/packages/apollo-server-plugin-operation-registry/src/__tests__/ApolloServerPluginOperationRegistry.test.ts index 79b02a2fd7e..f44fd73a77b 100644 --- a/packages/apollo-server-plugin-operation-registry/src/__tests__/ApolloServerPluginOperationRegistry.test.ts +++ b/packages/apollo-server-plugin-operation-registry/src/__tests__/ApolloServerPluginOperationRegistry.test.ts @@ -55,6 +55,7 @@ describe('Operation registry plugin', () => { const engineOptions: EngineReportingOptions = { apiKey, reportTiming: false, + graphVariant: 'current', }; const typeDefs = gql` type Query { diff --git a/packages/apollo-server-types/src/index.ts b/packages/apollo-server-types/src/index.ts index 3d02d315fd1..8521a82f2de 100644 --- a/packages/apollo-server-types/src/index.ts +++ b/packages/apollo-server-types/src/index.ts @@ -51,17 +51,36 @@ type Mutable = { -readonly [P in keyof T]: T[P] }; export type SchemaHash = Fauxpaque; -export interface GraphQLServiceContext { +// Configuration for how Apollo Server talks to the Apollo registry, as +// passed to the ApolloServer constructor. +export interface ApolloConfigInput { + key?: string; + graphId?: string; + graphVariant?: string; +} + +// Configuration for how Apollo Server talks to the Apollo registry, with +// some defaults filled in from the ApolloConfigInput passed to the constructor. +export interface ApolloConfig { + key?: string; + keyHash?: string; + graphId?: string; + graphVariant: string; +} + + export interface GraphQLServiceContext { logger: Logger; schema: GraphQLSchema; schemaHash: SchemaHash; + apollo: ApolloConfig; + persistedQueries?: { + cache: KeyValueCache; + }; + // For backwards compatibility only; prefer to use `graph`. engine: { serviceID?: string; apiKeyHash?: string; }; - persistedQueries?: { - cache: KeyValueCache; - }; } export interface GraphQLRequest {