Google Cloud Storage working with TechDocs (publish and fetch)

Plus
1. Introduce ncessary configs to connect with storage
2. Introduce config to prefer CI build of docs, or techdocs-backend to build and publish them.
3. Write guide how to use Google Cloud Storage with TechDocs
4. Add a TechDocs Configuration reference page in docucmentation
This commit is contained in:
Himanshu Mishra
2020-12-03 22:40:15 +01:00
parent e09cff1682
commit 084670b37a
20 changed files with 971 additions and 163 deletions
+5 -6
View File
@@ -55,16 +55,15 @@ proxy:
organization:
name: My Company
# Reference documentation http://backstage.io/docs/features/techdocs/configuration
techdocs:
storageUrl: http://localhost:7000/api/techdocs/static/docs
requestUrl: http://localhost:7000/api/techdocs
storageUrl: http://localhost:7000/api/techdocs/static/docs
docsBuilder: 'local' # Alternatives - 'ci'
generators:
techdocs: 'docker'
techdocs: 'docker' # Alternatives - 'local'
publisher:
# Ref: <Link to TechDocs publisher reference page>
type: 'local' # other options - google_gcs, aws_s3, azure_storage, etc.
# if type set to google_gcs
# google_token: 'abc' # with write access
type: 'local' # Alternatives - 'google_gcs'. Read documentation for using alternatives.
sentry:
organization: my-company
+71
View File
@@ -0,0 +1,71 @@
---
id: configuration
title: TechDocs Configuration Options
description: Reference documentation for configuring TechDocs using app-config.yaml
---
Using the `app-config.yaml` in the Backstage app, you can configure TechDocs using several options.
This page serves as a reference to all the available configuration options for TechDocs.
```yaml
# File: app-config.yaml
techdocs:
# TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc.
requestUrl: http://localhost:7000/api/techdocs
# Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware
# to serve files from either a local directory or an External storage provider.
storageUrl: http://localhost:7000/api/techdocs/static/docs
# generators.techdocs can have two values: 'docker' or 'local'. This is to determine how to run the generator - whether to
# spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of).
# You want to change this to 'local' if you are running Backstage using your own customer Docker setup and want to avoid running
# into Docker in Docker situation. Read more here
# https://backstage.io/docs/features/techdocs/getting-started#disable-docker-in-docker-situation-optional
generators:
techdocs: 'docker'
# techdocs.builder can be either 'local' or 'ci.
# If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to build the docs, publish to storage
# and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture.
# If builder is set to 'ci', techdocs-backend will only fetch the docs and will NOT try to build and publish. In this case of 'ci',
# we assume that docs are being built in the CI/CD pipeline of the repository. This is the "Recommended" setup of the architecture.
# Read more here https://backstage.io/docs/features/techdocs/architecture
builder: 'local'
# techdocs.publisher is used to configure the Storage option, whether you want to use the local filesystem to store generated docs
# or you want to use External storage providers like Google Cloud Storage, AWS S3, etc.
publisher:
# techdocs.publisher.type can be - 'local' or 'google_gcs' (aws_s3, azure_storage, etc. to be available as well).
# When set to 'local', techdocs-backend will create a 'static' directory at its root to store generated documentation files.
# When set to 'google_gcs', techdocs-backend will use a Google Cloud Storage Bucket to store generated documentation files.
type: 'local'
# Required when techdocs.publisher.type is set to 'google_gcs'. Skip otherwise.
google:
# An API key is required to write to a storage bucket.
pathToKey: '/path/to/google_application_credentials.json',
# Your GCP Project ID where the Cloud Storage Bucket is hosted.
projectId: 'gcp-project-id'
# Cloud Storage Bucket Name
bucketName: 'techdocs-storage',
```
@@ -0,0 +1,80 @@
---
id: using-cloud-storage
title: Using Cloud Storage for TechDocs generated files
description: Using Cloud Storage for TechDocs generated files
---
In the [TechDocs architecture](./architecture.md) you have the option to choose where you want to store
the Generated static files which TechDocs uses to render documentation. In both the "Basic" and "Recommended"
setup, you can add cloud storage providers like Google GCS, Amazon AWS S3, etc. By default, TechDocs
uses the local filesystem of the `techdocs-backend` plugin in the "Basic" setup. And in the recommended setup,
having one of the cloud storage is a prerequisite. Read more on the TechDocs Architecture documentation page.
On this page you you can read how to enable them.
## Configuring Google GCS Bucket with TechDocs
Follow the [official Google Cloud documentation](https://googleapis.dev/nodejs/storage/latest/index.html#quickstart) for the latest instructions
on the following steps involving GCP.
**1. Set `techdocs.publisher.type` config in your `app-config.yaml`**
Set `techdocs.publisher.type` to `'google_gcs'`.
```yaml
techdocs:
publisher:
type: 'google_gcs'
```
**2. GCP (Google Cloud Platform) Project**
Create or choose a dedicated GCP project. Set `techdocs.publisher.google.projectId` to the project ID.
```yaml
techdocs:
publisher:
type: 'google_gcs'
google:
projectId: 'gcp-project-id
```
**3. Service account API key**
Create a new Service Account and a key associated with it. In roles of the service account, use "Storage Admin".
If you want to create a custom role, make sure to include both `get` and `create` permissions for both "Objects" and "Buckets". See https://cloud.google.com/storage/docs/access-control/iam-permissions
A service account can have many keys. Open your newly created account's page (in IAM & Admin console), and create a new key. Use JSON format for the key.
A `<GCP-PROJECT-ID-random-uid>.json` file will be downloaded. This is the secret key TechDocs will use to make API calls. Make it available in your Backstage server and/or your local development server and set it in the app config `techdocs.publisher.google.pathToKey`.
```yaml
techdocs:
publisher:
type: 'google_gcs'
google:
projectId: 'gcp-project-id'
pathToKey: '/path/to/google_application_credentials.json'
```
**4. GCS Bucket**
Create a dedicated bucket for TechDocs sites. techdocs-backend will publish documentation to this bucket.
TechDocs will fetch files from here to serve documentation in Backstage.
Set the name of the bucket to `techdocs.publisher
```yaml
techdocs:
publisher:
type: 'google_gcs'
google:
projectId: 'gcp-project-id'
pathToKey: '/path/to/google_application_credentials.json'
bucketName: 'name-of-techdocs-storage-bucket'
```
**5. That's it!**
Your Backstage app is now ready to use Google Cloud Storage for TechDocs, to store the static generated documentation files.
+2
View File
@@ -74,6 +74,8 @@
"features/techdocs/concepts",
"features/techdocs/architecture",
"features/techdocs/creating-and-publishing",
"features/techdocs/configuration",
"features/techdocs/using-cloud-storage",
"features/techdocs/troubleshooting",
"features/techdocs/faqs"
]
+2
View File
@@ -54,6 +54,8 @@ nav:
- Concepts: 'features/techdocs/concepts.md'
- TechDocs Architecture: 'features/techdocs/architecture.md'
- Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md'
- Configuration: 'features/techdocs/configuration.md'
- Using Cloud Storage: 'features/techdocs/using-cloud-storage.md'
- Troubleshooting: 'features/techdocs/troubleshooting.md'
- FAQ: 'features/techdocs/FAQ.md'
- Plugins:
+3 -1
View File
@@ -39,14 +39,16 @@
"@backstage/backend-common": "^0.3.2",
"@backstage/catalog-model": "^0.3.1",
"@backstage/config": "^0.1.1",
"@google-cloud/storage": "^5.6.0",
"@types/dockerode": "^2.5.34",
"@types/express-serve-static-core": "^4.17.14",
"@types/klaw": "^3.0.1",
"cross-fetch": "^3.0.6",
"dockerode": "^3.2.1",
"express": "^4.17.1",
"fs-extra": "^9.0.1",
"git-url-parse": "^11.4.0",
"js-yaml": "^3.14.0",
"klaw": "^3.0.0",
"mock-fs": "^4.13.0",
"nodegit": "^0.27.0",
"winston": "^3.2.1"
@@ -0,0 +1,222 @@
/*
* 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 path from 'path';
import express from 'express';
import walk from 'klaw';
import { Storage, UploadResponse } from '@google-cloud/storage';
import { Logger } from 'winston';
import { Entity, EntityName } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { getHeadersForFileExtension, supportedFileType } from './helpers';
import { PublisherBase, PublisherBaseParams } from './types';
export class GoogleGCSPublish implements PublisherBase {
static fromConfig(config: Config, logger: Logger): PublisherBase {
let pathToKey = '';
let projectId = '';
let bucketName = '';
try {
pathToKey = config.getString('techdocs.publisher.google.pathToKey');
projectId = config.getString('techdocs.publisher.google.projectId');
bucketName = config.getString('techdocs.publisher.google.bucketName');
} catch (error) {
throw new Error(
"Since techdocs.publisher.type is set to 'google_gcs' in your app config, " +
'pathToKey, projectId and bucketName are required in techdocs.publisher.google ' +
'required to authenticate with Google Cloud Storage.',
);
}
const storageClient = new Storage({
projectId: projectId,
keyFilename: pathToKey,
});
// Check if the defined bucket exists. Being able to connect means the configuration is good
// and the storage client will work.
storageClient
.bucket(bucketName)
.getMetadata()
.then(() => {
logger.info(
`Successfully connected to the GCS bucket ${bucketName} in the GCP project ${projectId}.`,
);
})
.catch(reason => {
logger.error(
`Could not retrieve metadata about the GCS bucket ${bucketName} in the GCP project ${projectId}. ` +
'Make sure the GCP project and the bucket exists and the access key located at the path ' +
"techdocs.publisher.google.pathToKey defined in app config has the role 'Storage Object Creator'. " +
'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
throw new Error(`from GCS client library: ${reason.message}`);
});
return new GoogleGCSPublish(storageClient, bucketName, logger);
}
constructor(
private readonly storageClient: Storage,
private readonly bucketName: string,
private readonly logger: Logger,
) {
this.storageClient = storageClient;
this.bucketName = bucketName;
this.logger = logger;
}
/**
* Upload all the files from the generated `directory` to the GCS bucket.
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
*/
publish({ entity, directory }: PublisherBaseParams): Promise<{}> {
return new Promise((resolve, reject) => {
// Path of all files to upload, relative to the root of the source directory
// e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png']
const allFilesToUpload: Array<string> = [];
// Iterate on all the files in the directory and its sub-directories
walk(directory)
.on('data', (item: walk.Item) => {
// GCS manages creation of parent directories if they do not exist.
// So collecting path of only the files is good enough.
if (item.stats.isFile()) {
// Remove the absolute path prefix of the source directory
const relativeFilePath = item.path.replace(`${directory}/`, '');
allFilesToUpload.push(relativeFilePath);
}
})
.on('error', (err: Error, item: walk.Item) => {
const errorMessage = `Unable to read file at ${item.path}. Error ${err.message}`;
this.logger.error(errorMessage);
reject(errorMessage);
})
.on('end', () => {
// 'end' event happens when all the files have been read.
const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
allFilesToUpload.forEach(filePath => {
const source = path.join(directory, filePath); // Local file absolutely path
const destination = `${entityRootDir}/${filePath}`; // GCS Bucket file relative path
this.storageClient
.bucket(this.bucketName)
.upload(source, { destination })
.then(
(uploadResp: UploadResponse) => ({
fileName: destination,
status: uploadResp[0],
}),
(err: Error) => {
const errorMessage = `Unable to upload file ${destination} to GCS. Error ${err.message}`;
this.logger.error(errorMessage);
reject(errorMessage);
},
);
});
this.logger.info(
`Successfully uploaded all the generated files for Entity ${entityRootDir}. Total number of files: ${allFilesToUpload.length}`,
);
resolve({});
});
});
}
fetchTechDocsMetadata(entityName: EntityName): Promise<string> {
return new Promise((resolve, reject) => {
const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
const fileStreamChunks: Array<any> = [];
this.storageClient
.bucket(this.bucketName)
.file(`${entityRootDir}/techdocs_metadata.json`)
.createReadStream()
.on('error', err => {
this.logger.error(err.message);
reject(err.message);
})
.on('data', chunk => {
fileStreamChunks.push(chunk);
})
.on('end', () => {
const techdocsMetadataJson = Buffer.concat(
fileStreamChunks,
).toString();
resolve(techdocsMetadataJson);
});
});
}
/**
* Express route middleware to serve static files on a route in techdocs-backend.
*/
docsRouter(): express.Handler {
return (req, res) => {
// Trim the leading forward slash
// filePath example - /default/Component/documented-component/index.html
const filePath = req.path.replace(/^\//, '');
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = filePath.split('.')[filePath.split('.').length - 1];
const responseHeaders = getHeadersForFileExtension(
fileExtension as supportedFileType,
);
const fileStreamChunks: Array<any> = [];
this.storageClient
.bucket(this.bucketName)
.file(filePath)
.createReadStream()
.on('error', err => {
this.logger.error(err.message);
res.send(err.message);
})
.on('data', chunk => {
fileStreamChunks.push(chunk);
})
.on('end', () => {
const fileContent = Buffer.concat(fileStreamChunks).toString();
// Inject response headers
for (const [headerKey, headerValue] of Object.entries(
responseHeaders,
)) {
res.setHeader(headerKey, headerValue);
}
res.send(fileContent);
});
};
}
/**
* A helper function which checks if index.html of an Entity's docs site is available. This
* can be used to verify if there are any pre-generated docs available to serve.
*/
async hasDocsBeenGenerated(entity: Entity): Promise<boolean> {
return new Promise(resolve => {
const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
this.storageClient
.bucket(this.bucketName)
.file(`${entityRootDir}/index.html`)
.createReadStream()
.on('error', () => {
resolve(false);
})
.on('data', () => {
resolve(true);
});
});
}
}
@@ -0,0 +1,46 @@
/*
* 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 type supportedFileType = 'html' | 'css';
export type responseHeadersType = {
'Content-Type': string;
};
export const getHeadersForFileExtension = (
fileType: supportedFileType,
): responseHeadersType => {
const headersCommon = {
'Content-Type': 'text/plain',
};
const headersHTML = {
...headersCommon,
'Content-Type': 'text/html; charset=UTF-8',
};
const headersCSS = {
...headersCommon,
'Content-Type': 'text/css; charset=UTF-8',
};
switch (fileType) {
case 'html':
return headersHTML;
case 'css':
return headersCSS;
default:
return headersCommon;
}
};
@@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import * as expressServeStaticCore from 'express-serve-static-core'; // Type from express library
import fetch from 'cross-fetch';
import express from 'express';
import fs from 'fs-extra';
import { Logger } from 'winston';
import { EntityName } from '@backstage/catalog-model';
import { Entity, EntityName } from '@backstage/catalog-model';
import {
resolvePackagePath,
PluginEndpointDiscovery,
@@ -92,9 +91,7 @@ export class LocalPublish implements PublisherBase {
});
}
fetchTechDocsMetadata(
entityName: EntityName,
): Promise<{ techdocsMetadataJson: string }> {
fetchTechDocsMetadata(entityName: EntityName): Promise<string> {
return new Promise((resolve, reject) => {
this.discovery.getBaseUrl('techdocs').then(techdocsApiUrl => {
const storageUrl = new URL(
@@ -102,8 +99,8 @@ export class LocalPublish implements PublisherBase {
techdocsApiUrl,
).toString();
const path = `${entityName.kind}/${entityName.namespace}/${entityName.name}`;
const metadataURL = `${storageUrl}/${path}/techdocs_metadata.json`;
const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
const metadataURL = `${storageUrl}/${entityRootDir}/techdocs_metadata.json`;
fetch(metadataURL)
.then(response =>
response
@@ -111,18 +108,37 @@ export class LocalPublish implements PublisherBase {
.then(techdocsMetadataJson => resolve(techdocsMetadataJson))
.catch(err => {
reject(
`Unable to parse metadata JSON for ${path}. Error: ${err}`,
`Unable to parse metadata JSON for ${entityRootDir}. Error: ${err}`,
);
}),
)
.catch(err => {
reject(`Unable to fetch metadata for ${path}. Error ${err}`);
reject(
`Unable to fetch metadata for ${entityRootDir}. Error ${err}`,
);
});
});
});
}
docsRouter(): expressServeStaticCore.Handler {
docsRouter(): express.Handler {
return express.static(staticDocsDir);
}
async hasDocsBeenGenerated(entity: Entity): Promise<boolean> {
return new Promise(resolve => {
this.discovery.getBaseUrl('techdocs').then(techdocsApiUrl => {
const storageUrl = new URL(
new URL(this.config.getString('techdocs.storageUrl')).pathname,
techdocsApiUrl,
).toString();
const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
const indexHtmlUrl = `${storageUrl}/${entityRootDir}/index.html`;
fetch(indexHtmlUrl)
.then(() => resolve(true))
.catch(() => resolve(false));
});
});
}
}
@@ -19,6 +19,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { PublisherType, PublisherBase } from './types';
import { LocalPublish } from './local';
import { GoogleGCSPublish } from './googleStorage';
/**
* Factory class to create a TechDocs publisher based on defined publisher type in app config.
@@ -37,7 +38,7 @@ export class Publisher {
switch (publisherType) {
case 'google_gcs':
logger.info('Creating Google Storage Bucket publisher for TechDocs');
return new LocalPublish(config, logger, discovery);
return GoogleGCSPublish.fromConfig(config, logger);
case 'local':
logger.info('Creating Local publisher for TechDocs');
return new LocalPublish(config, logger, discovery);
@@ -14,9 +14,7 @@
* limitations under the License.
*/
import { Entity, EntityName } from '@backstage/catalog-model';
// import serveStatic from 'serve-static';
// import express from 'express';
import * as express from 'express-serve-static-core';
import express from 'express';
/**
* Key for all the different types of TechDocs publishers that are supported.
@@ -49,12 +47,15 @@ export interface PublisherBase {
* Retrieve TechDocs Metadata about a site e.g. name, contributors, last updated, etc.
* This API uses the techdocs_metadata.json file that co-exists along with the generated docs.
*/
fetchTechDocsMetadata(
entityName: EntityName,
): Promise<{ techdocsMetadataJson: string }>;
fetchTechDocsMetadata(entityName: EntityName): Promise<string>;
/**
*
* Route middleware to serve static documentation files for an entity.
*/
docsRouter(): express.Handler;
/**
* Check if the index.html is present for the Entity at the Storage location.
*/
hasDocsBeenGenerated(entityName: Entity): Promise<boolean>;
}
@@ -0,0 +1,129 @@
/*
* 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 Docker from 'dockerode';
import { Logger } from 'winston';
import { Entity } from '@backstage/catalog-model';
import {
PreparerBuilder,
PublisherBase,
GeneratorBuilder,
PreparerBase,
GeneratorBase,
getLocationForEntity,
getLastCommitTimestamp,
} from '@backstage/techdocs-common';
import { BuildMetadataStorage } from '.';
const getEntityId = (entity: Entity) => {
return `${entity.kind}:${entity.metadata.namespace ?? ''}:${
entity.metadata.name
}`;
};
type DocsBuilderArguments = {
preparers: PreparerBuilder;
generators: GeneratorBuilder;
publisher: PublisherBase;
entity: Entity;
logger: Logger;
dockerClient: Docker;
};
export class DocsBuilder {
private preparer: PreparerBase;
private generator: GeneratorBase;
private publisher: PublisherBase;
private entity: Entity;
private logger: Logger;
private dockerClient: Docker;
constructor({
preparers,
generators,
publisher,
entity,
logger,
dockerClient,
}: DocsBuilderArguments) {
this.preparer = preparers.get(entity);
this.generator = generators.get(entity);
this.publisher = publisher;
this.entity = entity;
this.logger = logger;
this.dockerClient = dockerClient;
}
public async build() {
this.logger.info(`Running preparer on entity ${getEntityId(this.entity)}`);
const preparedDir = await this.preparer.prepare(this.entity);
const parsedLocationAnnotation = getLocationForEntity(this.entity);
this.logger.info(`Running generator on entity ${getEntityId(this.entity)}`);
const { resultDir } = await this.generator.run({
directory: preparedDir,
dockerClient: this.dockerClient,
parsedLocationAnnotation,
});
this.logger.info(`Running publisher on entity ${getEntityId(this.entity)}`);
await this.publisher.publish({
entity: this.entity,
directory: resultDir,
});
if (!this.entity.metadata.uid) {
throw new Error(
'Trying to build documentation for entity not in service catalog',
);
}
new BuildMetadataStorage(this.entity.metadata.uid).storeBuildTimestamp();
}
public async docsUpToDate() {
if (!this.entity.metadata.uid) {
throw new Error(
'Trying to build documentation for entity not in service catalog',
);
}
const buildMetadataStorage = new BuildMetadataStorage(
this.entity.metadata.uid,
);
const { type, target } = getLocationForEntity(this.entity);
// Unless docs are stored locally
const nonAgeCheckTypes = ['dir', 'file', 'url'];
if (!nonAgeCheckTypes.includes(type)) {
const lastCommit = await getLastCommitTimestamp(target, this.logger);
const storageTimeStamp = buildMetadataStorage.getTimestamp();
// Check if documentation source is newer than what we have
if (storageTimeStamp && storageTimeStamp >= lastCommit) {
this.logger.debug(
`Docs for entity ${getEntityId(this.entity)} is up to date.`,
);
return true;
}
}
this.logger.debug(
`Docs for entity ${getEntityId(this.entity)} was outdated.`,
);
return false;
}
}
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export * from './BuildMetadataStorage';
export * from './builder';
+1
View File
@@ -15,4 +15,5 @@
*/
export * from './service/router';
// TODO: Do named exports here e.g. publishers, generators.
export * from '@backstage/techdocs-common';
@@ -0,0 +1,26 @@
/*
* 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 { getEntityNameFromUrlPath } from './helpers';
describe('getEntityNameFromUrlPath', () => {
it('should parse correctly', () => {
const path = 'default/Component/documented-component';
const parsedEntity = getEntityNameFromUrlPath(path);
expect(parsedEntity).toHaveProperty('namespace', 'default');
expect(parsedEntity).toHaveProperty('kind', 'Component');
expect(parsedEntity).toHaveProperty('name', 'documented-component');
});
});
+3 -117
View File
@@ -13,132 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Docker from 'dockerode';
import { Logger } from 'winston';
import { Entity, EntityName } from '@backstage/catalog-model';
import {
PreparerBuilder,
PublisherBase,
GeneratorBuilder,
PreparerBase,
GeneratorBase,
getLocationForEntity,
getLastCommitTimestamp,
} from '@backstage/techdocs-common';
import { BuildMetadataStorage } from '../storage';
const getEntityId = (entity: Entity) => {
return `${entity.kind}:${entity.metadata.namespace ?? ''}:${
entity.metadata.name
}`;
};
type DocsBuilderArguments = {
preparers: PreparerBuilder;
generators: GeneratorBuilder;
publisher: PublisherBase;
entity: Entity;
logger: Logger;
dockerClient: Docker;
};
export class DocsBuilder {
private preparer: PreparerBase;
private generator: GeneratorBase;
private publisher: PublisherBase;
private entity: Entity;
private logger: Logger;
private dockerClient: Docker;
constructor({
preparers,
generators,
publisher,
entity,
logger,
dockerClient,
}: DocsBuilderArguments) {
this.preparer = preparers.get(entity);
this.generator = generators.get(entity);
this.publisher = publisher;
this.entity = entity;
this.logger = logger;
this.dockerClient = dockerClient;
}
public async build() {
this.logger.info(`Running preparer on entity ${getEntityId(this.entity)}`);
const preparedDir = await this.preparer.prepare(this.entity);
const parsedLocationAnnotation = getLocationForEntity(this.entity);
this.logger.info(`Running generator on entity ${getEntityId(this.entity)}`);
const { resultDir } = await this.generator.run({
directory: preparedDir,
dockerClient: this.dockerClient,
parsedLocationAnnotation,
});
this.logger.info(`Running publisher on entity ${getEntityId(this.entity)}`);
await this.publisher.publish({
entity: this.entity,
directory: resultDir,
});
if (!this.entity.metadata.uid) {
throw new Error(
'Trying to build documentation for entity not in service catalog',
);
}
new BuildMetadataStorage(this.entity.metadata.uid).storeBuildTimestamp();
}
public async docsUpToDate() {
if (!this.entity.metadata.uid) {
throw new Error(
'Trying to build documentation for entity not in service catalog',
);
}
const buildMetadataStorage = new BuildMetadataStorage(
this.entity.metadata.uid,
);
const { type, target } = getLocationForEntity(this.entity);
// Unless docs are stored locally
const nonAgeCheckTypes = ['dir', 'file', 'url'];
if (!nonAgeCheckTypes.includes(type)) {
const lastCommit = await getLastCommitTimestamp(target, this.logger);
const storageTimeStamp = buildMetadataStorage.getTimestamp();
// Check if documentation source is newer than what we have
if (storageTimeStamp && storageTimeStamp >= lastCommit) {
this.logger.debug(
`Docs for entity ${getEntityId(this.entity)} is up to date.`,
);
return true;
}
}
this.logger.debug(
`Docs for entity ${getEntityId(this.entity)} was outdated.`,
);
return false;
}
}
import { EntityName } from '@backstage/catalog-model';
/**
* Using the path of the TechDocs page URL, return a structured EntityName type object with namespace,
* kind and name of the Entity.
* @param {string} path Example: default/Component/documented-component
*/
export const getEntityNameFromUrlPath = (path: string): EntityName => {
const [kind, namespace, name] = path.split('/');
const [namespace, kind, name] = path.split('/');
return {
kind,
namespace,
kind,
name,
};
};
+46 -13
View File
@@ -28,7 +28,8 @@ import {
} from '@backstage/techdocs-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { DocsBuilder, getEntityNameFromUrlPath } from './helpers';
import { getEntityNameFromUrlPath } from './helpers';
import { DocsBuilder } from '../DocsBuilder';
type RouterOptions = {
preparers: PreparerBuilder;
@@ -92,9 +93,8 @@ export async function createRouter({
});
router.get('/docs/:namespace/:kind/:name/*', async (req, res) => {
const storageUrl = config.getString('techdocs.storageUrl');
const { kind, namespace, name } = req.params;
const storageUrl = config.getString('techdocs.storageUrl');
const catalogUrl = await discovery.getBaseUrl('catalog');
const triple = [kind, namespace, name].map(encodeURIComponent).join('/');
@@ -109,22 +109,55 @@ export async function createRouter({
const entity: Entity = await catalogRes.json();
const docsBuilder = new DocsBuilder({
preparers,
generators,
publisher,
dockerClient,
logger,
entity,
});
let publisherType = '';
try {
publisherType = config.getString('techdocs.publisher.type');
} catch (err) {
throw new Error(
'Unable to get techdocs.publisher.type in your app config. Set it to either ' +
"'local', 'google_gcs' or other support storage providers. Read more here " +
'https://backstage.io/docs/features/techdocs/architecture',
);
}
if (!(await docsBuilder.docsUpToDate())) {
await docsBuilder.build();
// techdocs-backend will only try to build documentation for an entity if techdocs.docsBuilder is set to 'local'
// If set to 'ci', it will only try to fetch and assume that that CI/CD pipeline of the repository hosting the
// entity's documentation is responsible for building and publishing documentation to the storage provider.
const shouldBuildDocs =
config.getString('techdocs.docsBuilder') === 'local';
if (shouldBuildDocs) {
const docsBuilder = new DocsBuilder({
preparers,
generators,
publisher,
dockerClient,
logger,
entity,
});
if (publisherType === 'local') {
if (!(await docsBuilder.docsUpToDate())) {
await docsBuilder.build();
}
} else if (publisherType === 'google_gcs') {
if (!(await publisher.hasDocsBeenGenerated(entity))) {
logger.info(
'Did not find generated docs files for the entity. Building docs.',
);
await docsBuilder.build();
} else {
logger.info(
'Found pre-generated docs for this entity. Serving them.',
);
// TODO: When to re-trigger build for cache invalidation?
}
}
}
res.redirect(`${storageUrl}${req.path.replace('/docs', '')}`);
});
// Route middleware which serves files from the storage set in the publisher.
router.use('/static/docs', publisher.docsRouter());
return router;
+297 -7
View File
@@ -1828,6 +1828,65 @@
query-string "^6.13.3"
xcase "^2.0.1"
"@google-cloud/common@^3.5.0":
version "3.5.0"
resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.5.0.tgz#0959e769e8075a06eb0823cc567eef00fd0c2d02"
integrity sha512-10d7ZAvKhq47L271AqvHEd8KzJqGU45TY+rwM2Z3JHuB070FeTi7oJJd7elfrnKaEvaktw3hH2wKnRWxk/3oWQ==
dependencies:
"@google-cloud/projectify" "^2.0.0"
"@google-cloud/promisify" "^2.0.0"
arrify "^2.0.1"
duplexify "^4.1.1"
ent "^2.2.0"
extend "^3.0.2"
google-auth-library "^6.1.1"
retry-request "^4.1.1"
teeny-request "^7.0.0"
"@google-cloud/paginator@^3.0.0":
version "3.0.5"
resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz#9d6b96c421a89bd560c1bc2c197c7611ef21db6c"
integrity sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw==
dependencies:
arrify "^2.0.0"
extend "^3.0.2"
"@google-cloud/projectify@^2.0.0":
version "2.0.1"
resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz#13350ee609346435c795bbfe133a08dfeab78d65"
integrity sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==
"@google-cloud/promisify@^2.0.0":
version "2.0.3"
resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz#f934b5cdc939e3c7039ff62b9caaf59a9d89e3a8"
integrity sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==
"@google-cloud/storage@^5.6.0":
version "5.6.0"
resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.6.0.tgz#bc6925c7970c375212a4da21c123298fc9665dec"
integrity sha512-nLcym8IuCzy1O7tNTXNFuMHfX900sTM3kSTqbKe7oFSoKUiaIM+FHuuuDimMMlieY6StA1xYNPRFFHz57Nv8YQ==
dependencies:
"@google-cloud/common" "^3.5.0"
"@google-cloud/paginator" "^3.0.0"
"@google-cloud/promisify" "^2.0.0"
arrify "^2.0.0"
compressible "^2.0.12"
date-and-time "^0.14.0"
duplexify "^4.0.0"
extend "^3.0.2"
gaxios "^4.0.0"
gcs-resumable-upload "^3.1.0"
get-stream "^6.0.0"
hash-stream-validation "^0.2.2"
mime "^2.2.0"
mime-types "^2.0.8"
onetime "^5.1.0"
p-limit "^3.0.1"
pumpify "^2.0.0"
snakeize "^0.1.0"
stream-events "^1.0.1"
xdg-basedir "^4.0.0"
"@graphql-codegen/cli@^1.17.7":
version "1.17.10"
resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.17.10.tgz#efebf9b887fdb94dd26dbf3eb1950e832efcda0e"
@@ -5492,6 +5551,13 @@
dependencies:
"@types/node" "*"
"@types/klaw@^3.0.1":
version "3.0.1"
resolved "https://registry.npmjs.org/@types/klaw/-/klaw-3.0.1.tgz#29f90021c0234976aa4eb97efced9cb6db9fa8b3"
integrity sha512-acnF3n9mYOr1aFJKFyvfNX0am9EtPUsYPq22QUCGdJE+MVt6UyAN1jwo+PmOPqXD4K7ZS9MtxDEp/un0lxFccA==
dependencies:
"@types/node" "*"
"@types/koa-compose@*":
version "3.2.5"
resolved "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d"
@@ -6521,6 +6587,13 @@ abbrev@1:
resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
abstract-logging@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.0.tgz#08a85814946c98ef06f4256ad470aba1886d4490"
@@ -7208,6 +7281,11 @@ arrify@^1.0.1:
resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
arrify@^2.0.0, arrify@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
asap@^2.0.0, asap@~2.0.3, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@@ -7807,7 +7885,7 @@ base64-js@^1.0.2, base64-js@^1.2.0:
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
base64-js@^1.3.1:
base64-js@^1.3.0, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@@ -7893,6 +7971,11 @@ big.js@^5.2.2:
resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bignumber.js@^9.0.0:
version "9.0.1"
resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5"
integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==
binary-extensions@^1.0.0:
version "1.13.1"
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
@@ -9134,7 +9217,7 @@ compress-commons@^4.0.0:
normalize-path "^3.0.0"
readable-stream "^3.6.0"
compressible@~2.0.16:
compressible@^2.0.12, compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
@@ -9228,7 +9311,7 @@ config-chain@^1.1.11:
ini "^1.3.4"
proto-list "~1.2.1"
configstore@^5.0.1:
configstore@^5.0.0, configstore@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==
@@ -10193,6 +10276,11 @@ dataloader@2.0.0:
resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz#41eaf123db115987e21ca93c005cd7753c55fe6f"
integrity sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==
date-and-time@^0.14.0:
version "0.14.1"
resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.1.tgz#969634697b78956fb66b8be6fb0f39fbd631f2f6"
integrity sha512-M4RggEH5OF2ZuCOxgOU67R6Z9ohjKbxGvAQz48vj53wLmL0bAgumkBvycR32f30pK+Og9pIR+RFDyChbaE4oLA==
date-fns@^1.27.2:
version "1.30.1"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
@@ -10874,6 +10962,16 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
duplexify@^4.0.0, duplexify@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61"
integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==
dependencies:
end-of-stream "^1.4.1"
inherits "^2.0.3"
readable-stream "^3.1.1"
stream-shift "^1.0.0"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -10882,7 +10980,7 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11:
ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
version "1.0.11"
resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
@@ -11026,6 +11124,11 @@ enquirer@^2.3.0, enquirer@^2.3.5:
dependencies:
ansi-colors "^4.1.1"
ent@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
entities@^1.1.1, entities@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
@@ -11574,6 +11677,11 @@ event-stream@=3.3.4:
stream-combiner "~0.0.4"
through "~2.3.1"
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter2@^6.4.2:
version "6.4.3"
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820"
@@ -11809,7 +11917,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
extend@3.0.2, extend@^3.0.0, extend@~3.0.2:
extend@3.0.2, extend@^3.0.0, extend@^3.0.2, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@@ -11936,6 +12044,11 @@ fast-shallow-equal@^1.0.0:
resolved "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
fast-text-encoding@^1.0.0:
version "1.0.3"
resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==
fastest-stable-stringify@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028"
@@ -12532,6 +12645,49 @@ gauge@~2.7.3:
strip-ansi "^3.0.1"
wide-align "^1.1.0"
gaxios@^3.0.0:
version "3.2.0"
resolved "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz#11b6f0e8fb08d94a10d4d58b044ad3bec6dd486a"
integrity sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q==
dependencies:
abort-controller "^3.0.0"
extend "^3.0.2"
https-proxy-agent "^5.0.0"
is-stream "^2.0.0"
node-fetch "^2.3.0"
gaxios@^4.0.0:
version "4.0.1"
resolved "https://registry.npmjs.org/gaxios/-/gaxios-4.0.1.tgz#bc7b205a89d883452822cc75e138620c35e3291e"
integrity sha512-jOin8xRZ/UytQeBpSXFqIzqU7Fi5TqgPNLlUsSB8kjJ76+FiGBfImF8KJu++c6J4jOldfJUtt0YmkRj2ZpSHTQ==
dependencies:
abort-controller "^3.0.0"
extend "^3.0.2"
https-proxy-agent "^5.0.0"
is-stream "^2.0.0"
node-fetch "^2.3.0"
gcp-metadata@^4.2.0:
version "4.2.1"
resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz#31849fbcf9025ef34c2297c32a89a1e7e9f2cd62"
integrity sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==
dependencies:
gaxios "^4.0.0"
json-bigint "^1.0.0"
gcs-resumable-upload@^3.1.0:
version "3.1.1"
resolved "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz#67c766a0555d6a352f9651b7603337207167d0de"
integrity sha512-RS1osvAicj9+MjCc6jAcVL1Pt3tg7NK2C2gXM5nqD1Gs0klF2kj5nnAFSBy97JrtslMIQzpb7iSuxaG8rFWd2A==
dependencies:
abort-controller "^3.0.0"
configstore "^5.0.0"
extend "^3.0.2"
gaxios "^3.0.0"
google-auth-library "^6.0.0"
pumpify "^2.0.0"
stream-events "^1.0.4"
generic-names@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/generic-names/-/generic-names-2.0.1.tgz#f8a378ead2ccaa7a34f0317b05554832ae41b872"
@@ -12636,6 +12792,11 @@ get-stream@^5.0.0, get-stream@^5.1.0:
dependencies:
pump "^3.0.0"
get-stream@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz#3e0012cb6827319da2706e601a1583e8629a6718"
integrity sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==
get-value@^2.0.3, get-value@^2.0.6:
version "2.0.6"
resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -12930,6 +13091,28 @@ good-listener@^1.2.2:
dependencies:
delegate "^3.1.2"
google-auth-library@^6.0.0, google-auth-library@^6.1.1:
version "6.1.3"
resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.3.tgz#39d868140b70d0c4b32c6f6d8f4ccc1400d84dca"
integrity sha512-m9mwvY3GWbr7ZYEbl61isWmk+fvTmOt0YNUfPOUY2VH8K5pZlAIWJjxEi0PqR3OjMretyiQLI6GURMrPSwHQ2g==
dependencies:
arrify "^2.0.0"
base64-js "^1.3.0"
ecdsa-sig-formatter "^1.0.11"
fast-text-encoding "^1.0.0"
gaxios "^4.0.0"
gcp-metadata "^4.2.0"
gtoken "^5.0.4"
jws "^4.0.0"
lru-cache "^6.0.0"
google-p12-pem@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e"
integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==
dependencies:
node-forge "^0.10.0"
got@^10.7.0:
version "10.7.0"
resolved "https://registry.npmjs.org/got/-/got-10.7.0.tgz#62889dbcd6cca32cd6a154cc2d0c6895121d091f"
@@ -13195,6 +13378,16 @@ growly@^1.3.0:
resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
gtoken@^5.0.4:
version "5.1.0"
resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.1.0.tgz#4ba8d2fc9a8459098f76e7e8fd7beaa39fda9fe4"
integrity sha512-4d8N6Lk8TEAHl9vVoRVMh9BNOKWVgl2DdNtr3428O75r3QFrF/a5MMu851VmK0AA8+iSvbwRv69k5XnMLURGhg==
dependencies:
gaxios "^4.0.0"
google-p12-pem "^3.0.3"
jws "^4.0.0"
mime "^2.2.0"
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
@@ -13326,6 +13519,11 @@ hash-base@^3.0.0:
inherits "^2.0.1"
safe-buffer "^5.0.1"
hash-stream-validation@^0.2.2:
version "0.2.4"
resolved "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512"
integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.7"
resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
@@ -13618,7 +13816,7 @@ http-proxy-agent@^2.1.0:
agent-base "4"
debug "3.1.0"
http-proxy-agent@^4.0.1:
http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==
@@ -15287,6 +15485,13 @@ jsesc@~0.5.0:
resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
json-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
dependencies:
bignumber.js "^9.0.0"
json-buffer@3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
@@ -15587,6 +15792,15 @@ jwa@^1.4.1:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jwa@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
@@ -15595,6 +15809,14 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
jws@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
dependencies:
jwa "^2.0.0"
safe-buffer "^5.0.1"
jwt-decode@2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
@@ -15657,6 +15879,13 @@ klaw@^1.0.0:
optionalDependencies:
graceful-fs "^4.1.9"
klaw@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146"
integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==
dependencies:
graceful-fs "^4.1.9"
kleur@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
@@ -16862,7 +17091,7 @@ mime-db@1.44.0, "mime-db@>= 1.43.0 < 2":
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
mime-types@^2.1.12, mime-types@^2.1.26, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.26, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
version "2.1.27"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
@@ -16874,6 +17103,11 @@ mime@1.6.0, mime@^1.4.1:
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.2.0:
version "2.4.6"
resolved "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
mime@^2.3.1, mime@^2.4.4:
version "2.4.4"
resolved "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
@@ -18177,6 +18411,13 @@ p-limit@^2.0.0, p-limit@^2.2.0:
dependencies:
p-try "^2.0.0"
p-limit@^3.0.1:
version "3.1.0"
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
dependencies:
yocto-queue "^0.1.0"
p-locate@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
@@ -19707,6 +19948,15 @@ pumpify@^1.3.3:
inherits "^2.0.3"
pump "^2.0.0"
pumpify@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e"
integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==
dependencies:
duplexify "^4.1.1"
inherits "^2.0.3"
pump "^3.0.0"
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
@@ -21183,6 +21433,13 @@ ret@~0.1.10:
resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
retry-request@^4.1.1:
version "4.1.3"
resolved "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde"
integrity sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ==
dependencies:
debug "^4.1.1"
retry@0.12.0, retry@^0.12.0:
version "0.12.0"
resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
@@ -21847,6 +22104,11 @@ smartwrap@^1.2.3:
wcwidth "^1.0.1"
yargs "^15.1.0"
snakeize@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d"
integrity sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -22298,6 +22560,13 @@ stream-each@^1.1.0:
end-of-stream "^1.1.0"
stream-shift "^1.0.0"
stream-events@^1.0.1, stream-events@^1.0.4, stream-events@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5"
integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==
dependencies:
stubs "^3.0.0"
stream-http@^2.7.2:
version "2.8.3"
resolved "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
@@ -22577,6 +22846,11 @@ strong-log-transformer@^2.0.0:
minimist "^1.2.0"
through "^2.3.4"
stubs@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b"
integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls=
style-inject@^0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3"
@@ -22953,6 +23227,17 @@ tdigest@^0.1.1:
dependencies:
bintrees "1.0.1"
teeny-request@^7.0.0:
version "7.0.1"
resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz#bdd41fdffea5f8fbc0d29392cb47bec4f66b2b4c"
integrity sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw==
dependencies:
http-proxy-agent "^4.0.0"
https-proxy-agent "^5.0.0"
node-fetch "^2.6.1"
stream-events "^1.0.5"
uuid "^8.0.0"
telejson@^5.0.2:
version "5.0.2"
resolved "https://registry.npmjs.org/telejson/-/telejson-5.0.2.tgz#ed1e64be250cc1c757a53c19e1740b49832b3d51"
@@ -25045,6 +25330,11 @@ yn@^4.0.0:
resolved "https://registry.npmjs.org/yn/-/yn-4.0.0.tgz#611480051ea43b510da1dfdbe177ed159f00a979"
integrity sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg==
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
yup@^0.29.3:
version "0.29.3"
resolved "https://registry.npmjs.org/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea"