Integração do NodeJS com a REST API do Docker #1 - Modulo de interação com o Docker
Tags: docker, typescript, tutorial, javascript
Published: 2020-05-02 17:05:28

---
title: Integração do NodeJS com a REST API do Docker #1 - Modulo de interação com o Docker
published: true
description: Iremos desenvolver um executor de código sandbox utilizando o Docker
tags: #docker #typescript #tutorial #javascript
---

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](https://nodejs.org/en/download/) e [Docker](https://docs.docker.com/get-docker/) instalado.

Você precisar habilitar que o Docker receba [requisições](https://docs.docker.com/engine/install/linux-postinstall/#configuring-remote-access-with-systemd-unit-file) 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 thierrysantos/sandbox no-readme %}

## 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.

```bash
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.

```bash
npx tsc --init 
```

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

```json
{
    "outDir": "./dist" ,
    "rootDir": "./src" 
}
```

### Instalando as dependencias

Iremos instalar as seguintes dependencias:

- [express](https://www.npmjs.com/package/express) - Para criação da API
- [fs-extra](https://www.npmjs.com/package/fs-extra) - Para manipulação de arquivos
- [nodegit](https://www.npmjs.com/package/nodegit) - Para acesso a repositórios git
- [handlebars](https://www.npmjs.com/package/handlebars) - Para criação de dockerfile
- [uuid](https://www.npmjs.com/package/uuid) - Para geração de ID's
- [axios](https://www.npmjs.com/package/axios) - Para fazer requisições à API REST do Docker
- [yup](https://www.npmjs.com/package/yup) - Para criar validações

```bash
yarn add express fs-extra nodegit handlebars uuid axios yup
# ou 
npm install express fs-extra nodegit handlebars uuid axios yup
```

E seus tipos

```bash
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](https://www.npmjs.com/package/nodemon) - Para fazer reiniciar a aplicação ao atualizarmos o código
- [typescript](https://www.npmjs.com/package/typescript) - Para compilar nosso código Typescript para Javascript
- [concurrently](https://www.npmjs.com/package/concurrently) - Para executarmos comandos concorrentes
- [dotenv](https://www.npmjs.com/package/dotenv) - Para carregar nossas variáveis de ambiente

```bash
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:

```json
{
    "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.

```bash
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](https://docs.docker.com/engine/api/v1.40/) do Docker): 

- [/containers](https://docs.docker.com/engine/api/v1.40/#tag/Container) - Gerenciamento dos containers
- [/build](https://docs.docker.com/engine/api/v1.40/#operation/ImageBuild) - Build de uma imagem
- [/images/prune](https://docs.docker.com/engine/api/v1.40/#operation/ImagePrune) - 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](https://docs.docker.com/engine/install/linux-postinstall/#configuring-remote-access-with-systemd-unit-file) da api do Docker(No meu caso é esse abaixo, mas você deve colocar o endereço que você configurou no **docker.service**):

```typescript
import axios from 'axios';

const api = axios.create({ baseURL: 'http://localhost:5555/v1.40' });

export default api;
```

E nosso **docker.repository** ficará assim:

```typescript
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:

```bash
touch docker/interfaces.ts
```

```typescript
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:

```typescript
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.

{% github thierrysantos/sandbox %}