Pular para o conteúdo principal

victorstein.dev

79/100 Dias de Golang - Mini Servidor de CI/CD – Parte 3 - Implementação da API com Gin

Table of Contents

# Mini Servidor de CI/CD – Parte 3: Implementação da API com Gin

Na Parte 2, criamos o banco de dados e modelamos nossas entidades com GORM. Agora, vamos adicionar a camada de API que permitirá: Criar novos pipelines, listar pipelines existentes e executar um pipeline específico. Iremos utilizar o Gin. Teremos dois arquivos novos nessa etapa do projeto o internal/api/routes.go e o internal/api/handlers.go.

No arquivo routes.go, definimos as 3 rotas e quais os handlers de cada rota:

package api

import (
	"github.com/gin-gonic/gin"
)

func RegisterRoutes(r *gin.Engine) {
	r.GET("/pipelines", GetAllPipelines)
	r.POST("/pipelines", CreatePipeline)
	r.POST("/pipelines/run/:id", RunPipeline)
}

No handlers.go teremos as funções que serão executadas em cada rota.

A função CreatePipeline trata a criação de um novo pipeline via requisição POST. Ela recebe um JSON com os dados do pipeline e seus steps, valida e converte para a estrutura Go com ShouldBindJSON, e então salva no banco usando DB.Create. Se os dados estiverem incorretos ou ocorrer algum erro no momento de persistir, a função retorna um erro apropriado com status HTTP 400 ou 500. Em caso de sucesso, o novo pipeline é retornado com status 201 (Created).

func CreatePipeline(c *gin.Context) {
	var pipeline models.Pipeline
	if err := c.ShouldBindJSON(&pipeline); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Dados inválidos"})
		return
	}

	if err := db.DB.Create(&pipeline).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao salvar pipeline"})
		return
	}

	c.JSON(http.StatusCreated, pipeline)
}

A função GetAllPipelines é responsável por retornar todos os pipelines cadastrados no banco de dados

func GetAllPipelines(c *gin.Context) {
	var pipelines []models.Pipeline
	err := db.DB.Preload("Steps").Find(&pipelines).Error
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao buscar pipelines"})
		return
	}
	c.JSON(http.StatusOK, pipelines)
}

A função RunPipeline executa um pipeline específico, cujo ID é passado via parâmetro na URL. Primeiro, ela busca o pipeline e seus steps no banco de dados. Em seguida, cria um novo registro de execução com status “running”. Cada step é executado sequencialmente usando comandos shell (sh -c). Os logs de saída são capturados, e caso algum comando falhe, o status da execução é atualizado para “failed”, com os logs detalhados retornados na resposta. Se todos os comandos forem executados com sucesso, o status final da execução é “success”, e a resposta inclui os logs completos da execução com status HTTP 200.

func RunPipeline(c *gin.Context) {
	idStr := c.Param("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "ID inválido"})
		return
	}

	var pipeline models.Pipeline
	if err := db.DB.Preload("Steps").First(&pipeline, id).Error; err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Pipeline não encontrado"})
		return
	}

	execution := models.Execution{
		PipelineID: uint(id),
		Status:     "running",
		Log:        "",
	}
	db.DB.Create(&execution)

	var logOutput string
	for _, step := range pipeline.Steps {
		log.Printf("Executando: %s", step.Command)
		cmd := exec.Command("sh", "-c", step.Command)
		out, err := cmd.CombinedOutput()
		logOutput += string(out) + "\n"

		if err != nil {
			execution.Status = "failed"
			execution.Log = logOutput + "\n[ERRO] " + err.Error()
			db.DB.Save(&execution)
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro na execução", "log": execution.Log})
			return
		}
	}

	execution.Status = "success"
	execution.Log = logOutput
	db.DB.Save(&execution)

	c.JSON(http.StatusOK, gin.H{"message": "Pipeline executado com sucesso", "log": execution.Log})
}

Temos que atualizar o main.go para chamar o router:

package main

import (
	"log"

	"github.com/gin-gonic/gin"

	"cicd-server/internal/api"
	"cicd-server/internal/db"
)

func main() {

	db.InitDatabase()

	router := gin.Default()

	api.RegisterRoutes(router)

	router.GET("/status", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok"})
	})

	log.Println("Servidor CI/CD rodando na porta 8080...")
	err := router.Run(":8080")
	if err != nil {
		log.Fatalf("Erro ao iniciar servidor: %v", err)
	}
}

Comandos cURL para testar as rotas:

curl -X GET http://localhost:8080/pipelines
curl -X POST http://localhost:8080/pipelines \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Exemplo CI com repo lipgloss",
    "repo": "https://github.com/usuario/repositorio",
    "steps": [
      { "order": 1, "command": "echo Iniciando build" },
      { "order": 2, "command": "git clone https://github.com/charmbracelet/lipgloss" },
      { "order": 3, "command": "echo Fim do build" }
    ]
  }'
curl -X POST http://localhost:8080/pipelines/run/1

No próximo post faremos alguns testes para o repositório.