diff --git a/packages/backend-common/src/reading/GcsUrlReader.test.ts b/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts similarity index 77% rename from packages/backend-common/src/reading/GcsUrlReader.test.ts rename to packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts index 16102d2812..c8262767e1 100644 --- a/packages/backend-common/src/reading/GcsUrlReader.test.ts +++ b/packages/backend-common/src/reading/GoogleGcsUrlReader.test.ts @@ -17,12 +17,12 @@ import { ConfigReader, JsonObject } from '@backstage/config'; import { getVoidLogger } from '../logging'; import { ReadTreeResponseFactory } from './tree'; -import { GcsUrlReader } from './GcsUrlReader'; +import { GoogleGcsUrlReader } from './GoogleGcsUrlReader'; import { UrlReaderPredicateTuple } from './types'; describe('GcsUrlReader', () => { const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { - return GcsUrlReader.factory({ + return GoogleGcsUrlReader.factory({ config: new ConfigReader(config), logger: getVoidLogger(), treeResponseFactory: ReadTreeResponseFactory.create({ @@ -57,43 +57,19 @@ describe('GcsUrlReader', () => { expect(entries).toHaveLength(2); }); - it('does not create a reader if the privateKey is missing', () => { + it('creates a reader with default credentials provider', () => { const entries = createReader({ integrations: { - googleGcs: [ - { - clientEmail: 'someone@example.com', - }, - ], + googleGcs: [{}], }, }); - expect(entries).toHaveLength(0); - }); - - it('does not create a reader if the clientEmail is missing', () => { - const entries = createReader({ - integrations: { - googleGcs: [ - { - privateKey: - '-----BEGIN PRIVATE KEY----- fakekey -----END PRIVATE KEY-----', - }, - ], - }, - }); - expect(entries).toHaveLength(0); + expect(entries).toHaveLength(1); }); describe('predicates', () => { const readers = createReader({ integrations: { - googleGcs: [ - { - privateKey: - '-----BEGIN PRIVATE KEY----- fakekey -----END PRIVATE KEY-----', - clientEmail: 'someone@example.com', - }, - ], + googleGcs: [{}], }, }); const predicate = readers[0].predicate; diff --git a/packages/backend-common/src/reading/GcsUrlReader.ts b/packages/backend-common/src/reading/GoogleGcsUrlReader.ts similarity index 60% rename from packages/backend-common/src/reading/GcsUrlReader.ts rename to packages/backend-common/src/reading/GoogleGcsUrlReader.ts index 08891d7b49..a9100051a6 100644 --- a/packages/backend-common/src/reading/GcsUrlReader.ts +++ b/packages/backend-common/src/reading/GoogleGcsUrlReader.ts @@ -22,13 +22,17 @@ import { UrlReader, } from './types'; import getRawBody from 'raw-body'; +import { + GOOGLE_GCS_HOST, + readGoogleGcsIntegrationConfigs, +} from '@backstage/integration'; const parseURL = ( url: string, ): { host: string; bucket: string; key: string } => { const { host, pathname } = new URL(url); - if (host !== 'storage.cloud.google.com') { + if (host !== GOOGLE_GCS_HOST) { throw new Error(`not a valid GCS URL: ${url}`); } @@ -40,45 +44,33 @@ const parseURL = ( }; }; -export class GcsUrlReader implements UrlReader { +export class GoogleGcsUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, logger }) => { if (!config.has('integrations.googleGcs')) { return []; } - const configs = config.getOptionalConfigArray('integrations.googleGcs'); - if (!configs) { - return []; - } - return configs - .filter(integration => { - if (!integration.has('clientEmail') || !integration.has('privateKey')) { - logger.warn( - "Skipping gcs integration, Missing required config value at 'integration.gcs.clientEmail' or 'integration.gcs.privateKey'", - ); - return false; - } - return true; - }) - .map(integration => { - const privKey = integration - .getOptionalString('privateKey') - ?.split('\\n') - .join('\n'); - - const storage = new Storage({ + const configs = readGoogleGcsIntegrationConfigs( + config.getOptionalConfigArray('integrations.googleGcs') ?? [], + ); + return configs.map(integration => { + let storage: Storage; + if (!integration.clientEmail || !integration.privateKey) { + logger.warn( + 'googleGcs credentials not found in config. Using default credentials provider.', + ); + storage = new Storage(); + } else { + storage = new Storage({ credentials: { - client_email: integration.getOptionalString('clientEmail'), - private_key: privKey, + client_email: integration.clientEmail || undefined, + private_key: integration.privateKey || undefined, }, }); - const reader = new GcsUrlReader(storage); - const host = - integration.getOptionalString('host') || 'storage.cloud.google.com'; - - logger.info('Configuring integration, gcs'); - const predicate = (url: URL) => url.host === host; - return { reader, predicate }; - }); + } + const reader = new GoogleGcsUrlReader(storage); + const predicate = (url: URL) => url.host === GOOGLE_GCS_HOST; + return { reader, predicate }; + }); }; constructor(private readonly storage: Storage) {} @@ -104,6 +96,6 @@ export class GcsUrlReader implements UrlReader { } toString() { - return `gcs{host=storage.cloud.google.com,authed=true}}`; + return `gcs{host=${GOOGLE_GCS_HOST},authed=true}}`; } } diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts index 785325faa7..06c6fdcdcf 100644 --- a/packages/backend-common/src/reading/UrlReaders.ts +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -24,7 +24,7 @@ import { GithubUrlReader } from './GithubUrlReader'; import { GitlabUrlReader } from './GitlabUrlReader'; import { ReadTreeResponseFactory } from './tree'; import { FetchUrlReader } from './FetchUrlReader'; -import { GcsUrlReader } from './GcsUrlReader'; +import { GoogleGcsUrlReader } from './GoogleGcsUrlReader'; type CreateOptions = { /** Root config object */ @@ -71,7 +71,7 @@ export class UrlReaders { BitbucketUrlReader.factory, GithubUrlReader.factory, GitlabUrlReader.factory, - GcsUrlReader.factory, + GoogleGcsUrlReader.factory, FetchUrlReader.factory, ]), }); diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index f0d72b7ceb..2947ba3889 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -149,5 +149,19 @@ export interface Config { */ baseUrl?: string; }>; + + /** Integration configuration for Google Cloud Storage */ + googleGcs?: Array<{ + /** + * Service account email used to authenticate requests. + * @visibility secret + */ + clientEmail?: string; + /** + * Service account private key used to authenticate requests. + * @visibility secret + */ + privateKey?: string; + }>; }; } diff --git a/packages/integration/src/googleGcs/config.test.ts b/packages/integration/src/googleGcs/config.test.ts new file mode 100644 index 0000000000..d612343b5e --- /dev/null +++ b/packages/integration/src/googleGcs/config.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config, ConfigReader } from '@backstage/config'; +import { + GoogleGcsIntegrationConfig, + readGoogleGcsIntegrationConfig, + readGoogleGcsIntegrationConfigs, +} from './config'; + +describe('readGoogleGcsIntegrationConfig', () => { + function buildConfig(data: Partial): Config { + return new ConfigReader(data); + } + + it('reads all values', () => { + const output = readGoogleGcsIntegrationConfig( + buildConfig({ + privateKey: 'fake-key', + clientEmail: 'someone@example.com', + }), + ); + expect(output).toEqual({ + privateKey: 'fake-key', + token: 'someone@example.com', + }); + }); +}); + +describe('readGoogleGcsIntegrationConfigs', () => { + function buildConfig(data: Partial[]): Config[] { + return data.map(item => new ConfigReader(item)); + } + + it('reads all values', () => { + const output = readGoogleGcsIntegrationConfigs( + buildConfig([ + { + privateKey: 'fake-key', + clientEmail: 'someone@example.com', + }, + ]), + ); + expect(output).toContainEqual({ + privateKey: 'fake-key', + clientEmail: 'someone@example.com', + }); + }); +}); diff --git a/packages/integration/src/googleGcs/config.ts b/packages/integration/src/googleGcs/config.ts new file mode 100644 index 0000000000..5f70932638 --- /dev/null +++ b/packages/integration/src/googleGcs/config.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; + +/** + * The configuration parameters for a single Google Cloud Storage provider. + */ +export type GoogleGcsIntegrationConfig = { + /** + * Service account email used to authenticate requests. + */ + clientEmail?: string; + /** + * Service account private key used to authenticate requests. + */ + privateKey?: string; +}; + +/** + * Reads a single Google GCS integration config. + * + * @param config The config object of a single integration + */ +export function readGoogleGcsIntegrationConfig( + config: Config, +): GoogleGcsIntegrationConfig { + if (!config.has('clientEmail') || !config.has('privateKey')) { + return {}; + } + + const privateKey = config + .getOptionalString('privateKey') + ?.split('\\n') + .join('\n'); + + const clientEmail = config.getOptionalString('clientEmail'); + return { clientEmail: clientEmail, privateKey: privateKey }; +} + +/** + * Reads a set of Google Cloud Storage integration configs. + * + * @param configs All of the integration config objects + */ +export function readGoogleGcsIntegrationConfigs( + configs: Config[], +): GoogleGcsIntegrationConfig[] { + return configs.map(readGoogleGcsIntegrationConfig); +} diff --git a/packages/integration/src/googleGcs/index.ts b/packages/integration/src/googleGcs/index.ts new file mode 100644 index 0000000000..f120256d35 --- /dev/null +++ b/packages/integration/src/googleGcs/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + readGoogleGcsIntegrationConfig, + readGoogleGcsIntegrationConfigs, +} from './config'; +export type { GoogleGcsIntegrationConfig } from './config'; +export const GOOGLE_GCS_HOST = 'storage.cloud.google.com'; diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index e89cf6ef92..d15eea0832 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -18,6 +18,7 @@ export * from './azure'; export * from './bitbucket'; export * from './github'; export * from './gitlab'; +export * from './googleGcs'; export { defaultScmResolveUrl } from './helpers'; export { ScmIntegrations } from './ScmIntegrations'; export type {