Essa semana decidi me aprofundar e conhecer mais sobre o funcionamento e a API Docker e resolvi criar um executor de código. Nesse post iremos iniciar o desenvolvimento de uma API que irá receber um repositorio git(mas não necessáriamente somente git) e iremos executar o código desse repositório de forma isolada em um container.
Para prosseguir com esse tutorial você precisar ter o NodeJS e Docker instalado.
Você precisar habilitar que o Docker receba requisições através da sua API.
Melhorias são bem-vindas, se quiser adicione seus comentários com sugestões de melhorias ou novas funcionalidades. O projeto final pode ser acessado abaixo.
thierrysantos / sandbox
Code executor in sandbox 🚀
O que é sandbox ?
O sandbox é um ambiente isolado que ira realizar a execução do código.
Dentre as diversas aplicabilidades irei citar algumas como:
- Aplicações que precisam executar um código não confiavel
- Aplicações que precisam limitar recursos como memória, cpu...
Com a melhoria do projeto irei desenvolver algumas das aplicabilidades citada acima e irei registrar o desenvolvimento em outros post's.
Setup inicial
Nesse passo iremos instalar as dependencias do projeto e configurar o compilador do typescript.
mkdir sandbox-tutorial
cd sandbox-tutorial
# Diretório que ficará o código
mkdir src
# Iniciando o projeto
yarn init -y
# ou
npm init -y
Configurando o compilador do Typescript
Iremos utilizar esse comando a baixo para iniciarmos nosso projeto Typescript.
npx tsc --init
Ele vai gerar um arquivo com nome tsconfig.json, nesse arquivo iremos modificar o rootDir e outDir do compilador
{
"outDir": "./dist" ,
"rootDir": "./src"
}
Instalando as dependencias
Iremos instalar as seguintes dependencias:
- express - Para criação da API
- fs-extra - Para manipulação de arquivos
- nodegit - Para acesso a repositórios git
- handlebars - Para criação de dockerfile
- uuid - Para geração de ID's
- axios - Para fazer requisições à API REST do Docker
- yup - Para criar validações
yarn add express fs-extra nodegit handlebars uuid axios yup
# ou
npm install express fs-extra nodegit handlebars uuid axios yup
E seus tipos
yarn add @types/express @types/fs-extra @types/nodegit @types/handlebars @types/uuid @types/axios @types/yup --dev
# ou
npm install @types/express @types/fs-extra @types/nodegit @types/handlebars @types/uuid @types/axios @types/yup --save-dev
Agora iremos instalar as dependencias de desenvolvimento:
- nodemon - Para fazer reiniciar a aplicação ao atualizarmos o código
- typescript - Para compilar nosso código Typescript para Javascript
- concurrently - Para executarmos comandos concorrentes
- dotenv - Para carregar nossas variáveis de ambiente
yarn add nodemon typescript concurrently dotenv --dev
# ou
npm install nodemon typescript concurrently dotenv --save-dev
No package.json iremos adicionar um script para a execução da aplicação:
{
"scripts": {
"dev": "concurrently \"tsc -w\" \"nodemon dist/index.js\"",
}
}
Interação com o Docker
O módulo de interação com o Docker vai ser reposável pela criação e gerencimento dos containers e imagens.
cd src
mkdir docker
# Iremos criar dois arquivos
# Camada de interação com o Docker
touch docker/docker.repository.ts
# Camada com as regras de negócios
touch docker/docker.service.ts
No docker.repository iremos mapear os seguintes endpoints(Você pode encontrar os enpoints disponívels na documentação do Docker):
- /containers - Gerenciamento dos containers
- /build - Build de uma imagem
- /images/prune - Remover imagens não utilizadas
Iremos criar uma pasta utils com um arquivo chamado axios e iremos configurar a baseURL:
mkdir utils
touch utils/axios.ts
E iremos adicionar o endereço da api do Docker(No meu caso é esse abaixo, mas você deve colocar o endereço que você configurou no docker.service):
import axios from 'axios';
const api = axios.create({ baseURL: 'http://localhost:5555/v1.40' });
export default api;
E nosso docker.repository ficará assim:
import fs from 'fs';
import axios from '../utils/axios';
import { IContainer, IContainerConfig } from './interfaces';
export default class DockerRepository {
async createContainer(data: Partial<IContainerConfig>): Promise<string> {
const response = await axios.post(`/containers/create`, { ...data });
return response.data.Id;
}
async getOneContainer(id: string): Promise<IContainer> {
const { data } = await axios.get(`/containers/${id}/json`);
return data;
}
async deleteContainer(
id: string,
removeVolumes = false,
force = false,
link = false
): Promise<void> {
await axios.delete(`/containers/${id}`, {
params: {
v: removeVolumes,
force,
link,
},
});
}
async startContainer(id: string): Promise<void> {
await axios.post(`/containers/${id}/start`);
}
async buildImage(
name: string,
dockerfileContext: string,
file: fs.ReadStream
): Promise<void> {
await axios({
method: 'POST',
url: '/build',
data: file,
params: {
dockerfile: dockerfileContext,
t: name,
},
headers: {
'Content-type': 'application/x-tar"',
},
});
}
async pruneImage(): Promise<void> {
await axios.post(`/images/prune`);
}
}
Agora iremos criar um arquivo que conterá algumas interfaces para tiparmos algumas entidades do Docker:
touch docker/interfaces.ts
export interface IContainerHostConfig {
CpuShares: number;
Memory: number;
AutoRemove: boolean;
Mounts: {
Target: string;
Source: string;
Type: 'bind' | 'volume' | 'tmpfs' | 'npipe';
ReadOnly: boolean;
}[];
}
export interface IContainerConfig {
ExposedPorts: Record<string, {}>;
Tty: false;
OpenStdin: false;
StdinOnce: false;
Env: string[];
Cmd: string[];
Image: string;
Volumes: Record<string, {}>;
WorkingDir: string;
Entrypoint: string | string[];
HostConfig: Partial<IContainerHostConfig>;
}
export interface IContainer {
Id: string;
Created: string;
State: {
Status: string;
Running: boolean;
Paused: false;
StartedAt: string;
FinishedAt: string;
};
Name: string;
config: Partial<IContainerConfig>;
}
E por fim o docker.service que vai disponibilizar todo o gerencimento dos containers para os demais módulos da aplicação:
import fs from 'fs';
import { IContainer, IContainerConfig } from './interfaces';
import DockerRepository from './docker.repository'
export default class DockerService {
constructor(private dockerRepository: DockerRepository) {}
async createContainer(data: Partial<IContainerConfig>): Promise<string> {
const containerId = await this.dockerRepository.createContainer(data);
return containerId;
}
async getOneContainer(id: string): Promise<IContainer> {
const container = await this.dockerRepository.getOneContainer(id);
return container;
}
async deleteContainer(id: string): Promise<void> {
await this.dockerRepository.deleteContainer(id);
}
async startContainer(id: string): Promise<void> {
await this.dockerRepository.startContainer(id);
}
async buildImage(
name: string,
dockerfileContext: string,
file: fs.ReadStream
): Promise<void> {
await this.dockerRepository.buildImage(name, dockerfileContext, file);
}
async pruneImage(): Promise<void> {
await this.dockerRepository.pruneImage();
}
}
Com isso finalizamos a interação com o Docker, nos próximos dias iremos desenvolver as outras camadas.
thierrysantos / sandbox
Code executor in sandbox 🚀
Sandbox
Code executor in sandbox
Summary 📝
- Motivation
- Architecture
- Prerequisites
- Installing
- Proof of concepts
- Next steps
- Built With
- Contributing
- License
Motivation 💝
The goal of this project is to permit execute code from a determined source(actualy only git pr's are open) and limit time of execution, cpu consumption and memory consumption.
Architecture 🔨
It is the initial architecture and we are basically doing:
- Downloading source code
- Creating a image
- Creating a container
- Starting a container
Here you can see the next steps of this project and possible modifications in architecture.
Getting Started 💻
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
Prerequisites
You need to have Docker and NodeJS already installed to follow the nexts steps.
Your Docker must be able to receive requests. Here you can see how to enable it.
Installing
A step by…
Top comments (2)
Ficou bom em
Show de bola!!