DEV Community

Thierry
Thierry

Posted on

Integração do NodeJS com a REST API do Docker #1 - Modulo de interação com o Docker

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.

GitHub logo 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
Enter fullscreen mode Exit fullscreen mode

Configurando o compilador do Typescript

Iremos utilizar esse comando a baixo para iniciarmos nosso projeto Typescript.

npx tsc --init 
Enter fullscreen mode Exit fullscreen mode

Ele vai gerar um arquivo com nome tsconfig.json, nesse arquivo iremos modificar o rootDir e outDir do compilador

{
    "outDir": "./dist" ,
    "rootDir": "./src" 
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

No package.json iremos adicionar um script para a execução da aplicação:

{
    "scripts": {
        "dev": "concurrently \"tsc -w\" \"nodemon dist/index.js\"",
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

No docker.repository iremos mapear os seguintes endpoints(Você pode encontrar os enpoints disponívels na documentação do Docker):

Iremos criar uma pasta utils com um arquivo chamado axios e iremos configurar a baseURL:

mkdir utils
touch utils/axios.ts
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora iremos criar um arquivo que conterá algumas interfaces para tiparmos algumas entidades do Docker:

touch docker/interfaces.ts
Enter fullscreen mode Exit fullscreen mode
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>;
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Com isso finalizamos a interação com o Docker, nos próximos dias iremos desenvolver as outras camadas.

GitHub logo thierrysantos / sandbox

Code executor in sandbox 🚀

Sandbox

Code executor in sandbox

Summary 📝

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.

Application 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)

Collapse
 
rammaciel profile image
rammaciel

Ficou bom em

Collapse
 
soosbr profile image
Soosbr

Show de bola!!