Merge branch 'master' into feature/aws-assume-role
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-sonarqube': patch
|
||||
---
|
||||
|
||||
Fix bug retrieving current theme
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Added githubApp authentication to the scaffolder-backend plugin
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-pagerduty': minor
|
||||
---
|
||||
|
||||
Improved the UI of the pagerduty plugin, and added a standalone TriggerButton
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Improved error reporting in AzureBlobStorage to surface errors when fetching metadata and uploading files fails.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Pass user and group ID when invoking docker container. When TechDocs invokes Docker, docker could be run as a `root` user which results in generation of files by applications run by non-root user (e.g. TechDocs) will not have access to modify. This PR passes in current user and group ID to docker so that the file permissions of the generated files and folders are correct.
|
||||
@@ -181,7 +181,25 @@ The `IgnoringErrorApi` would then be imported in the app, and wired up like
|
||||
this:
|
||||
|
||||
```ts
|
||||
builder.add(errorApiRef, new IgnoringErrorApi());
|
||||
const app = createApp({
|
||||
apis: [
|
||||
/* ApiFactories */
|
||||
createApiFactory(errorApiRef, new IgnoringErrorApi()),
|
||||
|
||||
// OR
|
||||
// If your API has dependencies, you use the object form
|
||||
createApiFactory({
|
||||
api: errorApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory({ configApi }) {
|
||||
return new IgnoringErrorApi({
|
||||
reportingUrl: configApi.getString('error.reportingUrl'),
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
// ... other options
|
||||
});
|
||||
```
|
||||
|
||||
Note that the above line will cause an error if `IgnoreErrorApi` does not fully
|
||||
|
||||
@@ -142,6 +142,8 @@ variables**
|
||||
You should follow the
|
||||
[AWS security best practices guide for authentication](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html).
|
||||
|
||||
TechDocs needs access to read files and metadata of the S3 bucket.
|
||||
|
||||
If the environment variables
|
||||
|
||||
- `AWS_ACCESS_KEY_ID`
|
||||
@@ -149,15 +151,21 @@ If the environment variables
|
||||
- `AWS_REGION`
|
||||
|
||||
are set and can be used to access the bucket you created in step 2, they will be
|
||||
used by the AWS SDK v2 Node.js client for authentication.
|
||||
[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html)
|
||||
used by the AWS SDK V2 Node.js client for authentication.
|
||||
[Refer to the official documentation for loading credentials in Node.js from environment variables](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html).
|
||||
|
||||
If the environment variables are missing, the AWS SDK tries to read the
|
||||
`~/.aws/credentials` file for credentials.
|
||||
[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html)
|
||||
[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html)
|
||||
|
||||
Note that the region of the bucket has to be set for the AWS SDK to work.
|
||||
[See this](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html).
|
||||
If you are using Amazon EC2 instance to deploy Backstage, you do not need to
|
||||
obtain the access keys separately. They can be made available in the environment
|
||||
automatically by defining appropriate IAM role with access to the bucket. Read
|
||||
more in
|
||||
[official AWS documentation for using IAM roles.](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#use-roles).
|
||||
|
||||
The AWS Region of the bucket is optional since TechDocs uses AWS SDK V2 and not
|
||||
V3.
|
||||
|
||||
**3b. Authentication using app-config.yaml**
|
||||
|
||||
@@ -183,12 +191,6 @@ techdocs:
|
||||
Refer to the
|
||||
[official AWS documentation for obtaining the credentials](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html).
|
||||
|
||||
Note: If you are using Amazon EC2 instance to deploy Backstage, you do not need
|
||||
to obtain the access keys separately. They can be made available in the
|
||||
environment automatically by defining appropriate IAM role with access to the
|
||||
bucket. Read more
|
||||
[here](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#use-roles).
|
||||
|
||||
**3c. Authentication using an assumed role** Users with multiple AWS accounts
|
||||
may want to use a role for S3 storage that is in a different AWS account. Using
|
||||
the `roleArn` parameter as seen below, you can instruct the TechDocs publisher
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
EntityLinksCard,
|
||||
EntityPageLayout,
|
||||
} from '@backstage/plugin-catalog';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { EntityProvider, useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
isPluginApplicableToEntity as isCircleCIAvailable,
|
||||
Router as CircleCIRouter,
|
||||
@@ -184,7 +184,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
</Grid>
|
||||
{isPagerDutyAvailable(entity) && (
|
||||
<Grid item md={6}>
|
||||
<PagerDutyCard entity={entity} />
|
||||
<EntityProvider entity={entity}>
|
||||
<PagerDutyCard />
|
||||
</EntityProvider>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item md={4} sm={6}>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import type {
|
||||
BlobUploadCommonResponse,
|
||||
ContainerGetPropertiesResponse,
|
||||
} from '@azure/storage-blob';
|
||||
|
||||
export class BlockBlobClient {
|
||||
private readonly blobName;
|
||||
@@ -22,23 +26,33 @@ export class BlockBlobClient {
|
||||
this.blobName = blobName;
|
||||
}
|
||||
|
||||
uploadFile(source: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!fs.existsSync(source)) {
|
||||
reject('');
|
||||
} else {
|
||||
resolve('');
|
||||
}
|
||||
uploadFile(source: string): Promise<BlobUploadCommonResponse> {
|
||||
return Promise.resolve({
|
||||
_response: {
|
||||
request: {
|
||||
url: `https://example.blob.core.windows.net`,
|
||||
} as any,
|
||||
status: 200,
|
||||
headers: {} as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
exists() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (fs.existsSync(this.blobName)) {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject({ message: 'The object doest not exist !' });
|
||||
}
|
||||
return Promise.resolve(fs.existsSync(this.blobName));
|
||||
}
|
||||
}
|
||||
|
||||
class BlockBlobClientFailUpload extends BlockBlobClient {
|
||||
uploadFile(source: string): Promise<BlobUploadCommonResponse> {
|
||||
return Promise.resolve({
|
||||
_response: {
|
||||
request: {
|
||||
url: `https://example.blob.core.windows.net`,
|
||||
} as any,
|
||||
status: 500,
|
||||
headers: {} as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -50,9 +64,16 @@ export class ContainerClient {
|
||||
this.containerName = containerName;
|
||||
}
|
||||
|
||||
getProperties() {
|
||||
return new Promise(resolve => {
|
||||
resolve('');
|
||||
getProperties(): Promise<ContainerGetPropertiesResponse> {
|
||||
return Promise.resolve({
|
||||
_response: {
|
||||
request: {
|
||||
url: `https://example.blob.core.windows.net`,
|
||||
} as any,
|
||||
status: 200,
|
||||
headers: {} as any,
|
||||
parsedHeaders: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,6 +82,27 @@ export class ContainerClient {
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerClientFailGetProperties extends ContainerClient {
|
||||
getProperties(): Promise<ContainerGetPropertiesResponse> {
|
||||
return Promise.resolve({
|
||||
_response: {
|
||||
request: {
|
||||
url: `https://example.blob.core.windows.net`,
|
||||
} as any,
|
||||
status: 404,
|
||||
headers: {} as any,
|
||||
parsedHeaders: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerClientFailUpload extends ContainerClient {
|
||||
getBlockBlobClient(blobName: string) {
|
||||
return new BlockBlobClientFailUpload(blobName);
|
||||
}
|
||||
}
|
||||
|
||||
export class BlobServiceClient {
|
||||
private readonly url;
|
||||
private readonly credential;
|
||||
@@ -71,6 +113,12 @@ export class BlobServiceClient {
|
||||
}
|
||||
|
||||
getContainerClient(containerName: string) {
|
||||
if (containerName === 'bad_container') {
|
||||
return new ContainerClientFailGetProperties(containerName);
|
||||
}
|
||||
if (this.credential.accountName === 'failupload') {
|
||||
return new ContainerClientFailUpload(containerName);
|
||||
}
|
||||
return new ContainerClient(containerName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
patchMkdocsYmlPreBuild,
|
||||
runDockerContainer,
|
||||
storeEtagMetadata,
|
||||
UserOptions,
|
||||
} from './helpers';
|
||||
|
||||
const mockEntity = {
|
||||
@@ -114,7 +115,7 @@ describe('helpers', () => {
|
||||
imageName,
|
||||
args,
|
||||
expect.any(Stream),
|
||||
{
|
||||
expect.objectContaining({
|
||||
Volumes: {
|
||||
'/content': {},
|
||||
'/result': {},
|
||||
@@ -123,7 +124,7 @@ describe('helpers', () => {
|
||||
HostConfig: {
|
||||
Binds: [`${docsDir}:/content`, `${outputDir}:/result`],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -139,6 +140,30 @@ describe('helpers', () => {
|
||||
expect(mockDocker.ping).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass through the user and group id from the host machine and set the home dir', async () => {
|
||||
await runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
docsDir,
|
||||
outputDir,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
const userOptions: UserOptions = {};
|
||||
if (process.getuid && process.getgid) {
|
||||
userOptions.User = `${process.getuid()}:${process.getgid()}`;
|
||||
}
|
||||
|
||||
expect(mockDocker.run).toHaveBeenCalledWith(
|
||||
imageName,
|
||||
args,
|
||||
expect.any(Stream),
|
||||
expect.objectContaining({
|
||||
...userOptions,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('where docker is unavailable', () => {
|
||||
const dockerError = 'a docker error';
|
||||
|
||||
|
||||
@@ -51,6 +51,12 @@ export type RunCommandOptions = {
|
||||
logStream?: Writable;
|
||||
};
|
||||
|
||||
export type UserOptions = {
|
||||
User?: string;
|
||||
};
|
||||
|
||||
// To be replaced by a runDockerContainer from backend-common
|
||||
// shared between Scaffolder and TechDocs and any other plugin.
|
||||
export async function runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
@@ -78,6 +84,17 @@ export async function runDockerContainer({
|
||||
});
|
||||
});
|
||||
|
||||
const userOptions: UserOptions = {};
|
||||
// @ts-ignore
|
||||
if (process.getuid && process.getgid) {
|
||||
// Files that are created inside the Docker container will be owned by
|
||||
// root on the host system on non Mac systems, because of reasons. Mainly the fact that
|
||||
// volume sharing is done using NFS on Mac and actual mounts in Linux world.
|
||||
// So we set the user in the container as the same user and group id as the host.
|
||||
// On Windows we don't have process.getuid nor process.getgid
|
||||
userOptions.User = `${process.getuid()}:${process.getgid()}`;
|
||||
}
|
||||
|
||||
const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run(
|
||||
imageName,
|
||||
args,
|
||||
@@ -91,6 +108,7 @@ export async function runDockerContainer({
|
||||
HostConfig: {
|
||||
Binds: [`${docsDir}:/content`, `${outputDir}:/result`],
|
||||
},
|
||||
...userOptions,
|
||||
...createOptions,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -53,10 +53,13 @@ export class AwsS3Publish implements PublisherBase {
|
||||
);
|
||||
}
|
||||
|
||||
// Credentials is an optional config. If missing, default AWS environment variables
|
||||
// or AWS shared credentials file at ~/.aws/credentials will be used to authenticate
|
||||
// https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html
|
||||
// https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html
|
||||
// Credentials is an optional config. If missing, the default ways of authenticating AWS SDK V2 will be used.
|
||||
// 1. AWS environment variables
|
||||
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
|
||||
// 2. AWS shared credentials file at ~/.aws/credentials
|
||||
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html
|
||||
// 3. IAM Roles for EC2
|
||||
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-iam.html
|
||||
const credentialsConfig = config.getOptionalConfig(
|
||||
'techdocs.publisher.awsS3.credentials',
|
||||
);
|
||||
@@ -88,9 +91,7 @@ export class AwsS3Publish implements PublisherBase {
|
||||
}
|
||||
|
||||
// AWS Region is an optional config. If missing, default AWS env variable AWS_REGION
|
||||
// or AWS shared credentials file at ~/.aws/credentials will be used. Any way, AWS SDK v3 client needs
|
||||
// to have the AWS Region information for it to work.
|
||||
// https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html
|
||||
// or AWS shared credentials file at ~/.aws/credentials will be used.
|
||||
const region = config.getOptionalString('techdocs.publisher.awsS3.region');
|
||||
|
||||
const storageClient = new aws.S3({
|
||||
|
||||
@@ -19,6 +19,7 @@ import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { AzureBlobStoragePublish } from './azureBlobStorage';
|
||||
import { PublisherBase } from './types';
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import type { Logger } from 'winston';
|
||||
|
||||
const createMockEntity = (annotations = {}) => {
|
||||
return {
|
||||
@@ -43,33 +44,40 @@ const getEntityRootDir = (entity: Entity) => {
|
||||
return entityRootDir;
|
||||
};
|
||||
|
||||
const logger = getVoidLogger();
|
||||
jest.spyOn(logger, 'info').mockReturnValue(logger);
|
||||
jest.spyOn(logger, 'error').mockReturnValue(logger);
|
||||
function createLogger() {
|
||||
const logger = getVoidLogger();
|
||||
jest.spyOn(logger, 'info').mockReturnValue(logger);
|
||||
jest.spyOn(logger, 'error').mockReturnValue(logger);
|
||||
return logger;
|
||||
}
|
||||
|
||||
let publisher: PublisherBase;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
requestUrl: 'http://localhost:7000',
|
||||
publisher: {
|
||||
type: 'azureBlobStorage',
|
||||
azureBlobStorage: {
|
||||
credentials: {
|
||||
accountName: 'accountName',
|
||||
accountKey: 'accountKey',
|
||||
describe('publishing with valid credentials', () => {
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
requestUrl: 'http://localhost:7000',
|
||||
publisher: {
|
||||
type: 'azureBlobStorage',
|
||||
azureBlobStorage: {
|
||||
credentials: {
|
||||
accountName: 'accountName',
|
||||
accountKey: 'accountKey',
|
||||
},
|
||||
containerName: 'containerName',
|
||||
},
|
||||
containerName: 'containerName',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger = createLogger();
|
||||
|
||||
publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger);
|
||||
});
|
||||
|
||||
publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger);
|
||||
});
|
||||
|
||||
describe('AzureBlobStoragePublish', () => {
|
||||
describe('publish', () => {
|
||||
it('should publish a directory', async () => {
|
||||
const entity = createMockEntity();
|
||||
@@ -116,7 +124,7 @@ describe('AzureBlobStoragePublish', () => {
|
||||
})
|
||||
.catch(error =>
|
||||
expect(error.message).toContain(
|
||||
'Unable to upload file(s) to Azure Blob Storage. Error Failed to read template directory: ENOENT, no such file or directory',
|
||||
'Unable to upload file(s) to Azure Blob Storage. Failed to read template directory: ENOENT, no such file or directory',
|
||||
),
|
||||
);
|
||||
mockFs.restore();
|
||||
@@ -145,3 +153,92 @@ describe('AzureBlobStoragePublish', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error reporting', () => {
|
||||
it('reports an error when unable to read container properties', async () => {
|
||||
const mockConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
requestUrl: 'http://localhost:7000',
|
||||
publisher: {
|
||||
type: 'azureBlobStorage',
|
||||
azureBlobStorage: {
|
||||
credentials: {
|
||||
accountName: 'accountName',
|
||||
},
|
||||
containerName: 'bad_container',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const logger = createLogger();
|
||||
|
||||
let error;
|
||||
try {
|
||||
publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`Could not retrieve metadata about the Azure Blob Storage container bad_container.`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('reports an error when bad account credentials', async () => {
|
||||
const mockConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
requestUrl: 'http://localhost:7000',
|
||||
publisher: {
|
||||
type: 'azureBlobStorage',
|
||||
azureBlobStorage: {
|
||||
credentials: {
|
||||
accountName: 'failupload',
|
||||
accountKey: 'accountKey',
|
||||
},
|
||||
containerName: 'containerName',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const logger = createLogger();
|
||||
|
||||
publisher = await AzureBlobStoragePublish.fromConfig(mockConfig, logger);
|
||||
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'index.html': '',
|
||||
},
|
||||
});
|
||||
|
||||
let error;
|
||||
try {
|
||||
await publisher.publish({
|
||||
entity,
|
||||
directory: entityRootDir,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error.message).toContain(
|
||||
`Unable to upload file(s) to Azure Blob Storage.`,
|
||||
);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
`Unable to upload file(s) to Azure Blob Storage. Upload failed for test-namespace/TestKind/test-component-name/index.html with status code 500`,
|
||||
),
|
||||
);
|
||||
|
||||
mockFs.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ import platformPath from 'path';
|
||||
import express from 'express';
|
||||
import {
|
||||
BlobServiceClient,
|
||||
BlobUploadCommonResponse,
|
||||
StorageSharedKeyCredential,
|
||||
} from '@azure/storage-blob';
|
||||
import { DefaultAzureCredential } from '@azure/identity';
|
||||
@@ -79,25 +78,25 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
credential,
|
||||
);
|
||||
|
||||
await storageClient
|
||||
.getContainerClient(containerName)
|
||||
.getProperties()
|
||||
.then(() => {
|
||||
logger.info(
|
||||
`Successfully connected to the Azure Blob Storage container ${containerName}.`,
|
||||
);
|
||||
})
|
||||
.catch(reason => {
|
||||
logger.error(
|
||||
`Could not retrieve metadata about the Azure Blob Storage container ${containerName}. ` +
|
||||
'Make sure that the Azure project and container exist and the access key is setup correctly ' +
|
||||
'techdocs.publisher.azureBlobStorage.credentials defined in app config has correct permissions. ' +
|
||||
'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
|
||||
);
|
||||
try {
|
||||
const response = await storageClient
|
||||
.getContainerClient(containerName)
|
||||
.getProperties();
|
||||
|
||||
if (response._response.status >= 400) {
|
||||
throw new Error(
|
||||
`from Azure Blob Storage client library: ${reason.message}`,
|
||||
`Failed to retrieve metadata from ${response._response.request.url} with status code ${response._response.status}.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Could not retrieve metadata about the Azure Blob Storage container ${containerName}. ` +
|
||||
'Make sure that the Azure project and container exist and the access key is setup correctly ' +
|
||||
'techdocs.publisher.azureBlobStorage.credentials defined in app config has correct permissions. ' +
|
||||
'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
|
||||
);
|
||||
throw new Error(`from Azure Blob Storage client library: ${e.message}`);
|
||||
}
|
||||
|
||||
return new AzureBlobStoragePublish(storageClient, containerName, logger);
|
||||
}
|
||||
@@ -122,8 +121,6 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
// So collecting path of only the files is good enough.
|
||||
const allFilesToUpload = await getFileTreeRecursively(directory);
|
||||
|
||||
const uploadPromises: Array<Promise<BlobUploadCommonResponse>> = [];
|
||||
|
||||
// Bound the number of concurrent batches. We want a bit of concurrency for
|
||||
// performance reasons, but not so much that we starve the connection pool
|
||||
// or start thrashing.
|
||||
@@ -140,23 +137,43 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
); // Azure Blob Storage Container file relative path
|
||||
|
||||
return limiter(async () => {
|
||||
await uploadPromises.push(
|
||||
this.storageClient
|
||||
.getContainerClient(this.containerName)
|
||||
.getBlockBlobClient(destination)
|
||||
.uploadFile(filePath),
|
||||
);
|
||||
const response = await this.storageClient
|
||||
.getContainerClient(this.containerName)
|
||||
.getBlockBlobClient(destination)
|
||||
.uploadFile(filePath);
|
||||
|
||||
if (response._response.status >= 400) {
|
||||
return {
|
||||
...response,
|
||||
error: new Error(
|
||||
`Upload failed for ${filePath} with status code ${response._response.status}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...response,
|
||||
error: undefined,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(promises).then(() => {
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
const failed = responses.filter(r => r.error);
|
||||
if (failed.length === 0) {
|
||||
this.logger.info(
|
||||
`Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`,
|
||||
`Successfully uploaded the ${responses.length} generated file(s) for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`,
|
||||
);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw new Error(
|
||||
failed
|
||||
.map(r => r.error?.message)
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = `Unable to upload file(s) to Azure Blob Storage. Error ${e.message}`;
|
||||
const errorMessage = `Unable to upload file(s) to Azure Blob Storage. ${e.message}`;
|
||||
this.logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
@@ -241,19 +258,11 @@ export class AzureBlobStoragePublish implements PublisherBase {
|
||||
* 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
|
||||
.getContainerClient(this.containerName)
|
||||
.getBlockBlobClient(`${entityRootDir}/index.html`)
|
||||
.exists()
|
||||
.then((response: boolean) => {
|
||||
resolve(response);
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
hasDocsBeenGenerated(entity: Entity): Promise<boolean> {
|
||||
const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
|
||||
return this.storageClient
|
||||
.getContainerClient(this.containerName)
|
||||
.getBlockBlobClient(`${entityRootDir}/index.html`)
|
||||
.exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@types/react": "^16.9",
|
||||
"classnames": "^2.2.6",
|
||||
"luxon": "1.25.0",
|
||||
"react": "^16.13.1",
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemSecondaryAction,
|
||||
Tooltip,
|
||||
ListItemText,
|
||||
@@ -25,13 +24,16 @@ import {
|
||||
IconButton,
|
||||
Link,
|
||||
Typography,
|
||||
Chip,
|
||||
} from '@material-ui/core';
|
||||
import { StatusError, StatusWarning } from '@backstage/core';
|
||||
import Done from '@material-ui/icons/Done';
|
||||
import Warning from '@material-ui/icons/Warning';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { Incident } from '../types';
|
||||
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
denseListIcon: {
|
||||
marginRight: 0,
|
||||
display: 'flex',
|
||||
@@ -42,10 +44,21 @@ const useStyles = makeStyles({
|
||||
listItemPrimary: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
listItemIcon: {
|
||||
minWidth: '1em',
|
||||
warning: {
|
||||
borderColor: theme.palette.status.warning,
|
||||
color: theme.palette.status.warning,
|
||||
'& *': {
|
||||
color: theme.palette.status.warning,
|
||||
},
|
||||
},
|
||||
});
|
||||
error: {
|
||||
borderColor: theme.palette.status.error,
|
||||
color: theme.palette.status.error,
|
||||
'& *': {
|
||||
color: theme.palette.status.error,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
incident: Incident;
|
||||
@@ -62,19 +75,24 @@ export const IncidentListItem = ({ incident }: Props) => {
|
||||
|
||||
return (
|
||||
<ListItem dense key={incident.id}>
|
||||
<ListItemIcon className={classes.listItemIcon}>
|
||||
<Tooltip title={incident.status} placement="top">
|
||||
<div className={classes.denseListIcon}>
|
||||
{incident.status === 'triggered' ? (
|
||||
<StatusError />
|
||||
) : (
|
||||
<StatusWarning />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={incident.title}
|
||||
primary={
|
||||
<>
|
||||
<Chip
|
||||
data-testid={`chip-${incident.status}`}
|
||||
label={incident.status}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={incident.status === 'acknowledged' ? <Done /> : <Warning />}
|
||||
className={
|
||||
incident.status === 'triggered'
|
||||
? classes.error
|
||||
: classes.warning
|
||||
}
|
||||
/>
|
||||
{incident.title}
|
||||
</>
|
||||
}
|
||||
primaryTypographyProps={{
|
||||
variant: 'body1',
|
||||
className: classes.listItemPrimary,
|
||||
|
||||
@@ -84,13 +84,7 @@ describe('Incidents', () => {
|
||||
},
|
||||
] as Incident[],
|
||||
);
|
||||
const {
|
||||
getByText,
|
||||
getByTitle,
|
||||
getAllByTitle,
|
||||
getByLabelText,
|
||||
queryByTestId,
|
||||
} = render(
|
||||
const { getByText, getAllByTitle, queryByTestId } = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<Incidents serviceId="abc" refreshIncidents={false} />
|
||||
@@ -102,10 +96,10 @@ describe('Incidents', () => {
|
||||
expect(getByText('title2')).toBeInTheDocument();
|
||||
expect(getByText('person1')).toBeInTheDocument();
|
||||
expect(getByText('person2')).toBeInTheDocument();
|
||||
expect(getByTitle('triggered')).toBeInTheDocument();
|
||||
expect(getByTitle('acknowledged')).toBeInTheDocument();
|
||||
expect(getByLabelText('Status error')).toBeInTheDocument();
|
||||
expect(getByLabelText('Status warning')).toBeInTheDocument();
|
||||
expect(getByText('triggered')).toBeInTheDocument();
|
||||
expect(getByText('acknowledged')).toBeInTheDocument();
|
||||
expect(queryByTestId('chip-triggered')).toBeInTheDocument();
|
||||
expect(queryByTestId('chip-acknowledged')).toBeInTheDocument();
|
||||
|
||||
// assert links, mailto and hrefs, date calculation
|
||||
expect(getAllByTitle('View in PagerDuty').length).toEqual(2);
|
||||
|
||||
@@ -17,68 +17,38 @@ import React, { useState, useCallback } from 'react';
|
||||
import { useApi, Progress, HeaderIconLinkRow } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
Button,
|
||||
makeStyles,
|
||||
Card,
|
||||
CardHeader,
|
||||
Divider,
|
||||
CardContent,
|
||||
} from '@material-ui/core';
|
||||
import { Card, CardHeader, Divider, CardContent } from '@material-ui/core';
|
||||
import { Incidents } from './Incident';
|
||||
import { EscalationPolicy } from './Escalation';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { pagerDutyApiRef, UnauthorizedError } from '../api';
|
||||
import AlarmAddIcon from '@material-ui/icons/AlarmAdd';
|
||||
import { TriggerDialog } from './TriggerDialog';
|
||||
import { MissingTokenError } from './Errors/MissingTokenError';
|
||||
import WebIcon from '@material-ui/icons/Web';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
triggerAlarm: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
fontSize: '0.7rem',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1.2,
|
||||
lineHeight: 1.5,
|
||||
'&:hover, &:focus, &.focus': {
|
||||
backgroundColor: 'transparent',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
|
||||
import { PAGERDUTY_INTEGRATION_KEY } from './constants';
|
||||
import { TriggerButton, useShowDialog } from './TriggerButton';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]);
|
||||
|
||||
type Props = {
|
||||
/** @deprecated The entity is now grabbed from context instead */
|
||||
entity?: Entity;
|
||||
};
|
||||
|
||||
export const PagerDutyCard = (_props: Props) => {
|
||||
const classes = useStyles();
|
||||
export const PagerDutyCard = () => {
|
||||
const { entity } = useEntity();
|
||||
const api = useApi(pagerDutyApiRef);
|
||||
const [showDialog, setShowDialog] = useState<boolean>(false);
|
||||
const [refreshIncidents, setRefreshIncidents] = useState<boolean>(false);
|
||||
const integrationKey = entity.metadata.annotations![
|
||||
PAGERDUTY_INTEGRATION_KEY
|
||||
];
|
||||
const setShowDialog = useShowDialog()[1];
|
||||
|
||||
const showDialog = useCallback(() => {
|
||||
setShowDialog(true);
|
||||
}, [setShowDialog]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setRefreshIncidents(x => !x);
|
||||
}, []);
|
||||
|
||||
const handleDialog = useCallback(() => {
|
||||
setShowDialog(x => !x);
|
||||
}, []);
|
||||
|
||||
const { value: service, loading, error } = useAsync(async () => {
|
||||
const services = await api.getServiceByIntegrationKey(integrationKey);
|
||||
|
||||
@@ -114,17 +84,8 @@ export const PagerDutyCard = (_props: Props) => {
|
||||
|
||||
const triggerLink = {
|
||||
label: 'Create Incident',
|
||||
action: (
|
||||
<Button
|
||||
data-testid="trigger-button"
|
||||
color="secondary"
|
||||
onClick={handleDialog}
|
||||
className={classes.triggerAlarm}
|
||||
>
|
||||
Create Incident
|
||||
</Button>
|
||||
),
|
||||
icon: <AlarmAddIcon onClick={handleDialog} />,
|
||||
action: <TriggerButton design="link" onIncidentCreated={handleRefresh} />,
|
||||
icon: <AlarmAddIcon onClick={showDialog} />,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -140,13 +101,6 @@ export const PagerDutyCard = (_props: Props) => {
|
||||
refreshIncidents={refreshIncidents}
|
||||
/>
|
||||
<EscalationPolicy policyId={service!.policyId} />
|
||||
<TriggerDialog
|
||||
showDialog={showDialog}
|
||||
handleDialog={handleDialog}
|
||||
name={entity.metadata.name}
|
||||
integrationKey={integrationKey}
|
||||
onIncidentCreated={handleRefresh}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2021 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 React, { useCallback, PropsWithChildren } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { makeStyles, Button } from '@material-ui/core';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
import { TriggerDialog } from '../TriggerDialog';
|
||||
import { PAGERDUTY_INTEGRATION_KEY } from '../constants';
|
||||
|
||||
export interface TriggerButtonProps {
|
||||
design: 'link' | 'button';
|
||||
onIncidentCreated?: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
buttonStyle: {
|
||||
backgroundColor: theme.palette.error.main,
|
||||
color: theme.palette.error.contrastText,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
},
|
||||
},
|
||||
triggerAlarm: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
fontSize: '0.7rem',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 1.2,
|
||||
lineHeight: 1.5,
|
||||
'&:hover, &:focus, &.focus': {
|
||||
backgroundColor: 'transparent',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const useShowDialog = createGlobalState(false);
|
||||
|
||||
export function TriggerButton({
|
||||
design,
|
||||
onIncidentCreated,
|
||||
children,
|
||||
}: PropsWithChildren<TriggerButtonProps>) {
|
||||
const { buttonStyle, triggerAlarm } = useStyles();
|
||||
const { entity } = useEntity();
|
||||
const [dialogShown = false, setDialogShown] = useShowDialog();
|
||||
|
||||
const showDialog = useCallback(() => {
|
||||
setDialogShown(true);
|
||||
}, [setDialogShown]);
|
||||
const hideDialog = useCallback(() => {
|
||||
setDialogShown(false);
|
||||
}, [setDialogShown]);
|
||||
|
||||
const integrationKey = entity.metadata.annotations![
|
||||
PAGERDUTY_INTEGRATION_KEY
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
data-testid="trigger-button"
|
||||
{...(design === 'link' && { color: 'secondary' })}
|
||||
onClick={showDialog}
|
||||
className={design === 'link' ? triggerAlarm : buttonStyle}
|
||||
>
|
||||
{children ?? 'Create Incident'}
|
||||
</Button>
|
||||
<TriggerDialog
|
||||
showDialog={dialogShown}
|
||||
handleDialog={hideDialog}
|
||||
name={entity.metadata.name}
|
||||
integrationKey={integrationKey}
|
||||
onIncidentCreated={onIncidentCreated}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ type Props = {
|
||||
integrationKey: string;
|
||||
showDialog: boolean;
|
||||
handleDialog: () => void;
|
||||
onIncidentCreated: () => void;
|
||||
onIncidentCreated?: () => void;
|
||||
};
|
||||
|
||||
export const TriggerDialog = ({
|
||||
@@ -69,11 +69,17 @@ export const TriggerDialog = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
alertApi.post({
|
||||
message: `Alarm successfully triggered by ${userName}`,
|
||||
});
|
||||
onIncidentCreated();
|
||||
handleDialog();
|
||||
(async () => {
|
||||
alertApi.post({
|
||||
message: `Alarm successfully triggered by ${userName}`,
|
||||
});
|
||||
|
||||
handleDialog();
|
||||
|
||||
// The pager duty API isn't always returning the newly created alarm immediately
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
onIncidentCreated?.();
|
||||
})();
|
||||
}
|
||||
}, [value, alertApi, handleDialog, userName, onIncidentCreated]);
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2021 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 const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key';
|
||||
@@ -23,6 +23,7 @@ export {
|
||||
isPluginApplicableToEntity as isPagerDutyAvailable,
|
||||
PagerDutyCard,
|
||||
} from './components/PagerDutyCard';
|
||||
export { TriggerButton } from './components/TriggerButton';
|
||||
export {
|
||||
PagerDutyClient,
|
||||
pagerDutyApiRef,
|
||||
|
||||
@@ -18,14 +18,20 @@ import path from 'path';
|
||||
import { Git } from '@backstage/backend-common';
|
||||
import { PreparerBase, PreparerOptions } from './types';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { GitHubIntegrationConfig } from '@backstage/integration';
|
||||
import {
|
||||
GitHubIntegrationConfig,
|
||||
GithubCredentialsProvider,
|
||||
} from '@backstage/integration';
|
||||
|
||||
export class GithubPreparer implements PreparerBase {
|
||||
static fromConfig(config: GitHubIntegrationConfig) {
|
||||
return new GithubPreparer({ token: config.token });
|
||||
const credentialsProvider = GithubCredentialsProvider.create(config);
|
||||
return new GithubPreparer({ credentialsProvider });
|
||||
}
|
||||
|
||||
constructor(private readonly config: { token?: string }) {}
|
||||
constructor(
|
||||
private readonly config: { credentialsProvider: GithubCredentialsProvider },
|
||||
) {}
|
||||
|
||||
async prepare({ url, workspacePath, logger }: PreparerOptions) {
|
||||
const parsedGitUrl = parseGitUrl(url);
|
||||
@@ -36,10 +42,14 @@ export class GithubPreparer implements PreparerBase {
|
||||
parsedGitUrl.filepath ?? '',
|
||||
);
|
||||
|
||||
const git = this.config.token
|
||||
const { token } = await this.config.credentialsProvider.getCredentials({
|
||||
url,
|
||||
});
|
||||
|
||||
const git = token
|
||||
? Git.fromAuth({
|
||||
username: 'x-access-token',
|
||||
password: this.config.token,
|
||||
password: token,
|
||||
logger,
|
||||
})
|
||||
: Git.fromAuth({ logger });
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { initRepoAndPush } from './helpers';
|
||||
import { GitHubIntegrationConfig } from '@backstage/integration';
|
||||
import {
|
||||
GitHubIntegrationConfig,
|
||||
GithubCredentialsProvider,
|
||||
} from '@backstage/integration';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import path from 'path';
|
||||
@@ -28,27 +31,24 @@ export class GithubPublisher implements PublisherBase {
|
||||
config: GitHubIntegrationConfig,
|
||||
{ repoVisibility }: { repoVisibility: RepoVisibilityOptions },
|
||||
) {
|
||||
if (!config.token) {
|
||||
if (!config.token && !config.apps) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const githubClient = new Octokit({
|
||||
auth: config.token,
|
||||
baseUrl: config.apiBaseUrl,
|
||||
});
|
||||
const credentialsProvider = GithubCredentialsProvider.create(config);
|
||||
|
||||
return new GithubPublisher({
|
||||
token: config.token,
|
||||
client: githubClient,
|
||||
credentialsProvider,
|
||||
repoVisibility,
|
||||
apiBaseUrl: config.apiBaseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly config: {
|
||||
token: string;
|
||||
client: Octokit;
|
||||
credentialsProvider: GithubCredentialsProvider;
|
||||
repoVisibility: RepoVisibilityOptions;
|
||||
apiBaseUrl: string | undefined;
|
||||
},
|
||||
) {}
|
||||
|
||||
@@ -59,9 +59,25 @@ export class GithubPublisher implements PublisherBase {
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const { owner, name } = parseGitUrl(values.storePath);
|
||||
|
||||
const { token } = await this.config.credentialsProvider.getCredentials({
|
||||
url: values.storePath,
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
`No token could be acquired for URL: ${values.storePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
const client = new Octokit({
|
||||
auth: token,
|
||||
baseUrl: this.config.apiBaseUrl,
|
||||
});
|
||||
|
||||
const description = values.description as string;
|
||||
const access = values.access as string;
|
||||
const remoteUrl = await this.createRemote({
|
||||
client,
|
||||
description,
|
||||
access,
|
||||
name,
|
||||
@@ -73,7 +89,7 @@ export class GithubPublisher implements PublisherBase {
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: 'x-access-token',
|
||||
password: this.config.token,
|
||||
password: token,
|
||||
},
|
||||
logger,
|
||||
});
|
||||
@@ -86,27 +102,28 @@ export class GithubPublisher implements PublisherBase {
|
||||
}
|
||||
|
||||
private async createRemote(opts: {
|
||||
client: Octokit;
|
||||
access: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
description: string;
|
||||
}) {
|
||||
const { access, description, owner, name } = opts;
|
||||
const { client, access, description, owner, name } = opts;
|
||||
|
||||
const user = await this.config.client.users.getByUsername({
|
||||
const user = await client.users.getByUsername({
|
||||
username: owner,
|
||||
});
|
||||
|
||||
const repoCreationPromise =
|
||||
user.data.type === 'Organization'
|
||||
? this.config.client.repos.createInOrg({
|
||||
? client.repos.createInOrg({
|
||||
name,
|
||||
org: owner,
|
||||
private: this.config.repoVisibility !== 'public',
|
||||
visibility: this.config.repoVisibility,
|
||||
description,
|
||||
})
|
||||
: this.config.client.repos.createForAuthenticatedUser({
|
||||
: client.repos.createForAuthenticatedUser({
|
||||
name,
|
||||
private: this.config.repoVisibility === 'private',
|
||||
description,
|
||||
@@ -116,7 +133,7 @@ export class GithubPublisher implements PublisherBase {
|
||||
|
||||
if (access?.startsWith(`${owner}/`)) {
|
||||
const [, team] = access.split('/');
|
||||
await this.config.client.teams.addOrUpdateRepoPermissionsInOrg({
|
||||
await client.teams.addOrUpdateRepoPermissionsInOrg({
|
||||
org: owner,
|
||||
team_slug: team,
|
||||
owner,
|
||||
@@ -125,7 +142,7 @@ export class GithubPublisher implements PublisherBase {
|
||||
});
|
||||
// no need to add access if it's the person who own's the personal account
|
||||
} else if (access && access !== owner) {
|
||||
await this.config.client.repos.addCollaborator({
|
||||
await client.repos.addCollaborator({
|
||||
owner,
|
||||
repo: name,
|
||||
username: access,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { useTheme } from '@material-ui/styles';
|
||||
import { useTheme } from '@material-ui/core';
|
||||
import { Circle } from 'rc-progress';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
@@ -2487,31 +2487,31 @@
|
||||
upper-case "2.0.1"
|
||||
|
||||
"@graphql-codegen/typescript-resolvers@^1.17.7":
|
||||
version "1.17.12"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-1.17.12.tgz#1f8f608d68b46780351c8224a61750b9b9307497"
|
||||
integrity sha512-qICKxl06R1yCBh03cdyiOYlHTebQ1n/FOiQPCxo4bsiF6HfbrgpMS8fdGEZ8v+VBPxQFZZ7HQ2KcuPNsuw5R6g==
|
||||
version "1.18.2"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-1.18.2.tgz#7513b92df7c5a0d3c27342c591ada7340696cf8f"
|
||||
integrity sha512-aWfRR5y1gXCPUNK7zaUiSbmceqidvZ38mNHIBvXmZArRigyz1QZgN26kKpXjJjtFbPvROPnlGsYcZBReybvZXA==
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers" "^1.18.2"
|
||||
"@graphql-codegen/typescript" "^1.18.1"
|
||||
"@graphql-codegen/visitor-plugin-common" "^1.17.20"
|
||||
"@graphql-tools/utils" "^6"
|
||||
auto-bind "~4.0.0"
|
||||
tslib "~2.0.1"
|
||||
|
||||
"@graphql-codegen/typescript@^1.17.7", "@graphql-codegen/typescript@^1.18.1":
|
||||
version "1.20.2"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.20.2.tgz#223327a8aea929e772f481e264c5aa3a36707fcb"
|
||||
integrity sha512-D/DMUz4O2UEoFucUVu2K2xoaMPAn68BwYGnCAKnSDqtFKsOEqmTjHFwcgyEnpucQ5xFeG0pKGMb1SlS2Go9J8Q==
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers" "^1.18.2"
|
||||
"@graphql-codegen/visitor-plugin-common" "^1.18.2"
|
||||
"@graphql-codegen/typescript" "^1.21.0"
|
||||
"@graphql-codegen/visitor-plugin-common" "^1.18.3"
|
||||
"@graphql-tools/utils" "^7.0.0"
|
||||
auto-bind "~4.0.0"
|
||||
tslib "~2.1.0"
|
||||
|
||||
"@graphql-codegen/visitor-plugin-common@^1.17.20", "@graphql-codegen/visitor-plugin-common@^1.18.2":
|
||||
version "1.18.2"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.18.2.tgz#1a30bd33f011b4fb976e0f8462d160126db875ea"
|
||||
integrity sha512-A8yBJGq7A7gxaVVXK4QXwV1ZpzZ64fH7U7JTGeq86o3jA7QNV2rmCRXCY0JttS2fu+olV18NjsWRwAXuAb1g9A==
|
||||
"@graphql-codegen/typescript@^1.17.7", "@graphql-codegen/typescript@^1.21.0":
|
||||
version "1.21.0"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.21.0.tgz#301b1851cd278bedd1f49e1b3d654f4dc0af2943"
|
||||
integrity sha512-23YttnZ+87dA/3lbCvPKdsrpEOx142dCT9xSh6XkSeyCvn+vUtETN2MhamCYB87G7Nu2EcLDFKDZjgXH73f4fg==
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers" "^1.18.2"
|
||||
"@graphql-codegen/visitor-plugin-common" "^1.18.3"
|
||||
auto-bind "~4.0.0"
|
||||
tslib "~2.1.0"
|
||||
|
||||
"@graphql-codegen/visitor-plugin-common@^1.18.3":
|
||||
version "1.18.3"
|
||||
resolved "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.18.3.tgz#9d2c4449c3bdaffe3e782e2321fe0cb998b8a91d"
|
||||
integrity sha512-6xJzt8hszCTKt3rTlcCURpuiAFuaiaZgStlVeRE1OrKEDiY1T3vwF3/7TonhfnEjqBWtZdMmXvNx3ArXkRUV4w==
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers" "^1.18.2"
|
||||
"@graphql-tools/optimize" "^1.0.1"
|
||||
@@ -2808,14 +2808,14 @@
|
||||
camel-case "4.1.1"
|
||||
tslib "~2.0.1"
|
||||
|
||||
"@graphql-tools/utils@^7.1.0":
|
||||
version "7.1.0"
|
||||
resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.1.0.tgz#0cbcfa7b6e7741650f78e8bde4823780ed9e8c57"
|
||||
integrity sha512-lMTgy08BdqQ31zyyCRrxcKZ6gAKQB9amGKJGg0mb3b4I3iJQKeaw9a7T96yGe1frGak1UK9y7OoYqx8RG7KitA==
|
||||
"@graphql-tools/utils@^7.0.0", "@graphql-tools/utils@^7.1.0":
|
||||
version "7.2.6"
|
||||
resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.2.6.tgz#5d974cbdec5ddf4d7fdc593816335512ee5fe4de"
|
||||
integrity sha512-/kY7Nb+cCHi/MvU3tjz3KrXzuJNWMlnn7EoWazLmpDvl6b2Qt69hlVoPd5zQtKlGib35zZw9NZ5zs5qTAw8Y9g==
|
||||
dependencies:
|
||||
"@ardatan/aggregate-error" "0.0.6"
|
||||
camel-case "4.1.1"
|
||||
tslib "~2.0.1"
|
||||
camel-case "4.1.2"
|
||||
tslib "~2.1.0"
|
||||
|
||||
"@graphql-tools/wrap@^6.2.4":
|
||||
version "6.2.4"
|
||||
@@ -9225,6 +9225,14 @@ camel-case@4.1.1, camel-case@^4.1.1:
|
||||
pascal-case "^3.1.1"
|
||||
tslib "^1.10.0"
|
||||
|
||||
camel-case@4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
|
||||
integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
|
||||
dependencies:
|
||||
pascal-case "^3.1.2"
|
||||
tslib "^2.0.3"
|
||||
|
||||
camelcase-keys@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
|
||||
@@ -17455,6 +17463,13 @@ lower-case@2.0.1, lower-case@^2.0.1:
|
||||
dependencies:
|
||||
tslib "^1.10.0"
|
||||
|
||||
lower-case@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
|
||||
integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
||||
@@ -18561,6 +18576,14 @@ no-case@^3.0.3:
|
||||
lower-case "^2.0.1"
|
||||
tslib "^1.10.0"
|
||||
|
||||
no-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
|
||||
integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
|
||||
dependencies:
|
||||
lower-case "^2.0.2"
|
||||
tslib "^2.0.3"
|
||||
|
||||
nock@^13.0.5:
|
||||
version "13.0.5"
|
||||
resolved "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz#a618c6f86372cb79fac04ca9a2d1e4baccdb2414"
|
||||
@@ -19762,6 +19785,14 @@ pascal-case@3.1.1, pascal-case@^3.1.1:
|
||||
no-case "^3.0.3"
|
||||
tslib "^1.10.0"
|
||||
|
||||
pascal-case@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
|
||||
integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
|
||||
dependencies:
|
||||
no-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
pascalcase@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||
@@ -24825,7 +24856,7 @@ tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@~2.1.0:
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
|
||||
|
||||
Reference in New Issue
Block a user