Merge pull request #27771 from solimant/23819_scaffolder_openapi
Move Scaffolder API to OpenAPI
This commit is contained in:
+1
-1
@@ -118,7 +118,7 @@ async function downloadDirectoryContents(
|
||||
directoryContents: {
|
||||
path: string;
|
||||
base64Content: string;
|
||||
executable: boolean;
|
||||
executable?: boolean;
|
||||
}[],
|
||||
name: string,
|
||||
) {
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* 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 { ConfigReader } from '@backstage/core-app-api';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ScaffolderClient } from './api';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
|
||||
jest.mock('@microsoft/fetch-event-source');
|
||||
const mockFetchEventSource = fetchEventSource as jest.MockedFunction<
|
||||
typeof fetchEventSource
|
||||
>;
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
describe('api', () => {
|
||||
registerMswTestHooks(server);
|
||||
const mockBaseUrl = 'http://backstage/api';
|
||||
|
||||
const discoveryApi = { getBaseUrl: async () => mockBaseUrl };
|
||||
const fetchApi = new MockFetchApi();
|
||||
const identityApi = {
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
signOut: jest.fn(),
|
||||
};
|
||||
|
||||
const scmIntegrationsApi = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'hello.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let apiClient: ScaffolderClient;
|
||||
beforeEach(() => {
|
||||
apiClient = new ScaffolderClient({
|
||||
scmIntegrationsApi,
|
||||
discoveryApi,
|
||||
fetchApi,
|
||||
identityApi,
|
||||
});
|
||||
|
||||
jest.restoreAllMocks();
|
||||
identityApi.getBackstageIdentity.mockReturnValue({});
|
||||
});
|
||||
|
||||
it('should return default and custom integrations', async () => {
|
||||
const allowedHosts = [
|
||||
'hello.com',
|
||||
'gitlab.com',
|
||||
'github.com',
|
||||
'dev.azure.com',
|
||||
'bitbucket.org',
|
||||
];
|
||||
const { integrations } = await apiClient.getIntegrationsList({
|
||||
allowedHosts,
|
||||
});
|
||||
integrations.forEach(integration =>
|
||||
expect(allowedHosts).toContain(integration.host),
|
||||
);
|
||||
});
|
||||
|
||||
describe('streamEvents', () => {
|
||||
describe('eventsource', () => {
|
||||
it('should work', async () => {
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({
|
||||
id: '',
|
||||
event: 'log',
|
||||
data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}',
|
||||
});
|
||||
await Promise.resolve();
|
||||
onmessage?.({
|
||||
id: '',
|
||||
event: 'completion',
|
||||
data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}',
|
||||
});
|
||||
});
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete => {
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete });
|
||||
});
|
||||
|
||||
expect(mockFetchEventSource).toHaveBeenCalledWith(
|
||||
'http://backstage/api/v2/tasks/a-random-task-id/eventstream',
|
||||
{
|
||||
fetch: fetchApi.fetch,
|
||||
onmessage: expect.any(Function),
|
||||
onerror: expect.any(Function),
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('longPolling', () => {
|
||||
beforeEach(() => {
|
||||
apiClient = new ScaffolderClient({
|
||||
scmIntegrationsApi,
|
||||
discoveryApi,
|
||||
fetchApi,
|
||||
identityApi,
|
||||
useLongPollingLogs: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(req, res, ctx) => {
|
||||
const { taskId } = req.params;
|
||||
const after = req.url.searchParams.get('after');
|
||||
|
||||
if (taskId === 'a-random-task-id') {
|
||||
if (!after) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
} else if (after === '1') {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete =>
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete }),
|
||||
);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should unsubscribe', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(req, res, ctx) => {
|
||||
const { taskId } = req.params;
|
||||
|
||||
const after = req.url.searchParams.get('after');
|
||||
|
||||
// use assertion to make sure it is not called after unsubscribing
|
||||
expect(after).toBe(null);
|
||||
|
||||
if (taskId === 'a-random-task-id') {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete => {
|
||||
const subscription = apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({
|
||||
next: (...args) => {
|
||||
next(...args);
|
||||
subscription.unsubscribe();
|
||||
complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
taskId: 'a-random-id',
|
||||
type: 'log',
|
||||
createdAt: '',
|
||||
body: { message: 'My log message' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should continue after error', async () => {
|
||||
const called = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks/:taskId/events`,
|
||||
(_req, res, ctx) => {
|
||||
called();
|
||||
|
||||
if (called.mock.calls.length > 1) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(ctx.status(500));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
await new Promise<void>(complete =>
|
||||
apiClient
|
||||
.streamLogs({ taskId: 'a-random-task-id' })
|
||||
.subscribe({ next, complete }),
|
||||
);
|
||||
|
||||
expect(called).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
id: 2,
|
||||
taskId: 'a-random-id',
|
||||
type: 'completion',
|
||||
createdAt: '',
|
||||
body: { message: 'Finished!' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listTasks', () => {
|
||||
it('should list all tasks', async () => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => {
|
||||
const createdBy = req.url.searchParams.get('createdBy');
|
||||
|
||||
if (createdBy) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
createdBy,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await apiClient.listTasks({ filterByOwnership: 'all' });
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should list tasks with limit and offset', async () => {
|
||||
server.use(
|
||||
rest.get(
|
||||
`${mockBaseUrl}/v2/tasks?limit=5&offset=0`,
|
||||
(_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
]),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const result = await apiClient.listTasks({
|
||||
filterByOwnership: 'all',
|
||||
limit: 5,
|
||||
offset: 0,
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should list task using the current user as owner', async () => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/v2/tasks`, (req, res, ctx) => {
|
||||
const createdBy = req.url.searchParams.get('createdBy');
|
||||
|
||||
if (createdBy) {
|
||||
return res(
|
||||
ctx.json({
|
||||
tasks: [
|
||||
{
|
||||
createdBy,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json({
|
||||
tasks: [
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
{
|
||||
createdBy: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
identityApi.getBackstageIdentity.mockResolvedValueOnce({
|
||||
userEntityRef: 'user:default/foo',
|
||||
});
|
||||
|
||||
const result = await apiClient.listTasks({ filterByOwnership: 'owned' });
|
||||
expect(identityApi.getBackstageIdentity).toHaveBeenCalled();
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,389 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
DiscoveryApi,
|
||||
FetchApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import {
|
||||
ListActionsResponse,
|
||||
ListTemplatingExtensionsResponse,
|
||||
LogEvent,
|
||||
ScaffolderApi,
|
||||
ScaffolderDryRunOptions,
|
||||
ScaffolderDryRunResponse,
|
||||
ScaffolderGetIntegrationsListOptions,
|
||||
ScaffolderGetIntegrationsListResponse,
|
||||
ScaffolderScaffoldOptions,
|
||||
ScaffolderScaffoldResponse,
|
||||
ScaffolderStreamLogsOptions,
|
||||
ScaffolderTask,
|
||||
TemplateParameterSchema,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
import { Observable } from '@backstage/types';
|
||||
import {
|
||||
EventSourceMessage,
|
||||
fetchEventSource,
|
||||
} from '@microsoft/fetch-event-source';
|
||||
import { default as qs, default as queryString } from 'qs';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import { ScaffolderClient as _ScaffolderClient } from '@backstage/plugin-scaffolder-common';
|
||||
|
||||
/**
|
||||
* An API to interact with the scaffolder backend.
|
||||
*
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderClient} instead as this has now been moved.
|
||||
*/
|
||||
export class ScaffolderClient implements ScaffolderApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
|
||||
private readonly fetchApi: FetchApi;
|
||||
private readonly identityApi?: IdentityApi;
|
||||
private readonly useLongPollingLogs: boolean;
|
||||
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
fetchApi: FetchApi;
|
||||
identityApi?: IdentityApi;
|
||||
scmIntegrationsApi: ScmIntegrationRegistry;
|
||||
useLongPollingLogs?: boolean;
|
||||
}) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.fetchApi = options.fetchApi ?? { fetch };
|
||||
this.scmIntegrationsApi = options.scmIntegrationsApi;
|
||||
this.useLongPollingLogs = options.useLongPollingLogs ?? false;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async listTasks(options: {
|
||||
filterByOwnership: 'owned' | 'all';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{ tasks: ScaffolderTask[]; totalTasks?: number }> {
|
||||
if (!this.identityApi) {
|
||||
throw new Error(
|
||||
'IdentityApi is not available in the ScaffolderClient, please pass through the IdentityApi to the ScaffolderClient constructor in order to use the listTasks method',
|
||||
);
|
||||
}
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const { userEntityRef } = await this.identityApi.getBackstageIdentity();
|
||||
|
||||
const query = queryString.stringify({
|
||||
createdBy:
|
||||
options.filterByOwnership === 'owned' ? userEntityRef : undefined,
|
||||
limit: options.limit,
|
||||
offset: options.offset,
|
||||
});
|
||||
|
||||
const response = await this.fetchApi.fetch(`${baseUrl}/v2/tasks?${query}`);
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getIntegrationsList(
|
||||
options: ScaffolderGetIntegrationsListOptions,
|
||||
): Promise<ScaffolderGetIntegrationsListResponse> {
|
||||
const integrations = [
|
||||
...this.scmIntegrationsApi.azure.list(),
|
||||
...this.scmIntegrationsApi.bitbucket
|
||||
.list()
|
||||
.filter(
|
||||
item =>
|
||||
!this.scmIntegrationsApi.bitbucketCloud.byHost(item.config.host) &&
|
||||
!this.scmIntegrationsApi.bitbucketServer.byHost(item.config.host),
|
||||
),
|
||||
...this.scmIntegrationsApi.bitbucketCloud.list(),
|
||||
...this.scmIntegrationsApi.bitbucketServer.list(),
|
||||
...this.scmIntegrationsApi.gerrit.list(),
|
||||
...this.scmIntegrationsApi.gitea.list(),
|
||||
...this.scmIntegrationsApi.github.list(),
|
||||
...this.scmIntegrationsApi.gitlab.list(),
|
||||
]
|
||||
.map(c => ({ type: c.type, title: c.title, host: c.config.host }))
|
||||
.filter(c => options.allowedHosts.includes(c.host));
|
||||
|
||||
return {
|
||||
integrations,
|
||||
};
|
||||
}
|
||||
|
||||
async getTemplateParameterSchema(
|
||||
templateRef: string,
|
||||
): Promise<TemplateParameterSchema> {
|
||||
const { namespace, kind, name } = parseEntityRef(templateRef, {
|
||||
defaultKind: 'template',
|
||||
});
|
||||
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const templatePath = [namespace, kind, name]
|
||||
.map(s => encodeURIComponent(s))
|
||||
.join('/');
|
||||
|
||||
const url = `${baseUrl}/v2/templates/${templatePath}/parameter-schema`;
|
||||
|
||||
const response = await this.fetchApi.fetch(url);
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
const schema: TemplateParameterSchema = await response.json();
|
||||
return schema;
|
||||
}
|
||||
|
||||
async scaffold(
|
||||
options: ScaffolderScaffoldOptions,
|
||||
): Promise<ScaffolderScaffoldResponse> {
|
||||
const { templateRef, values, secrets = {} } = options;
|
||||
const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`;
|
||||
const response = await this.fetchApi.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateRef,
|
||||
values: { ...values },
|
||||
secrets,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status !== 201) {
|
||||
const status = `${response.status} ${response.statusText}`;
|
||||
const body = await response.text();
|
||||
throw new Error(`Backend request failed, ${status} ${body.trim()}`);
|
||||
}
|
||||
|
||||
const { id } = (await response.json()) as { id: string };
|
||||
return { taskId: id };
|
||||
}
|
||||
|
||||
async getTask(taskId: string): Promise<ScaffolderTask> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`;
|
||||
|
||||
const response = await this.fetchApi.fetch(url);
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent> {
|
||||
if (this.useLongPollingLogs) {
|
||||
return this.streamLogsPolling(options);
|
||||
}
|
||||
|
||||
return this.streamLogsEventStream(options);
|
||||
}
|
||||
|
||||
async dryRun(
|
||||
options: ScaffolderDryRunOptions,
|
||||
): Promise<ScaffolderDryRunResponse> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const response = await this.fetchApi.fetch(`${baseUrl}/v2/dry-run`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template: options.template,
|
||||
values: options.values,
|
||||
secrets: options.secrets,
|
||||
directoryContents: options.directoryContents,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private streamLogsEventStream({
|
||||
isTaskRecoverable,
|
||||
taskId,
|
||||
after,
|
||||
}: ScaffolderStreamLogsOptions): Observable<LogEvent> {
|
||||
return new ObservableImpl(subscriber => {
|
||||
const params = new URLSearchParams();
|
||||
if (after !== undefined) {
|
||||
params.set('after', String(Number(after)));
|
||||
}
|
||||
|
||||
this.discoveryApi.getBaseUrl('scaffolder').then(
|
||||
baseUrl => {
|
||||
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
|
||||
taskId,
|
||||
)}/eventstream`;
|
||||
|
||||
const processEvent = (event: any) => {
|
||||
if (event.data) {
|
||||
try {
|
||||
subscriber.next(JSON.parse(event.data));
|
||||
} catch (ex) {
|
||||
subscriber.error(ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ctrl = new AbortController();
|
||||
void fetchEventSource(url, {
|
||||
fetch: this.fetchApi.fetch,
|
||||
signal: ctrl.signal,
|
||||
onmessage(e: EventSourceMessage) {
|
||||
if (e.event === 'log') {
|
||||
processEvent(e);
|
||||
return;
|
||||
} else if (e.event === 'completion' && !isTaskRecoverable) {
|
||||
processEvent(e);
|
||||
subscriber.complete();
|
||||
ctrl.abort();
|
||||
return;
|
||||
}
|
||||
processEvent(e);
|
||||
},
|
||||
onerror(err) {
|
||||
subscriber.error(err);
|
||||
},
|
||||
});
|
||||
},
|
||||
error => {
|
||||
subscriber.error(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private streamLogsPolling({
|
||||
taskId,
|
||||
after: inputAfter,
|
||||
}: {
|
||||
taskId: string;
|
||||
after?: number;
|
||||
}): Observable<LogEvent> {
|
||||
let after = inputAfter;
|
||||
|
||||
return new ObservableImpl(subscriber => {
|
||||
this.discoveryApi.getBaseUrl('scaffolder').then(async baseUrl => {
|
||||
while (!subscriber.closed) {
|
||||
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
|
||||
taskId,
|
||||
)}/events?${qs.stringify({ after })}`;
|
||||
const response = await this.fetchApi.fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
// wait for one second to not run into an
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
const logs = (await response.json()) as LogEvent[];
|
||||
|
||||
for (const event of logs) {
|
||||
after = Number(event.id);
|
||||
|
||||
subscriber.next(event);
|
||||
|
||||
if (event.type === 'completion') {
|
||||
subscriber.complete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async listActions(): Promise<ListActionsResponse> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const response = await this.fetchApi.fetch(`${baseUrl}/v2/actions`);
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async listTemplatingExtensions(): Promise<ListTemplatingExtensionsResponse> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const response = await this.fetchApi.fetch(
|
||||
`${baseUrl}/v2/templating-extensions`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw ResponseError.fromResponse(response);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async cancelTask(taskId: string): Promise<void> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/cancel`;
|
||||
|
||||
const response = await this.fetchApi.fetch(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async retry?(taskId: string): Promise<void> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/retry`;
|
||||
|
||||
const response = await this.fetchApi.fetch(url, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async autocomplete({
|
||||
token,
|
||||
resource,
|
||||
provider,
|
||||
context,
|
||||
}: {
|
||||
token: string;
|
||||
provider: string;
|
||||
resource: string;
|
||||
context?: Record<string, string>;
|
||||
}): Promise<{ results: { title?: string; id: string }[] }> {
|
||||
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
|
||||
|
||||
const url = `${baseUrl}/v2/autocomplete/${provider}/${resource}`;
|
||||
|
||||
const response = await this.fetchApi.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
context: context ?? {},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
const { results } = await response.json();
|
||||
return { results };
|
||||
}
|
||||
}
|
||||
export class ScaffolderClient extends _ScaffolderClient {}
|
||||
|
||||
@@ -85,6 +85,7 @@ export const BitbucketRepoPicker = (
|
||||
|
||||
scaffolderApi
|
||||
.autocomplete({
|
||||
context: {},
|
||||
token: accessToken,
|
||||
resource: 'workspaces',
|
||||
provider: 'bitbucket-cloud',
|
||||
|
||||
@@ -21,22 +21,8 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createScaffolderFieldExtension as createScaffolderFieldExtensionTemp,
|
||||
ScaffolderFieldExtensions as ScaffolderFieldExtensionsTemp,
|
||||
useTemplateSecrets as useTemplateSecretsTemp,
|
||||
scaffolderApiRef as scaffolderApiRefTemp,
|
||||
createScaffolderLayout as createScaffolderLayoutTemp,
|
||||
ScaffolderLayouts as ScaffolderLayoutsTemp,
|
||||
type LayoutOptions as LayoutOptionsTemp,
|
||||
type LayoutTemplate as LayoutTemplateTemp,
|
||||
type ScaffolderApi as ScaffolderApiTemp,
|
||||
type ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecretsTemp,
|
||||
type TemplateParameterSchema as TemplateParameterSchemaTemp,
|
||||
type CustomFieldExtensionSchema as CustomFieldExtensionSchemaTemp,
|
||||
type CustomFieldValidator as CustomFieldValidatorTemp,
|
||||
type FieldExtensionOptions as FieldExtensionOptionsTemp,
|
||||
type FieldExtensionComponentProps as FieldExtensionComponentPropsTemp,
|
||||
type FieldExtensionComponent as FieldExtensionComponentTemp,
|
||||
type ListActionsResponse as ListActionsResponseTemp,
|
||||
type LogEvent as LogEventTemp,
|
||||
type ScaffolderDryRunOptions as ScaffolderDryRunOptionsTemp,
|
||||
@@ -50,6 +36,23 @@ import {
|
||||
type ScaffolderTask as ScaffolderTaskTemp,
|
||||
type ScaffolderTaskOutput as ScaffolderTaskOutputTemp,
|
||||
type ScaffolderTaskStatus as ScaffolderTaskStatusTemp,
|
||||
} from '@backstage/plugin-scaffolder-common';
|
||||
|
||||
import {
|
||||
createScaffolderFieldExtension as createScaffolderFieldExtensionTemp,
|
||||
ScaffolderFieldExtensions as ScaffolderFieldExtensionsTemp,
|
||||
useTemplateSecrets as useTemplateSecretsTemp,
|
||||
scaffolderApiRef as scaffolderApiRefTemp,
|
||||
createScaffolderLayout as createScaffolderLayoutTemp,
|
||||
ScaffolderLayouts as ScaffolderLayoutsTemp,
|
||||
type LayoutOptions as LayoutOptionsTemp,
|
||||
type LayoutTemplate as LayoutTemplateTemp,
|
||||
type ScaffolderUseTemplateSecrets as ScaffolderUseTemplateSecretsTemp,
|
||||
type CustomFieldExtensionSchema as CustomFieldExtensionSchemaTemp,
|
||||
type CustomFieldValidator as CustomFieldValidatorTemp,
|
||||
type FieldExtensionOptions as FieldExtensionOptionsTemp,
|
||||
type FieldExtensionComponentProps as FieldExtensionComponentPropsTemp,
|
||||
type FieldExtensionComponent as FieldExtensionComponentTemp,
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import { rootRouteRef as rootRouteRefTemp } from './routes';
|
||||
@@ -82,7 +85,7 @@ export const useTemplateSecrets = useTemplateSecretsTemp;
|
||||
export const scaffolderApiRef = scaffolderApiRefTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderApi} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderApi} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderApi = ScaffolderApiTemp;
|
||||
/**
|
||||
@@ -92,7 +95,7 @@ export type ScaffolderApi = ScaffolderApiTemp;
|
||||
export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecretsTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#TemplateParameterSchema} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#TemplateParameterSchema} instead as this has now been moved.
|
||||
*/
|
||||
export type TemplateParameterSchema = TemplateParameterSchemaTemp;
|
||||
/**
|
||||
@@ -127,69 +130,69 @@ export type FieldExtensionComponent<_TReturnValue, _TInputProps> =
|
||||
FieldExtensionComponentTemp<_TReturnValue, _TInputProps>;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ListActionsResponse} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ListActionsResponse} instead as this has now been moved.
|
||||
*/
|
||||
export type ListActionsResponse = ListActionsResponseTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#LogEvent} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#LogEvent} instead as this has now been moved.
|
||||
*/
|
||||
export type LogEvent = LogEventTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderDryRunOptions} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderDryRunOptions} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderDryRunOptions = ScaffolderDryRunOptionsTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderDryRunResponse} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderDryRunResponse} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderDryRunResponse = ScaffolderDryRunResponseTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderGetIntegrationsListOptions} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderGetIntegrationsListOptions} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderGetIntegrationsListOptions =
|
||||
ScaffolderGetIntegrationsListOptionsTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderGetIntegrationsListResponse} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderGetIntegrationsListResponse} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderGetIntegrationsListResponse =
|
||||
ScaffolderGetIntegrationsListResponseTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderOutputlink} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderOutputlink} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderOutputlink = ScaffolderOutputLinkTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderScaffoldOptions} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderScaffoldOptions} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderScaffoldOptions = ScaffolderScaffoldOptionsTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderScaffoldResponse} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderScaffoldResponse} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderScaffoldResponse = ScaffolderScaffoldResponseTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderStreamLogsOptions} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderStreamLogsOptions} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderStreamLogsOptions = ScaffolderStreamLogsOptionsTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTask} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderTask} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderTask = ScaffolderTaskTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskOutput} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderTaskOutput} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderTaskOutput = ScaffolderTaskOutputTemp;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-react#ScaffolderTaskStatus} instead as this has now been moved.
|
||||
* @deprecated use import from {@link @backstage/plugin-scaffolder-common#ScaffolderTaskStatus} instead as this has now been moved.
|
||||
*/
|
||||
export type ScaffolderTaskStatus = ScaffolderTaskStatusTemp;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user