Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions workspaces/x2a/.changeset/fresh-bobcats-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@red-hat-developer-hub/backstage-plugin-x2a-backend': patch
'@red-hat-developer-hub/backstage-plugin-x2a-common': patch
'@red-hat-developer-hub/backstage-plugin-x2a': patch
---

Add module and project status.
4 changes: 2 additions & 2 deletions workspaces/x2a/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
"start": "NODE_OPTIONS=--no-node-snapshot ; backstage-cli repo start",
"build:backend": "yarn workspace backend build",
"build:all": "backstage-cli repo build --all",
"build:api-reports": "yarn build:api-reports:only --tsc",
"build:api-reports": "yarn tsc && yarn build:api-reports:only",
"build:api-reports:only": "backstage-repo-tools api-reports -o ae-wrong-input-file-type,ae-undocumented --validate-release-tags --exclude client/src/schema/openapi/generated",
"build:knip-reports": "backstage-repo-tools knip-reports",
"build-image": "yarn workspace backend build-image",
"openapi-generate": "cd plugins/x2a-backend && yarn openapi-generate",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"tsc:full": "tsc --skipLibCheck true --incremental false",
"clean": "backstage-cli repo clean",
"test": "backstage-cli repo test",
"test:all": "yarn openapi-generate && yarn prettier:check && yarn lint:all && backstage-cli repo test --coverage",
Expand Down
71 changes: 71 additions & 0 deletions workspaces/x2a/plugins/x2a-backend/src/router/modules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,33 @@ describe('createRouter – modules', () => {
LONG_TEST_TIMEOUT,
);

it.each(supportedDatabaseIds)(
'should return each module with status field from service enrichment - %p',
async databaseId => {
const { client } = await createDatabase(databaseId);
const x2aDatabase = X2ADatabaseService.create({
logger: mockServices.logger.mock(),
dbClient: client,
});
const app = await createApp(client);
const project = await createTestProject(x2aDatabase);
await createTestModule(x2aDatabase, project.id, {
name: 'Module A',
sourcePath: '/a',
});

const response = await request(app)
.get(`/projects/${project.id}/modules`)
.send();

expect(response.status).toBe(200);
expect(response.body).toHaveLength(1);
expect(response.body[0]).toHaveProperty('status');
expect(response.body[0].status).toBe('pending');
},
LONG_TEST_TIMEOUT,
);

it.each(supportedDatabaseIds)(
'should return 404 when project does not exist - %p',
async databaseId => {
Expand Down Expand Up @@ -357,5 +384,49 @@ describe('createRouter – modules', () => {
);
},
);

it.each(supportedDatabaseIds)(
'should accept optional aapCredentials and pass them to kubeService.createJob - %p',
async databaseId => {
const { client } = await createDatabase(databaseId);
const x2aDatabase = X2ADatabaseService.create({
logger: mockServices.logger.mock(),
dbClient: client,
});
const project = await createTestProject(x2aDatabase);
const module = await createTestModule(x2aDatabase, project.id);

const mockCreateJob = jest
.fn()
.mockResolvedValue({ k8sJobName: 'k8s-job' });
const appWithMock = await createApp(client, undefined, undefined, {
createJob: mockCreateJob,
});

const aapCredentials = {
url: 'https://aap.example.com',
orgName: 'Default',
oauthToken: 'oauth-token',
};
const response = await request(appWithMock)
.post(`/projects/${project.id}/modules/${module.id}/run`)
.send({
...runBody,
aapCredentials,
});

expect(response.status).toBe(200);
expect(mockCreateJob).toHaveBeenCalledTimes(1);
expect(mockCreateJob).toHaveBeenCalledWith(
expect.objectContaining({
aapCredentials,
phase: 'analyze',
moduleId: module.id,
moduleName: module.name,
}),
);
},
LONG_TEST_TIMEOUT,
);
});
});
52 changes: 2 additions & 50 deletions workspaces/x2a/plugins/x2a-backend/src/router/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,9 @@ import { z } from 'zod';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { InputError, NotFoundError } from '@backstage/errors';
import { Module } from '@red-hat-developer-hub/backstage-plugin-x2a-common';

import type { RouterDeps } from './types';
import {
getUserRef,
reconcileJobStatus,
removeSensitiveFromJob,
} from './common';
import { getUserRef, reconcileJobStatus } from './common';

export function registerModuleRoutes(
router: express.Router,
Expand Down Expand Up @@ -54,50 +49,7 @@ export function registerModuleRoutes(
// List modules
const modules = await x2aDatabase.listModules({ projectId });

// TODO: This can be optimized by using a single query to list all jobs for all modules.
const lastAnalyzeJobsOfModules = await Promise.all(
modules.map(module =>
x2aDatabase.listJobs({
projectId,
moduleId: module.id,
phase: 'analyze',
lastJobOnly: true,
}),
),
);
const lastMigrateJobsOfModules = await Promise.all(
modules.map(module =>
x2aDatabase.listJobs({
projectId,
moduleId: module.id,
phase: 'migrate',
lastJobOnly: true,
}),
),
);
const lastPublishJobsOfModules = await Promise.all(
modules.map(module =>
x2aDatabase.listJobs({
projectId,
moduleId: module.id,
phase: 'publish',
lastJobOnly: true,
}),
),
);

const response: Array<Module> = modules.map((module, idxModule) => {
return {
...module,
analyze: removeSensitiveFromJob(lastAnalyzeJobsOfModules[idxModule][0]),
migrate: removeSensitiveFromJob(lastMigrateJobsOfModules[idxModule][0]),
publish: removeSensitiveFromJob(lastPublishJobsOfModules[idxModule][0]),

// TODO: calculate module's status from the last job
};
});

res.json(response);
res.json(modules);
});

// TODO: This is a TEMPORARY endpoint for testing only.
Expand Down
83 changes: 81 additions & 2 deletions workspaces/x2a/plugins/x2a-backend/src/schema/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,6 @@ components:
targetRepoBranch:
description: Branch of the target repository
type: string
# TODO: status as optional field
createdAt:
type: string
format: date-time
Expand All @@ -468,6 +467,9 @@ components:
migrationPlan:
$ref: '#/components/schemas/Artifact'
description: Project migration plan artifact (by init phase)
status:
$ref: '#/components/schemas/ProjectStatus'
description: Project status calculated from the status of its modules
required:
- id
- name
Expand Down Expand Up @@ -501,7 +503,11 @@ components:
$ref: '#/components/schemas/Job'
publish:
$ref: '#/components/schemas/Job'
# TODO: module status
status:
$ref: '#/components/schemas/ModuleStatus'
errorDetails:
type: string
description: Detailed error information if the module failed to execute
required:
- id
- name
Expand All @@ -516,6 +522,79 @@ components:
- success
- error

ModuleStatus:
type: string
description: |
Module status is the status of the last job of its last phase.
If a later retrigger for an earlier phase fails (e.g. when retrigger on analyze
fails but a former migrate already passed), the modules status should not change (is still based on the last phase).
The pending state is used for modules that are scheduled for execution but not yet actually running. If a module
is in pending state for long time, it can refer to an issue with the OCP setup.
enum:
- pending
- running
- success
- error

ProjectStatusState:
type: string
description: |
Project status state.
It is calculated from the status of its modules.
- created: Project is created but not yet initialized
- initializing: Project's init job is running or scheduling
- initialized: Project's init job finished successfully. Either module list is empty or all modules are in pending state.
- inProgress: At least one module is beyond the pending state.
- completed: All modules are in success state
- failed: At least one module is in error state
enum:
- created
- initializing
- initialized
- inProgress
- completed
- failed

ModulesStatusSummary:
type: object
properties:
total:
type: integer
description: Total number of modules in the project
finished:
type: integer
description: Number of modules in success state of the publish phase (no more work is needed)
waiting:
type: integer
description: Number of modules in success state of a non-publish phase (means waiting for human interaction)
pending:
type: integer
description: Number of modules in pending state (scheduled for execution but not actually running)
running:
type: integer
description: Number of modules in running state (actually running)
error:
type: integer
description: Number of modules in error state (execution is over but failed)
required:
- total
- finished
- waiting
- pending
- running
- error

ProjectStatus:
type: object
properties:
state:
$ref: '#/components/schemas/ProjectStatusState'
modulesSummary:
$ref: '#/components/schemas/ModulesStatusSummary'
required:
- state
- modulesSummary

Job:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { Job } from '../models/Job.model';
import { ModuleStatus } from '../models/ModuleStatus.model';

/**
* @public
Expand All @@ -42,4 +43,9 @@ export interface Module {
analyze?: Job;
migrate?: Job;
publish?: Job;
status?: ModuleStatus;
/**
* Detailed error information if the module failed to execute
*/
errorDetails?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Red Hat, Inc.
*
* 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.
*/

// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************

/**
* @public
*/
export type ModuleStatus = 'pending' | 'running' | 'success' | 'error';
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Red Hat, Inc.
*
* 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.
*/

// ******************************************************************
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************

/**
* @public
*/
export interface ModulesStatusSummary {
/**
* Total number of modules in the project
*/
total: number;
/**
* Number of modules in success state of the publish phase (no more work is needed)
*/
finished: number;
/**
* Number of modules in success state of a non-publish phase (means waiting for human interaction)
*/
waiting: number;
/**
* Number of modules in pending state (scheduled for execution but not actually running)
*/
pending: number;
/**
* Number of modules in running state (actually running)
*/
running: number;
/**
* Number of modules in error state (execution is over but failed)
*/
error: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. *
// ******************************************************************
import { Artifact } from '../models/Artifact.model';
import { ProjectStatus } from '../models/ProjectStatus.model';

/**
* @public
Expand Down Expand Up @@ -64,4 +65,5 @@ export interface Project {
*/
createdBy: string;
migrationPlan?: Artifact;
status?: ProjectStatus;
}
Loading