Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): impl application update & deletion APIs #440

Merged
merged 1 commit into from
Nov 28, 2022
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
4 changes: 2 additions & 2 deletions server/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Controller, Get, Query, Req, Res, UseGuards } from '@nestjs/common'
import { ApiBearerAuth, ApiHeader, ApiTags } from '@nestjs/swagger'
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'
import { Response } from 'express'
import { AuthService } from './auth/auth.service'
import { JwtAuthGuard } from './auth/jwt-auth.guard'
import { ResponseUtil } from './common/response'
import { IRequest } from './common/types'

@ApiTags('Authentication')
@ApiBearerAuth('Authorization')
@Controller()
export class AppController {
constructor(private readonly authService: AuthService) {}
Expand Down Expand Up @@ -49,6 +48,7 @@ export class AppController {

@UseGuards(JwtAuthGuard)
@Get('profile')
@ApiBearerAuth('Authorization')
async getProfile(@Req() request: IRequest) {
const user = request.user
return ResponseUtil.ok(user)
Expand Down
66 changes: 54 additions & 12 deletions server/src/applications/applications.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
HttpException,
HttpStatus,
} from '@nestjs/common'
import { ApiOperation, ApiTags } from '@nestjs/swagger'
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'
import { IRequest } from 'src/common/types'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { ApiResponseUtil, ResponseUtil } from '../common/response'
Expand All @@ -23,6 +23,7 @@ import { Application, ApplicationList } from './entities/application.entity'

@ApiTags('Application')
@Controller('applications')
@ApiBearerAuth('Authorization')
export class ApplicationsController {
constructor(private readonly appService: ApplicationsService) {}

Expand All @@ -31,7 +32,7 @@ export class ApplicationsController {
* @returns
*/
@ApiResponseUtil(Application)
@ApiOperation({ summary: 'Get application list' })
@ApiOperation({ summary: 'Create a new application' })
@UseGuards(JwtAuthGuard)
@Post()
async create(@Body() dto: CreateApplicationDto, @Req() req: IRequest) {
Expand All @@ -56,15 +57,28 @@ export class ApplicationsController {
return ResponseUtil.ok(app)
}

/**
* Get user application list
* @param req
* @returns
*/
@ApiResponseUtil(ApplicationList)
@UseGuards(JwtAuthGuard)
@Get()
@ApiOperation({ summary: 'Get user application list' })
async findAll(@Req() req: IRequest) {
const user = req.user
const data = this.appService.findAllByUser(user.id)
return ResponseUtil.ok(data)
}

/**
* Get an application by appid
* @param appid
* @param req
* @returns
*/
@ApiOperation({ summary: 'Get an application by appid' })
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
@Get(':appid')
async findOne(@Param('appid') appid: string, @Req() req: IRequest) {
Expand All @@ -77,19 +91,47 @@ export class ApplicationsController {
return ResponseUtil.ok(data)
}

@Patch(':id')
update(
@Param('id') id: string,
@Body() updateApplicationDto: UpdateApplicationDto,
) {
return this.appService.update(+id, updateApplicationDto)
/**
* Update an application
* @param dto
* @param req
* @returns
*/
@ApiOperation({ summary: 'Update an application' })
@ApiResponseUtil(Application)
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
@Patch(':appid')
async update(@Body() dto: UpdateApplicationDto, @Req() req: IRequest) {
// check dto
const error = dto.validate()
if (error) {
return ResponseUtil.error(error)
}

// update app
const app = req.application
const res = await this.appService.update(app, dto)
if (res === null) {
return ResponseUtil.error('update application error')
}
return ResponseUtil.ok(res)
}

@Delete(':id')
/**
* Delete an application
* @returns
*/
@Delete(':appid')
@UseGuards(JwtAuthGuard, ApplicationAuthGuard)
@ApiOperation({
summary: 'TODO: delete application by its appid',
summary: 'Delete an application',
})
remove(@Param('id') id: string) {
return this.appService.remove(+id)
async remove(@Req() req: IRequest) {
const app = req.application
const res = await this.appService.remove(app)
if (res === null) {
return ResponseUtil.error('delete application error')
}
return ResponseUtil.ok(res)
}
}
4 changes: 2 additions & 2 deletions server/src/applications/applications.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('AppService create app', () => {
it('should create app', async () => {
appid = service.generateAppid(6)
const dto = new CreateApplicationDto()
dto.name = appid
dto.displayName = appid
dto.state = ApplicationState.ApplicationStateRunning
dto.region = 'default'
dto.bundleName = 'mini'
Expand All @@ -66,7 +66,7 @@ describe('AppService create app', () => {
const res = await service.create(userid, appid, dto)
expect(res).not.toBeNull()
expect(res.kind).toBe('Application')
expect(res.metadata.name).toBe(dto.name)
expect(res.metadata.name).toBe(dto.displayName)
expect(res.spec.state).toBe(ApplicationState.ApplicationStateRunning)
})

Expand Down
28 changes: 23 additions & 5 deletions server/src/applications/applications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ export class ApplicationsService {
async create(userid: string, appid: string, dto: CreateApplicationDto) {
// create app resources
const namespace = GetApplicationNamespaceById(appid)
const app = new Application(dto.name, namespace)
const app = new Application(appid, namespace)
app.setUserId(userid)
app.setDisplayName(dto.displayName)

app.spec = new ApplicationSpec({
appid,
Expand Down Expand Up @@ -118,11 +119,28 @@ export class ApplicationsService {
return null
}

update(id: number, dto: UpdateApplicationDto) {
return `This action updates a #${id} app`
async update(app: Application, dto: UpdateApplicationDto) {
if (dto.displayName) {
app.setDisplayName(dto.displayName)
}
app.spec.state = dto.state || app.spec.state

try {
const res = await this.k8sClient.patchCustomObject(app)
return res as Application
} catch (err) {
this.logger.error(err)
return null
}
}

remove(id: number) {
return `This action removes a #${id} app`
async remove(app: Application) {
try {
const res = await this.k8sClient.deleteCustomObject(app)
return res
} catch (error) {
this.logger.error(error)
return null
}
}
}
4 changes: 2 additions & 2 deletions server/src/applications/dto/create-application.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ApplicationState } from '../entities/application.entity'

export class CreateApplicationDto {
@ApiProperty({ required: true })
name: string
displayName: string

@ApiProperty({
default: ApplicationState.ApplicationStateRunning,
Expand All @@ -22,7 +22,7 @@ export class CreateApplicationDto {
runtimeName: string

validate(): string | null {
if (!this.name) {
if (!this.displayName) {
return 'name is required'
}
if (!this.state) {
Expand Down
27 changes: 24 additions & 3 deletions server/src/applications/dto/update-application.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
import { PartialType } from '@nestjs/swagger'
import { CreateApplicationDto } from './create-application.dto'
import { ApiPropertyOptional } from '@nestjs/swagger'
import { ApplicationState } from '../entities/application.entity'

export class UpdateApplicationDto extends PartialType(CreateApplicationDto) {}
export class UpdateApplicationDto {
/**
* Application name
*/
@ApiPropertyOptional()
displayName?: string

@ApiPropertyOptional({
enum: ['running', 'stopped'],
})
state?: ApplicationState

validate() {
if (this.state) {
if (!['running', 'stopped'].includes(this.state)) {
return 'state must be running or stopped'
}
}

return null
}
}
11 changes: 11 additions & 0 deletions server/src/applications/entities/application.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,20 @@ export class Application implements KubernetesObject {
}
}

setDisplayName(name: string) {
this.metadata.labels = {
...this.metadata.labels,
[ResourceLabels.DISPLAY_NAME]: name,
}
}

get userid() {
return this.metadata.labels?.[ResourceLabels.USER_ID]
}

get displayName() {
return this.metadata.labels?.[ResourceLabels.DISPLAY_NAME]
}
}
export class ApplicationList {
@ApiProperty()
Expand Down
4 changes: 4 additions & 0 deletions server/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export class ResourceLabels {
return 'laf.dev/appid'
}

static get DISPLAY_NAME() {
return 'laf.dev/display.name'
}

static get NAMESPACE_TYPE() {
return 'laf.dev/namespace.type'
}
Expand Down
25 changes: 22 additions & 3 deletions server/src/core/kubernetes.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ describe.skip('list custom objects with label', () => {
it('should be able to list custom objects with label', async () => {
const userid = 'test-user-id'
const res = await service.customObjectApi.listClusterCustomObject(
'application.laf.dev',
'v1',
'applications',
Application.GVK.group,
Application.GVK.version,
Application.GVK.plural,
undefined,
undefined,
undefined,
Expand Down Expand Up @@ -115,3 +115,22 @@ describe.skip('patch custom objects', () => {
console.log('patched', res2)
})
})

describe.skip('delete custom objects', () => {
it('should be able to delete custom objects', async () => {
const name = 'efme9x'
const namespace = name
const res = await service.customObjectApi.getNamespacedCustomObject(
Application.GVK.group,
Application.GVK.version,
namespace,
Application.GVK.plural,
name,
)

const data = res.body as Application

const res2 = await service.deleteCustomObject(data)
console.log('deleted', res2)
})
})
15 changes: 15 additions & 0 deletions server/src/core/kubernetes.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,19 @@ export class KubernetesService {

return response.body
}

async deleteCustomObject(spec: KubernetesObject) {
const client = this.customObjectApi
const gvk = GroupVersionKind.fromKubernetesObject(spec)

const response = await client.deleteNamespacedCustomObject(
gvk.group,
gvk.version,
spec.metadata.namespace,
gvk.plural,
spec.metadata.name,
)

return response.body
}
}