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): add user quota to limit user resource create #1498

Merged
merged 7 commits into from
Sep 5, 2023
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
26 changes: 22 additions & 4 deletions server/src/application/application.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { User } from 'src/user/entities/user'
import { GroupWithRole } from 'src/group/entities/group'
import { isEqual } from 'lodash'
import { InstanceService } from 'src/instance/instance.service'
import { QuotaService } from 'src/user/quota.service'

@ApiTags('Application')
@Controller('applications')
Expand All @@ -71,6 +72,7 @@ export class ApplicationController {
private readonly account: AccountService,
private readonly resource: ResourceService,
private readonly runtimeDomain: RuntimeDomainService,
private readonly quotaServiceTsService: QuotaService,
) {}

/**
Expand Down Expand Up @@ -117,10 +119,14 @@ export class ApplicationController {
}
}

// one user can only have 20 applications in one region
const count = await this.application.countByUser(user._id)
if (count > 20) {
return ResponseUtil.error(`too many applications, limit is 20`)
// check if a user exceeds the resource limit in a region
const limitResource = await this.quotaServiceTsService.resourceLimit(
user._id,
dto.cpu,
dto.memory,
)
if (limitResource) {
return ResponseUtil.error(limitResource)
}

// check account balance
Expand Down Expand Up @@ -311,6 +317,7 @@ export class ApplicationController {
@Param('appid') appid: string,
@Body() dto: UpdateApplicationBundleDto,
@InjectApplication() app: ApplicationWithRelations,
@InjectUser() user: User,
) {
const error = dto.autoscaling.validate()
if (error) {
Expand Down Expand Up @@ -341,6 +348,17 @@ export class ApplicationController {
return ResponseUtil.error('invalid resource specification')
}

// check if a user exceeds the resource limit in a region
const limitResource = await this.quotaServiceTsService.resourceLimit(
user._id,
dto.cpu,
dto.memory,
appid,
)
if (limitResource) {
return ResponseUtil.error(limitResource)
}

const doc = await this.application.updateBundle(appid, dto, isTrialTier)

// restart running application if cpu or memory changed
Expand Down
4 changes: 4 additions & 0 deletions server/src/application/application.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { BundleService } from './bundle.service'
import { ResourceService } from 'src/billing/resource.service'
import { FunctionRecycleBinService } from 'src/recycle-bin/cloud-function/function-recycle-bin.service'
import { HttpModule } from '@nestjs/axios'
import { QuotaService } from 'src/user/quota.service'
import { SettingService } from 'src/setting/setting.service'

@Module({
imports: [
Expand All @@ -41,6 +43,8 @@ import { HttpModule } from '@nestjs/axios'
WebsiteService,
BundleService,
ResourceService,
QuotaService,
SettingService,
],
exports: [
ApplicationService,
Expand Down
33 changes: 30 additions & 3 deletions server/src/database/database.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,20 @@ import { unlink, writeFile } from 'node:fs/promises'
import * as os from 'os'
import { ResponseUtil } from 'src/utils/response'
import { ImportDatabaseDto } from './dto/import-database.dto'
import { InjectUser } from 'src/utils/decorator'
import { User } from 'src/user/entities/user'
import { QuotaService } from 'src/user/quota.service'

@ApiTags('Database')
@ApiBearerAuth('Authorization')
@Controller('apps/:appid/databases')
export class DatabaseController {
private readonly logger = new Logger(DatabaseController.name)

constructor(private readonly dbService: DatabaseService) {}
constructor(
private readonly dbService: DatabaseService,
private readonly quotaService: QuotaService,
) {}

/**
* The database proxy for database management
Expand Down Expand Up @@ -91,7 +97,15 @@ export class DatabaseController {
async exportDatabase(
@Param('appid') appid: string,
@Res({ passthrough: true }) res: IResponse,
@InjectUser() user: User,
) {
// Check if user data import and export is out of limits
const databaseSyncLimit = await this.quotaService.databaseSyncLimit(
user._id,
)
if (databaseSyncLimit) {
return ResponseUtil.error('Database sync limit exceeded')
}
const tempFilePath = path.join(
os.tmpdir(),
'mongodb-data',
Expand All @@ -104,7 +118,7 @@ export class DatabaseController {
mkdirSync(path.dirname(tempFilePath), { recursive: true })
}

await this.dbService.exportDatabase(appid, tempFilePath)
await this.dbService.exportDatabase(appid, tempFilePath, user._id)
const filename = path.basename(tempFilePath)

res.set({
Expand Down Expand Up @@ -132,7 +146,15 @@ export class DatabaseController {
@UploadedFile() file: Express.Multer.File,
@Body('sourceAppid') sourceAppid: string,
@Param('appid') appid: string,
@InjectUser() user: User,
) {
// Check if user data import and export is out of limits
const databaseSyncLimit = await this.quotaService.databaseSyncLimit(
user._id,
)
if (databaseSyncLimit) {
return ResponseUtil.error('Database sync limit exceeded')
}
// check if db is valid
if (!/^[a-z0-9]{6}$/.test(sourceAppid)) {
return ResponseUtil.error('Invalid source appid')
Expand All @@ -156,7 +178,12 @@ export class DatabaseController {

try {
await writeFile(tempFilePath, file.buffer)
await this.dbService.importDatabase(appid, sourceAppid, tempFilePath)
await this.dbService.importDatabase(
appid,
sourceAppid,
tempFilePath,
user._id,
)
return ResponseUtil.ok({})
} finally {
if (existsSync(tempFilePath)) await unlink(tempFilePath)
Expand Down
4 changes: 4 additions & 0 deletions server/src/database/database.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { ApplicationService } from 'src/application/application.service'
import { BundleService } from 'src/application/bundle.service'
import { DatabaseUsageLimitTaskService } from './database-usage-limit-task.service'
import { DatabaseUsageCaptureTaskService } from './database-usage-capture-task.service'
import { QuotaService } from 'src/user/quota.service'
import { SettingService } from 'src/setting/setting.service'

@Module({
imports: [],
Expand All @@ -31,6 +33,8 @@ import { DatabaseUsageCaptureTaskService } from './database-usage-capture-task.s
BundleService,
DatabaseUsageCaptureTaskService,
DatabaseUsageLimitTaskService,
SettingService,
QuotaService,
],
exports: [
CollectionService,
Expand Down
11 changes: 10 additions & 1 deletion server/src/database/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
} from './entities/database'
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import { DatabaseSyncRecord } from './entities/database-sync-record'
import { ObjectId } from 'mongodb'

const p_exec = promisify(exec)

Expand Down Expand Up @@ -211,7 +213,7 @@ export class DatabaseService {
}
}

async exportDatabase(appid: string, filePath: string) {
async exportDatabase(appid: string, filePath: string, uid: ObjectId) {
const region = await this.regionService.findByAppId(appid)
const database = await this.findOne(appid)
assert(database, 'Database not found')
Expand All @@ -223,6 +225,9 @@ export class DatabaseService {
await p_exec(
`mongodump --uri='${connectionUri}' --gzip --archive=${filePath}`,
)
await this.db
.collection<DatabaseSyncRecord>('DatabaseSyncRecord')
.insertOne({ uid, createdAt: new Date() })
} catch (error) {
this.logger.error(`failed to export db ${appid}`, error)
throw error
Expand All @@ -233,6 +238,7 @@ export class DatabaseService {
appid: string,
dbName: string,
filePath: string,
uid: ObjectId,
): Promise<void> {
const region = await this.regionService.findByAppId(appid)
const database = await this.findOne(appid)
Expand All @@ -245,6 +251,9 @@ export class DatabaseService {
await p_exec(
`mongorestore --uri='${connectionUri}' --gzip --archive='${filePath}' --nsFrom="${dbName}.*" --nsTo="${appid}.*" -v --nsInclude="${dbName}.*"`,
)
await this.db
.collection<DatabaseSyncRecord>('DatabaseSyncRecord')
.insertOne({ uid, createdAt: new Date() })
} catch (error) {
console.error(`failed to import db to ${appid}:`, error)
throw error
Expand Down
6 changes: 6 additions & 0 deletions server/src/database/entities/database-sync-record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ObjectId } from 'mongodb'

export class DatabaseSyncRecord {
uid: ObjectId
createdAt: Date
}
51 changes: 51 additions & 0 deletions server/src/initializer/initializer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
AuthProvider,
AuthProviderState,
} from 'src/authentication/entities/auth-provider'
import { Setting } from 'src/setting/entities/setting'

@Injectable()
export class InitializerService {
Expand All @@ -24,6 +25,7 @@ export class InitializerService {
await this.createDefaultAuthProvider()
await this.createDefaultResourceOptions()
await this.createDefaultResourceBundles()
await this.createDefaultSettings()
}

async createDefaultRegion() {
Expand Down Expand Up @@ -331,4 +333,53 @@ export class InitializerService {

this.logger.verbose('Created default resource templates')
}

// create default settings
async createDefaultSettings() {
// check if exists
const existed = await this.db
.collection<Setting>('Setting')
.countDocuments()

if (existed) {
this.logger.debug('default settings already exists')
return
}

await this.db.collection<Setting>('Setting').insertOne({
public: false,
key: 'resource_limit',
value: 'default',
desc: 'resource limit of user',
metadata: {
limitOfCPU: 20000,
limitOfMemory: 20480,
limitCountOfApplication: 20,
limitOfDatabaseSyncCount: {
countLimit: 10,
timePeriodInSeconds: 86400,
},
},
})

await this.db.collection<Setting>('Setting').insertOne({
public: true,
key: 'invitation_profit',
value: '0',
desc: 'Set up invitation rebate',
})

await this.db.collection<Setting>('Setting').insertOne({
public: true,
key: 'id_verify',
value: 'off', // on | off
desc: 'real name authentication',
metadata: {
message: '',
authenticateSite: '',
},
})

this.logger.verbose('Created default settings')
}
}
11 changes: 11 additions & 0 deletions server/src/user/entities/user-quota.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'
import { ObjectId } from 'mongodb'

type LimitOfDatabaseSyncCount = {
countLimit: number
timePeriodInSeconds: number
}

export class UserQuota {
@ApiProperty({ type: String })
_id?: ObjectId
Expand All @@ -17,6 +22,12 @@ export class UserQuota {
@ApiProperty()
limitCountOfApplication: number

@ApiProperty({
description: 'Limits of database synchronization count and time period.',
type: { countLimit: Number, timePeriodInSeconds: Number },
})
limitOfDatabaseSyncCount: LimitOfDatabaseSyncCount

@ApiProperty()
createdAt: Date

Expand Down
Loading