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.