feat: add configuration-center module.

This commit is contained in:
Ivan Li 2021-10-16 10:54:35 +08:00
parent d5f49531e9
commit 3ba8fc9759
15 changed files with 249 additions and 10 deletions

View File

@ -20,6 +20,7 @@ import { LoggerModule } from 'nestjs-pino';
import { EtcdModule } from 'nestjs-etcd'; import { EtcdModule } from 'nestjs-etcd';
import pinoPretty from 'pino-pretty'; import pinoPretty from 'pino-pretty';
import { fromPairs, map, pipe, toPairs } from 'ramda'; import { fromPairs, map, pipe, toPairs } from 'ramda';
import { ConfigurationsModule } from './configurations/configurations.module';
@Module({ @Module({
imports: [ imports: [
@ -106,6 +107,7 @@ import { fromPairs, map, pipe, toPairs } from 'ramda';
}), }),
WebhooksModule, WebhooksModule,
CommonsModule, CommonsModule,
ConfigurationsModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService, AppResolver], providers: [AppService, AppResolver],

View File

@ -1,8 +1,9 @@
import { Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { PasswordConverter } from './services/password-converter'; import { PasswordConverter } from './services/password-converter';
import { RedisMutexModule } from './redis-mutex/redis-mutex.module'; import { RedisMutexModule } from './redis-mutex/redis-mutex.module';
import { AuthModule } from '@nestjs-lib/auth'; import { AuthModule } from '@nestjs-lib/auth';
@Global()
@Module({ @Module({
imports: [RedisMutexModule, AuthModule], imports: [RedisMutexModule, AuthModule],
providers: [PasswordConverter], providers: [PasswordConverter],

View File

@ -0,0 +1,11 @@
import { Configuration } from './entities/configuration.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Module } from '@nestjs/common';
import { ConfigurationsService } from './configurations.service';
import { ConfigurationsResolver } from './configurations.resolver';
@Module({
imports: [TypeOrmModule.forFeature([Configuration])],
providers: [ConfigurationsResolver, ConfigurationsService],
})
export class ConfigurationsModule {}

View File

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigurationsResolver } from './configurations.resolver';
import { ConfigurationsService } from './configurations.service';
describe('ConfigurationsResolver', () => {
let resolver: ConfigurationsResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: ConfigurationsService,
useValue: {},
},
],
}).compile();
resolver = module.get<ConfigurationsResolver>(ConfigurationsResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,39 @@
import { UnprocessableEntityException } from '@nestjs/common';
import { GetConfigurationArgs } from './dto/get-configuration.args';
import { SetConfigurationInput } from './dto/set-configuration.input';
import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
import { ConfigurationsService } from './configurations.service';
import { Configuration } from './entities/configuration.entity';
import { any, pipe, values } from 'ramda';
import { AccountRole, Roles } from '@nestjs-lib/auth';
@Roles(AccountRole.admin, AccountRole.super)
@Resolver(() => Configuration)
export class ConfigurationsResolver {
constructor(private readonly configurationsService: ConfigurationsService) {}
@Mutation(() => Configuration)
setConfiguration(
@Args('setConfigurationInput', { type: () => SetConfigurationInput })
setConfigurationInput: SetConfigurationInput,
) {
return this.configurationsService.setConfiguration(setConfigurationInput);
}
@Query(() => Configuration, { nullable: true })
getConfiguration(
@Args()
getConfigurationArgs: GetConfigurationArgs,
) {
if (
pipe(
values,
any((value) => !value),
)(getConfigurationArgs)
) {
throw new UnprocessableEntityException('Must pass a parameter');
}
return this.configurationsService.findOneByConditions(getConfigurationArgs);
}
}

View File

@ -0,0 +1,44 @@
import { getRepositoryToken } from '@nestjs/typeorm';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigurationsService } from './configurations.service';
import { Configuration } from './entities/configuration.entity';
import { IsNull } from 'typeorm';
describe('ConfigurationsService', () => {
let service: ConfigurationsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ConfigurationsService,
{
provide: getRepositoryToken(Configuration),
useValue: {},
},
],
}).compile();
service = module.get<ConfigurationsService>(ConfigurationsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('findOneByConditions', () => {
it('should select by projectId only', async () => {
const entity = new Configuration();
const findOne = jest.fn((_) => Promise.resolve(entity));
service['repository'].findOne = findOne;
await expect(
service.findOneByConditions({ projectId: 'uuid' }),
).resolves.toEqual(entity);
expect(findOne.mock.calls[0][0]).toMatchObject({
projectId: 'uuid',
pipelineId: IsNull(),
});
});
});
});

View File

@ -0,0 +1,39 @@
import { GetConfigurationArgs } from './dto/get-configuration.args';
import { BaseDbService } from './../commons/services/base-db.service';
import { Injectable } from '@nestjs/common';
import { Configuration } from './entities/configuration.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { FindConditions, IsNull, Repository } from 'typeorm';
import { SetConfigurationInput } from './dto/set-configuration.input';
import { pick } from 'ramda';
import { Etcd3 } from 'etcd3';
@Injectable()
export class ConfigurationsService extends BaseDbService<Configuration> {
constructor(
@InjectRepository(Configuration)
configurationRepository: Repository<Configuration>,
private readonly etcd: Etcd3,
) {
super(configurationRepository);
}
async setConfiguration(dto: SetConfigurationInput) {
let entity = await this.repository.findOne(
pick(['pipelineId', 'projectId'], dto),
);
if (!entity) {
entity = this.repository.create(dto);
}
entity = await this.repository.save(entity);
this.etcd.put(`share/config/${entity.id}`).value(entity.content);
return entity;
}
async findOneByConditions(dto: FindConditions<GetConfigurationArgs>) {
if (dto.projectId && !dto.pipelineId) {
dto.pipelineId = IsNull();
}
return await this.repository.findOne(dto);
}
}

View File

@ -0,0 +1,17 @@
import { ArgsType } from '@nestjs/graphql';
import { IsUUID, IsOptional } from 'class-validator';
@ArgsType()
export class GetConfigurationArgs {
@IsUUID()
@IsOptional()
pipelineId?: string;
@IsUUID()
@IsOptional()
projectId?: string;
@IsUUID()
@IsOptional()
id?: string;
}

View File

@ -0,0 +1,26 @@
import { ConfigurationLanguage } from './../enums/configuration-language.enum';
import { IsEnum, IsString, IsUUID, Length, IsOptional } from 'class-validator';
import { InputType } from '@nestjs/graphql';
@InputType()
export class SetConfigurationInput {
@IsOptional()
@IsUUID()
id?: string;
@IsUUID()
pipelineId: string;
@IsUUID()
projectId: string;
@IsString()
content: string;
@IsEnum(ConfigurationLanguage)
language: ConfigurationLanguage;
@Length(0, 100)
@IsOptional()
name = 'Default Configuration';
}

View File

@ -0,0 +1,35 @@
import { Project } from './../../projects/project.entity';
import { ConfigurationLanguage } from './../enums/configuration-language.enum';
import { Pipeline } from './../../pipelines/pipeline.entity';
import { AppBaseEntity } from './../../commons/entities/app-base-entity';
import { ObjectType } from '@nestjs/graphql';
import { Column, Entity, ManyToOne } from 'typeorm';
@Entity()
@ObjectType()
export class Configuration extends AppBaseEntity {
@ManyToOne(() => Pipeline)
pipeline: Pipeline;
@Column({ unique: true, nullable: true })
pipelineId: string;
@ManyToOne(() => Project)
project: Project;
@Column()
projectId: string;
@Column({ comment: 'language defined in type field.' })
content: string;
@Column({ comment: '配置名称' })
name: string;
@Column({
type: 'enum',
enum: ConfigurationLanguage,
comment: 'configuration content language',
})
language: ConfigurationLanguage;
}

View File

@ -0,0 +1,10 @@
import { registerEnumType } from '@nestjs/graphql';
export enum ConfigurationLanguage {
JavaScript = 'JavaScript',
YAML = 'YAML',
}
registerEnumType(ConfigurationLanguage, {
name: 'ConfigurationLanguage',
});

View File

@ -16,12 +16,10 @@ import {
} from './pipeline-tasks.constants'; } from './pipeline-tasks.constants';
import { PipelineTaskLogger } from './pipeline-task.logger'; import { PipelineTaskLogger } from './pipeline-task.logger';
import { PipelineTaskFlushService } from './pipeline-task-flush.service'; import { PipelineTaskFlushService } from './pipeline-task-flush.service';
import { CommonsModule } from '../commons/commons.module';
import { DeployByPm2Service } from './runners/deploy-by-pm2/deploy-by-pm2.service'; import { DeployByPm2Service } from './runners/deploy-by-pm2/deploy-by-pm2.service';
@Module({ @Module({
imports: [ imports: [
CommonsModule,
TypeOrmModule.forFeature([PipelineTask, Pipeline]), TypeOrmModule.forFeature([PipelineTask, Pipeline]),
RedisModule, RedisModule,
ReposModule, ReposModule,

View File

@ -5,14 +5,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Pipeline } from './pipeline.entity'; import { Pipeline } from './pipeline.entity';
import { CommitLogsResolver } from './commit-logs.resolver'; import { CommitLogsResolver } from './commit-logs.resolver';
import { PipelineTasksModule } from '../pipeline-tasks/pipeline-tasks.module'; import { PipelineTasksModule } from '../pipeline-tasks/pipeline-tasks.module';
import { ReposModule } from '../repos/repos.module';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { CommonsModule } from '../commons/commons.module';
@Module({ @Module({
imports: [ imports: [
CommonsModule,
TypeOrmModule.forFeature([Pipeline]), TypeOrmModule.forFeature([Pipeline]),
PipelineTasksModule, PipelineTasksModule,
RabbitMQModule.forRootAsync(RabbitMQModule, { RabbitMQModule.forRootAsync(RabbitMQModule, {

View File

@ -6,11 +6,9 @@ import { Project } from './project.entity';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { EXCHANGE_PROJECT_FANOUT } from './projects.constants'; import { EXCHANGE_PROJECT_FANOUT } from './projects.constants';
import { CommonsModule } from '../commons/commons.module';
@Module({ @Module({
imports: [ imports: [
CommonsModule,
TypeOrmModule.forFeature([Project]), TypeOrmModule.forFeature([Project]),
RabbitMQModule.forRootAsync(RabbitMQModule, { RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule], imports: [ConfigModule],

View File

@ -7,14 +7,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { ProjectsModule } from '../projects/projects.module'; import { ProjectsModule } from '../projects/projects.module';
import { EXCHANGE_REPO } from './repos.constants'; import { EXCHANGE_REPO } from './repos.constants';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { CommonsModule } from '../commons/commons.module';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Project]), TypeOrmModule.forFeature([Project]),
ConfigModule, ConfigModule,
ProjectsModule, ProjectsModule,
CommonsModule,
RabbitMQModule.forRootAsync(RabbitMQModule, { RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule], imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({ useFactory: (configService: ConfigService) => ({