Skip to content

Commit

Permalink
Merge pull request #164 from boostcampwm2023/feature-be-#140
Browse files Browse the repository at this point in the history
[BE]feat#140 ์ฑ„์ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ณธ ๋ชจ๋“ˆ์„ ๋งŒ๋“ ๋‹ค
  • Loading branch information
flydog98 authored Nov 28, 2023
2 parents b7a8b95 + 2d82aa3 commit 83bc962
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 61 deletions.
5 changes: 3 additions & 2 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"mongoose": "^8.0.1",
"nest-winston": "^1.9.4",
"papaparse": "^5.4.1",
Expand All @@ -46,7 +47,8 @@
"ssh2": "^1.14.0",
"typeorm": "^0.3.17",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1"
"winston-daily-rotate-file": "^4.7.1",
"jest": "^29.7.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
Expand All @@ -65,7 +67,6 @@
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"lint-staged": "^15.1.0",
"prettier": "^3.1.0",
"source-map-support": "^0.5.21",
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { format } from 'winston';
import { typeOrmConfig } from './configs/typeorm.config';
import { QuizzesModule } from './quizzes/quizzes.module';
import { LoggingInterceptor } from './common/logging.interceptor';
import { QuizWizardModule } from './quiz-wizard/quiz-wizard.module';

@Module({
imports: [
Expand Down Expand Up @@ -38,6 +39,7 @@ import { LoggingInterceptor } from './common/logging.interceptor';
),
),
}),
QuizWizardModule,
],
controllers: [AppController],
providers: [
Expand Down
58 changes: 58 additions & 0 deletions packages/backend/src/common/ssh.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Client } from 'ssh2';
import 'dotenv/config';

async function getSSH(
host: string,
port: number,
username: string,
password: string,
): Promise<Client> {
const conn = new Client();

await new Promise<void>((resolve, reject) => {
conn
.on('ready', () => resolve())
.on('error', reject)
.connect({
host,
port,
username,
password,
});
});

return conn;
}
export async function executeSSHCommand(
command: string,
): Promise<{ stdoutData: string; stderrData: string }> {
const conn: Client = await getSSH(
process.env.CONTAINER_SSH_HOST,
Number(process.env.CONTAINER_SSH_PORT),
process.env.CONTAINER_SSH_USERNAME,
process.env.CONTAINER_SSH_PASSWORD,
);

return new Promise((resolve, reject) => {
conn.exec(command, (err, stream) => {
if (err) {
reject(new Error('SSH command execution Server error'));
return;
}
let stdoutData = '';
let stderrData = '';
stream
.on('close', () => {
conn.end();
resolve({ stdoutData, stderrData });
})
.on('data', (chunk) => {
stdoutData += chunk;
});

stream.stderr.on('data', (chunk) => {
stderrData += chunk;
});
});
});
}
65 changes: 9 additions & 56 deletions packages/backend/src/containers/containers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Logger } from 'winston';
import { CommandResponseDto } from 'src/quizzes/dto/command-response.dto';
import { Client } from 'ssh2';
import shellEscape from 'shell-escape';
import { executeSSHCommand } from '../common/ssh.util';

@Injectable()
export class ContainersService {
Expand All @@ -12,58 +12,11 @@ export class ContainersService {
@Inject('winston') private readonly logger: Logger,
) {}

private async getSSH(): Promise<Client> {
const conn = new Client();

await new Promise<void>((resolve, reject) => {
conn
.on('ready', () => resolve())
.on('error', reject)
.connect({
host: this.configService.get<string>('CONTAINER_SSH_HOST'),
port: this.configService.get<number>('CONTAINER_SSH_PORT'),
username: this.configService.get<string>('CONTAINER_SSH_USERNAME'),
password: this.configService.get<string>('CONTAINER_SSH_PASSWORD'),
});
});

return conn;
}

private async executeSSHCommand(
command: string,
): Promise<{ stdoutData: string; stderrData: string }> {
const conn: Client = await this.getSSH();

return new Promise((resolve, reject) => {
conn.exec(command, (err, stream) => {
if (err) {
reject(new Error('SSH command execution Server error'));
return;
}
let stdoutData = '';
let stderrData = '';
stream
.on('close', () => {
conn.end();
resolve({ stdoutData, stderrData });
})
.on('data', (chunk) => {
stdoutData += chunk;
});

stream.stderr.on('data', (chunk) => {
stderrData += chunk;
});
});
});
}

async runGitCommand(
container: string,
command: string,
): Promise<CommandResponseDto> {
const { stdoutData, stderrData } = await this.executeSSHCommand(
const { stdoutData, stderrData } = await executeSSHCommand(
`docker exec -w /home/quizzer/quiz/ -u quizzer ${container} /usr/local/bin/restricted-shell ${command}`,
);

Expand All @@ -88,7 +41,7 @@ export class ContainersService {
): Promise<CommandResponseDto> {
const escapedMessage = shellEscape([message]);

const { stdoutData, stderrData } = await this.executeSSHCommand(
const { stdoutData, stderrData } = await executeSSHCommand(
`docker exec -w /home/quizzer/quiz/ -u quizzer ${container} sh -c "git config --global core.editor /editor/input.sh && echo ${escapedMessage} | ${command}; git config --global core.editor /editor/output.sh"`,
);

Expand All @@ -110,26 +63,26 @@ export class ContainersService {

const createContainerCommand = `docker run -itd --network none -v ~/editor:/editor \
mergemasters/alpine-git:0.2 /bin/sh`;
const { stdoutData } = await this.executeSSHCommand(createContainerCommand);
const { stdoutData } = await executeSSHCommand(createContainerCommand);
const containerId = stdoutData.trim();

// TODO: ์—ฐ์† ์‹คํ–‰ํ•  ๋•Œ ๋งค๋ฒˆ SSH ํ•˜๋Š”๊ฑฐ ๋ฆฌํŒฉํ† ๋ง ํ•ด์•ผ ํ•จ
const copyFilesCommand = `docker cp ~/quizzes/${quizId}/. ${containerId}:/home/${user}/quiz/`;
await this.executeSSHCommand(copyFilesCommand);
await executeSSHCommand(copyFilesCommand);

const chownCommand = `docker exec -u root ${containerId} chown -R ${user}:${user} /home/${user}`;
await this.executeSSHCommand(chownCommand);
await executeSSHCommand(chownCommand);

const coreEditorCommand = `docker exec -w /home/quizzer/quiz/ -u ${user} ${containerId} git config --global core.editor /editor/output.sh`;
await this.executeSSHCommand(coreEditorCommand);
await executeSSHCommand(coreEditorCommand);

return containerId;
}

async isValidateContainerId(containerId: string): Promise<boolean> {
const command = `docker ps -a --filter "id=${containerId}" --format "{{.ID}}"`;

const { stdoutData, stderrData } = await this.executeSSHCommand(command);
const { stdoutData, stderrData } = await executeSSHCommand(command);

if (stderrData) {
// ๋„์ปค ๋ฏธ์„ค์น˜ ๋“ฑ์˜ ์—๋Ÿฌ์ผ ๋“ฏ
Expand All @@ -142,7 +95,7 @@ mergemasters/alpine-git:0.2 /bin/sh`;
async deleteContainer(containerId: string): Promise<void> {
const command = `docker rm -f ${containerId}`;

const { stdoutData, stderrData } = await this.executeSSHCommand(command);
const { stdoutData, stderrData } = await executeSSHCommand(command);

console.log(`container deleted : ${stdoutData}`);

Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/quiz-wizard/quiz-wizard.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { QuizWizardService } from './quiz-wizard.service';

@Module({
providers: [QuizWizardService],
exports: [QuizWizardService],
})
export class QuizWizardModule {}
18 changes: 18 additions & 0 deletions packages/backend/src/quiz-wizard/quiz-wizard.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { QuizWizardService } from './quiz-wizard.service';

describe('QuizWizardService', () => {
let service: QuizWizardService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [QuizWizardService],
}).compile();

service = module.get<QuizWizardService>(QuizWizardService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
36 changes: 36 additions & 0 deletions packages/backend/src/quiz-wizard/quiz-wizard.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import { exec } from 'child_process';

@Injectable()
export class QuizWizardService {
async submit(containerId, quizId) {
const execAsync = promisify(exec);

console.log('ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ ์œ„์น˜:', process.cwd());
try {
await execAsync(
`yarn run jest ${path.resolve(
process.cwd(),
'src',
'quiz-wizard',
'tests',
`${quizId}.spec.ts`,
)} ${containerId}`,
);
return true;
} catch (e) {
return false;
}
}
checkDirectoryExists(directoryPath) {
const doesExist = fs.existsSync(directoryPath);
describe('Directory Existence Check', () => {
it('should check if a directory exists', () => {
expect(doesExist).toBe(true);
});
});
}
}
23 changes: 23 additions & 0 deletions packages/backend/src/quiz-wizard/tests/test.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { executeSSHCommand } from '../../common/ssh.util';

export async function isDirectoryExist(
container: string,
path: string,
): Promise<boolean> {
const { stdoutData } = await executeSSHCommand(
`docker exec -w /home/quizzer/quiz/ -u quizzer ${container} /usr/local/bin/sh "ls -l ${path} | grep ^d"`,
);

return stdoutData !== '';
}

export async function getConfig(
container: string,
key: string,
): Promise<string> {
const { stdoutData } = await executeSSHCommand(
`docker exec -u quizzer -w /home/quizzer/quiz ${container} git -C /home/quizzer/quiz config user.${key}`,
);

return stdoutData;
}
23 changes: 23 additions & 0 deletions packages/backend/src/quizzes/dto/submit.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsString } from 'class-validator';

export class Success {
@ApiProperty({ example: true })
@IsBoolean()
solved = true;

@ApiProperty({ example: 'gitchallenge.com/api/v1/quizzes/shared?answer=โ€โ€' })
@IsString()
link: string;

constructor(link: string) {
this.link = link;
}
}

export class Fail {
@ApiProperty({ example: false })
solved = false;
}

export type SubmitDto = Success | Fail;
Loading

0 comments on commit 83bc962

Please sign in to comment.