feat: 为 pipeline 添加运行环境,并用于配置文件的发布。

This commit is contained in:
Ivan Li 2021-10-20 22:48:08 +08:00
parent 3ba8fc9759
commit 6b9f846154
22 changed files with 64 additions and 45 deletions

1
.eslintcache Normal file

File diff suppressed because one or more lines are too long

6
package-lock.json generated
View File

@ -76,7 +76,7 @@
} }
}, },
"../configuration": { "../configuration": {
"version": "1.0.0", "version": "0.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.2", "debug": "^4.3.2",
@ -90,7 +90,7 @@
"@types/node": "^14.17.17", "@types/node": "^14.17.17",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript": "^4.4.3" "typescript": "^4.4.4"
} }
}, },
"node_modules/@angular-devkit/core": { "node_modules/@angular-devkit/core": {
@ -23187,7 +23187,7 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript": "^4.4.3" "typescript": "^4.4.4"
} }
}, },
"consola": { "consola": {

View File

@ -65,7 +65,7 @@ import { ConfigurationsModule } from './configurations/configurations.module';
playground: true, playground: true,
autoSchemaFile: true, autoSchemaFile: true,
installSubscriptionHandlers: true, installSubscriptionHandlers: true,
context: ({ req, connection, ...args }) => { context: ({ req, connection }) => {
return connection ? { req: connection.context } : { req }; return connection ? { req: connection.context } : { req };
}, },
subscriptions: { subscriptions: {

View File

@ -13,9 +13,9 @@ export class ApplicationException extends Error {
this.error = message.error; this.error = message.error;
this.message = message.message as any; this.message = message.message as any;
} else if (typeof message === 'string') { } else if (typeof message === 'string') {
super((message as unknown) as any); super(message as unknown as any);
} else { } else {
super((message as unknown) as any); super(message as unknown as any);
} }
} }

View File

@ -19,12 +19,16 @@ export class HttpExceptionFilter implements ExceptionFilter {
case 'graphql': { case 'graphql': {
const errorName = exception.message; const errorName = exception.message;
const extensions: Record<string, any> = {}; const extensions: Record<string, any> = {};
const err = exception.getResponse(); const err = exception.getResponse() as any;
if (typeof err === 'string') { if (typeof err === 'string') {
extensions.message = err; extensions.message = err;
} else { } else {
Object.assign(extensions, (err as any).extension); Object.assign(extensions, err.extension);
extensions.message = (err as any).message; if (typeof err.message === 'string') {
extensions.message = err.message;
} else {
extensions.message = err.error;
}
} }
extensions.error = errorName; extensions.error = errorName;
this.logger.error(extensions); this.logger.error(extensions);

View File

@ -126,6 +126,7 @@ export class BaseDbService<Entity extends AppBaseEntity> extends TypeormHelper {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async canYouRemoveWithIds(ids: string[]): Promise<void> { async canYouRemoveWithIds(ids: string[]): Promise<void> {
return; return;
} }

View File

@ -1,3 +1,4 @@
import { JwtService } from '@nestjs-lib/auth';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ConfigurationsResolver } from './configurations.resolver'; import { ConfigurationsResolver } from './configurations.resolver';
import { ConfigurationsService } from './configurations.service'; import { ConfigurationsService } from './configurations.service';
@ -8,10 +9,15 @@ describe('ConfigurationsResolver', () => {
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
ConfigurationsResolver,
{ {
provide: ConfigurationsService, provide: ConfigurationsService,
useValue: {}, useValue: {},
}, },
{
provide: JwtService,
useValue: {},
},
], ],
}).compile(); }).compile();

View File

@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { ConfigurationsService } from './configurations.service'; import { ConfigurationsService } from './configurations.service';
import { Configuration } from './entities/configuration.entity'; import { Configuration } from './entities/configuration.entity';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { Etcd3 } from 'etcd3';
describe('ConfigurationsService', () => { describe('ConfigurationsService', () => {
let service: ConfigurationsService; let service: ConfigurationsService;
@ -15,6 +16,10 @@ describe('ConfigurationsService', () => {
provide: getRepositoryToken(Configuration), provide: getRepositoryToken(Configuration),
useValue: {}, useValue: {},
}, },
{
provide: Etcd3,
useValue: {},
},
], ],
}).compile(); }).compile();
@ -28,7 +33,7 @@ describe('ConfigurationsService', () => {
describe('findOneByConditions', () => { describe('findOneByConditions', () => {
it('should select by projectId only', async () => { it('should select by projectId only', async () => {
const entity = new Configuration(); const entity = new Configuration();
const findOne = jest.fn((_) => Promise.resolve(entity)); const findOne = jest.fn<any, [any]>(() => Promise.resolve(entity));
service['repository'].findOne = findOne; service['repository'].findOne = findOne;
await expect( await expect(

View File

@ -26,7 +26,7 @@ export class ConfigurationsService extends BaseDbService<Configuration> {
entity = this.repository.create(dto); entity = this.repository.create(dto);
} }
entity = await this.repository.save(entity); entity = await this.repository.save(entity);
this.etcd.put(`share/config/${entity.id}`).value(entity.content); await this.syncToEtcd(entity);
return entity; return entity;
} }
@ -36,4 +36,20 @@ export class ConfigurationsService extends BaseDbService<Configuration> {
} }
return await this.repository.findOne(dto); return await this.repository.findOne(dto);
} }
async syncToEtcd({ pipelineId, id }: { pipelineId?: string; id?: string }) {
const config = await this.repository.findOneOrFail({
where: { pipelineId, id },
relations: ['pipeline', 'project'],
});
await this.etcd
.put(`share/config/${config.id}`)
.value(config.content)
.exec();
await this.etcd
.put(`share/config/${config.pipeline.environment}/${config.project.name}`)
.value(config.content)
.exec();
}
} }

View File

@ -1,4 +1,4 @@
import { Field, InputType } from '@nestjs/graphql'; import { InputType } from '@nestjs/graphql';
import { PipelineUnits } from '../enums/pipeline-units.enum'; import { PipelineUnits } from '../enums/pipeline-units.enum';
@InputType() @InputType()

View File

@ -1,6 +1,6 @@
import { Field, InputType, Int, ObjectType } from '@nestjs/graphql'; import { Field, InputType, Int, ObjectType } from '@nestjs/graphql';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsInstance, isInstance, ValidateNested } from 'class-validator'; import { IsInstance, ValidateNested } from 'class-validator';
import { WorkUnit } from './work-unit.model'; import { WorkUnit } from './work-unit.model';
@InputType('WorkUnitMetadataInput') @InputType('WorkUnitMetadataInput')

View File

@ -35,15 +35,6 @@ export class PipelineTasksResolver {
); );
} }
@Subscription(() => PipelineTask, {
resolve: (value) => {
return value;
},
})
async pipelineTaskChanged(@Args('id') id: string) {
// return await this.service.watchTaskUpdated(id);
}
@Query(() => [PipelineTask]) @Query(() => [PipelineTask])
async listPipelineTaskByPipelineId(@Args('pipelineId') pipelineId: string) { async listPipelineTaskByPipelineId(@Args('pipelineId') pipelineId: string) {
return await this.service.listTasksByPipelineId(pipelineId); return await this.service.listTasksByPipelineId(pipelineId);

View File

@ -12,7 +12,6 @@ describe('PipelineTasksService', () => {
let service: PipelineTasksService; let service: PipelineTasksService;
let module: TestingModule; let module: TestingModule;
let taskRepository: Repository<PipelineTask>; let taskRepository: Repository<PipelineTask>;
let pipelineRepository: Repository<Pipeline>;
beforeEach(async () => { beforeEach(async () => {
module = await Test.createTestingModule({ module = await Test.createTestingModule({
@ -43,7 +42,6 @@ describe('PipelineTasksService', () => {
service = module.get<PipelineTasksService>(PipelineTasksService); service = module.get<PipelineTasksService>(PipelineTasksService);
taskRepository = module.get(getRepositoryToken(PipelineTask)); taskRepository = module.get(getRepositoryToken(PipelineTask));
pipelineRepository = module.get(getRepositoryToken(Pipeline));
jest jest
.spyOn(taskRepository, 'save') .spyOn(taskRepository, 'save')
.mockImplementation(async (data: any) => data); .mockImplementation(async (data: any) => data);

View File

@ -4,7 +4,6 @@ import { PipelineTask } from './pipeline-task.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { CreatePipelineTaskInput } from './dtos/create-pipeline-task.input'; import { CreatePipelineTaskInput } from './dtos/create-pipeline-task.input';
import { Pipeline } from '../pipelines/pipeline.entity'; import { Pipeline } from '../pipelines/pipeline.entity';
import debug from 'debug';
import { AmqpConnection, RabbitRPC } from '@golevelup/nestjs-rabbitmq'; import { AmqpConnection, RabbitRPC } from '@golevelup/nestjs-rabbitmq';
import { import {
EXCHANGE_PIPELINE_TASK_TOPIC, EXCHANGE_PIPELINE_TASK_TOPIC,
@ -19,8 +18,6 @@ import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { getAppInstanceRouteKey } from '../commons/utils/rabbit-mq'; import { getAppInstanceRouteKey } from '../commons/utils/rabbit-mq';
import { ROUTE_PIPELINE_TASK_KILL } from './pipeline-tasks.constants'; import { ROUTE_PIPELINE_TASK_KILL } from './pipeline-tasks.constants';
const log = debug('fennec:pipeline-tasks:service');
@Injectable() @Injectable()
export class PipelineTasksService { export class PipelineTasksService {
constructor( constructor(

View File

@ -28,4 +28,8 @@ export class CreatePipelineInput {
@ValidateNested() @ValidateNested()
@IsInstance(WorkUnitMetadata) @IsInstance(WorkUnitMetadata)
workUnitMetadata: WorkUnitMetadata; workUnitMetadata: WorkUnitMetadata;
@IsString()
@MaxLength(100)
environment: string;
} }

View File

@ -12,7 +12,7 @@ export class Pipeline extends AppBaseEntity {
@Column() @Column()
projectId: string; projectId: string;
@Column({ comment: 'eg: remotes/origin/master' }) @Column({ comment: 'E.g., remotes/origin/master' })
branch: string; branch: string;
@Column() @Column()
@ -20,4 +20,7 @@ export class Pipeline extends AppBaseEntity {
@Column({ type: 'jsonb' }) @Column({ type: 'jsonb' })
workUnitMetadata: WorkUnitMetadata; workUnitMetadata: WorkUnitMetadata;
@Column()
environment: string;
} }

View File

@ -2,13 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing';
import { PipelinesService } from './pipelines.service'; import { PipelinesService } from './pipelines.service';
import { Pipeline } from './pipeline.entity'; import { Pipeline } from './pipeline.entity';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Project } from '../projects/project.entity'; import { Project } from '../projects/project.entity';
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
describe('PipelinesService', () => { describe('PipelinesService', () => {
let service: PipelinesService; let service: PipelinesService;
let repository: Repository<Pipeline>;
let pipeline: Pipeline; let pipeline: Pipeline;
beforeEach(async () => { beforeEach(async () => {
@ -40,7 +38,6 @@ describe('PipelinesService', () => {
}).compile(); }).compile();
service = module.get<PipelinesService>(PipelinesService); service = module.get<PipelinesService>(PipelinesService);
repository = module.get(getRepositoryToken(Pipeline));
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -19,7 +19,9 @@ import { plainToClass } from 'class-transformer';
@Injectable() @Injectable()
export class PipelinesService extends BaseDbService<Pipeline> { export class PipelinesService extends BaseDbService<Pipeline> {
readonly uniqueFields: Array<Array<keyof Pipeline>> = [['projectId', 'name']]; readonly uniqueFields: Array<Array<keyof Pipeline>> = [
['projectId', 'name', 'environment'],
];
constructor( constructor(
@InjectRepository(Pipeline) @InjectRepository(Pipeline)
readonly repository: Repository<Pipeline>, readonly repository: Repository<Pipeline>,

View File

@ -1,5 +1,5 @@
import { ObjectType } from '@nestjs/graphql'; import { ObjectType } from '@nestjs/graphql';
import { Entity, Column, DeleteDateColumn } from 'typeorm'; import { Entity, Column } from 'typeorm';
import { AppBaseEntity } from '../commons/entities/app-base-entity'; import { AppBaseEntity } from '../commons/entities/app-base-entity';
@ObjectType() @ObjectType()

View File

@ -1,10 +1,5 @@
import { ObjectType, Field } from '@nestjs/graphql'; import { ObjectType, Field } from '@nestjs/graphql';
import { import { BranchSummaryBranch } from 'simple-git';
LogResult,
DefaultLogFields,
BranchSummary,
BranchSummaryBranch,
} from 'simple-git';
@ObjectType() @ObjectType()
export class Branch implements BranchSummaryBranch { export class Branch implements BranchSummaryBranch {

View File

@ -161,7 +161,7 @@ describe('ReposService', () => {
const project = new Project(); const project = new Project();
const pipeline = new Pipeline(); const pipeline = new Pipeline();
pipeline.branch = 'test'; pipeline.branch = 'test';
const fetch = jest.fn((_: any) => Promise.resolve()); const fetch = jest.fn<any, [any]>(() => Promise.resolve());
pipeline.project = project; pipeline.project = project;
const getGit = jest.spyOn(service, 'getGit').mockImplementation(() => const getGit = jest.spyOn(service, 'getGit').mockImplementation(() =>
Promise.resolve({ Promise.resolve({
@ -182,7 +182,7 @@ describe('ReposService', () => {
const project = new Project(); const project = new Project();
const pipeline = new Pipeline(); const pipeline = new Pipeline();
pipeline.branch = 'test'; pipeline.branch = 'test';
const fetch = jest.fn((_: any) => Promise.resolve()); const fetch = jest.fn<any, [any]>(() => Promise.resolve());
pipeline.project = project; pipeline.project = project;
const getGit = jest const getGit = jest
.spyOn(service, 'getGit') .spyOn(service, 'getGit')
@ -196,7 +196,7 @@ describe('ReposService', () => {
const project = new Project(); const project = new Project();
const pipeline = new Pipeline(); const pipeline = new Pipeline();
pipeline.branch = 'test'; pipeline.branch = 'test';
const fetch = jest.fn((_: any) => Promise.reject('error')); const fetch = jest.fn<any, [any]>(() => Promise.reject('error'));
pipeline.project = project; pipeline.project = project;
const getGit = jest.spyOn(service, 'getGit').mockImplementation(() => const getGit = jest.spyOn(service, 'getGit').mockImplementation(() =>
Promise.resolve({ Promise.resolve({

View File

@ -1,4 +1,4 @@
import { MiddlewareConsumer, Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { PipelineTasksModule } from '../pipeline-tasks/pipeline-tasks.module'; import { PipelineTasksModule } from '../pipeline-tasks/pipeline-tasks.module';
import { GiteaWebhooksController } from './gitea-webhooks.controller'; import { GiteaWebhooksController } from './gitea-webhooks.controller';
@ -10,5 +10,4 @@ import { WebhooksService } from './webhooks.service';
controllers: [GiteaWebhooksController], controllers: [GiteaWebhooksController],
providers: [WebhooksService], providers: [WebhooksService],
}) })
export class WebhooksModule { export class WebhooksModule {}
}