mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
369 lines
11 KiB
Go
369 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"containr/internal/build"
|
|
"containr/internal/docker"
|
|
"containr/internal/types"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// BuildHandler handles build-related API endpoints
|
|
type BuildHandler struct {
|
|
buildManager *build.BuildManager
|
|
dockerClient *docker.Client
|
|
}
|
|
|
|
func (h *BuildHandler) buildUnavailable(c *gin.Context) bool {
|
|
if h.buildManager != nil {
|
|
return false
|
|
}
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"error": "Build service is unavailable: Docker client not initialized",
|
|
})
|
|
return true
|
|
}
|
|
|
|
// NewBuildHandler creates a new build handler
|
|
func NewBuildHandler(buildManager *build.BuildManager, dockerClient *docker.Client) *BuildHandler {
|
|
return &BuildHandler{
|
|
buildManager: buildManager,
|
|
dockerClient: dockerClient,
|
|
}
|
|
}
|
|
|
|
// BuildRequest represents the request body for starting a build
|
|
type BuildRequest struct {
|
|
BuildType string `json:"build_type" binding:"required"`
|
|
SourcePath string `json:"source_path"`
|
|
PrebuiltImage string `json:"prebuilt_image"`
|
|
ImageName string `json:"image_name" binding:"required"`
|
|
ImageTag string `json:"image_tag" binding:"required"`
|
|
RegistryURL string `json:"registry_url"`
|
|
BuildCommand string `json:"build_command"`
|
|
StartCommand string `json:"start_command"`
|
|
Environment map[string]string `json:"environment"`
|
|
BuildArgs map[string]string `json:"build_args"`
|
|
Labels map[string]string `json:"labels"`
|
|
ProjectID string `json:"project_id"`
|
|
ServiceID string `json:"service_id"`
|
|
Branch string `json:"branch"`
|
|
Commit string `json:"commit"`
|
|
}
|
|
|
|
// BuildResponse represents the response for a build operation
|
|
type BuildResponse struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
ImageName string `json:"image_name"`
|
|
ImageTag string `json:"image_tag"`
|
|
Size int64 `json:"size"`
|
|
Digest string `json:"digest"`
|
|
BuildTime time.Time `json:"build_time"`
|
|
Success bool `json:"success"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// BuildStatusResponse represents the response for build status
|
|
type BuildStatusResponse struct {
|
|
ID string `json:"id"`
|
|
ProjectID string `json:"project_id"`
|
|
ServiceID string `json:"service_id"`
|
|
Status string `json:"status"`
|
|
Progress int `json:"progress"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
ImageName string `json:"image_name"`
|
|
ImageTag string `json:"image_tag"`
|
|
Size int64 `json:"size"`
|
|
Error string `json:"error,omitempty"`
|
|
Log string `json:"log"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
// BuildListResponse represents the response for listing builds
|
|
type BuildListResponse struct {
|
|
Builds []BuildStatusResponse `json:"builds"`
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
Limit int `json:"limit"`
|
|
}
|
|
|
|
// StartBuild starts a new build
|
|
// @Summary Start a build
|
|
// @Description Starts a new build process for the given configuration
|
|
// @Tags builds
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body BuildRequest true "Build request"
|
|
// @Success 200 {object} BuildResponse
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/builds [post]
|
|
func (h *BuildHandler) StartBuild(c *gin.Context) {
|
|
if h.buildUnavailable(c) {
|
|
return
|
|
}
|
|
|
|
var req BuildRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Convert to internal build request
|
|
buildReq := &types.BuildRequest{
|
|
BuildType: req.BuildType,
|
|
SourcePath: req.SourcePath,
|
|
PrebuiltImage: req.PrebuiltImage,
|
|
ImageName: req.ImageName,
|
|
ImageTag: req.ImageTag,
|
|
RegistryURL: req.RegistryURL,
|
|
BuildCommand: req.BuildCommand,
|
|
StartCommand: req.StartCommand,
|
|
Environment: req.Environment,
|
|
BuildArgs: req.BuildArgs,
|
|
Labels: req.Labels,
|
|
ProjectID: req.ProjectID,
|
|
ServiceID: req.ServiceID,
|
|
TriggeredBy: "api",
|
|
Branch: req.Branch,
|
|
Commit: req.Commit,
|
|
}
|
|
|
|
// Validate build request
|
|
if err := h.buildManager.ValidateBuildRequest(c.Request.Context(), buildReq); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Start build (this would be async in production)
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
|
defer cancel()
|
|
|
|
_, err := h.buildManager.Build(ctx, buildReq)
|
|
if err != nil {
|
|
// Log error or update build status in database
|
|
return
|
|
}
|
|
}()
|
|
|
|
// For now, return a mock response
|
|
// In production, this would return the actual build ID and status
|
|
c.JSON(http.StatusOK, BuildResponse{
|
|
ID: "build-" + strconv.FormatInt(time.Now().Unix(), 10),
|
|
Status: "pending",
|
|
ImageName: req.ImageName,
|
|
ImageTag: req.ImageTag,
|
|
Success: true,
|
|
})
|
|
}
|
|
|
|
// GetBuildStatus gets the status of a build
|
|
// @Summary Get build status
|
|
// @Description Gets the current status of a build
|
|
// @Tags builds
|
|
// @Produce json
|
|
// @Param id path string true "Build ID"
|
|
// @Success 200 {object} BuildStatusResponse
|
|
// @Failure 404 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/builds/{id} [get]
|
|
func (h *BuildHandler) GetBuildStatus(c *gin.Context) {
|
|
buildID := c.Param("id")
|
|
|
|
// For now, return a mock response
|
|
// In production, this would query the database for the actual build status
|
|
c.JSON(http.StatusOK, BuildStatusResponse{
|
|
ID: buildID,
|
|
Status: "completed",
|
|
Progress: 100,
|
|
StartedAt: time.Now().Add(-10 * time.Minute),
|
|
ImageName: "example-app",
|
|
ImageTag: "latest",
|
|
Size: 1024 * 1024 * 100, // 100MB
|
|
})
|
|
}
|
|
|
|
// ListBuilds lists all builds
|
|
// @Summary List builds
|
|
// @Description Lists all builds with optional filtering
|
|
// @Tags builds
|
|
// @Produce json
|
|
// @Param project_id query string false "Filter by project ID"
|
|
// @Param service_id query string false "Filter by service ID"
|
|
// @Param status query string false "Filter by status"
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param limit query int false "Items per page" default(20)
|
|
// @Success 200 {object} BuildListResponse
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/builds [get]
|
|
func (h *BuildHandler) ListBuilds(c *gin.Context) {
|
|
projectID := c.Query("project_id")
|
|
serviceID := c.Query("service_id")
|
|
status := c.Query("status")
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
|
|
// For now, return mock data
|
|
// In production, this would query the database with filters
|
|
builds := []BuildStatusResponse{
|
|
{
|
|
ID: "build-1",
|
|
ProjectID: projectID,
|
|
ServiceID: serviceID,
|
|
Status: status,
|
|
Progress: 100,
|
|
StartedAt: time.Now().Add(-1 * time.Hour),
|
|
ImageName: "example-app",
|
|
ImageTag: "latest",
|
|
Size: 1024 * 1024 * 100,
|
|
},
|
|
}
|
|
|
|
c.JSON(http.StatusOK, BuildListResponse{
|
|
Builds: builds,
|
|
Total: len(builds),
|
|
Page: page,
|
|
Limit: limit,
|
|
})
|
|
}
|
|
|
|
// CancelBuild cancels a running build
|
|
// @Summary Cancel build
|
|
// @Description Cancels a running build
|
|
// @Tags builds
|
|
// @Produce json
|
|
// @Param id path string true "Build ID"
|
|
// @Success 200 {object} map[string]string
|
|
// @Failure 404 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/builds/{id}/cancel [post]
|
|
func (h *BuildHandler) CancelBuild(c *gin.Context) {
|
|
buildID := c.Param("id")
|
|
|
|
// For now, just return success
|
|
// In production, this would actually cancel the build process
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Build " + buildID + " cancelled",
|
|
})
|
|
}
|
|
|
|
// GetBuildLogs gets the logs for a build
|
|
// @Summary Get build logs
|
|
// @Description Gets the build logs for a specific build
|
|
// @Tags builds
|
|
// @Produce text
|
|
// @Param id path string true "Build ID"
|
|
// @Param follow query bool false "Follow logs" default(false)
|
|
// @Success 200 {string} string "Build logs"
|
|
// @Failure 404 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/builds/{id}/logs [get]
|
|
func (h *BuildHandler) GetBuildLogs(c *gin.Context) {
|
|
buildID := c.Param("id")
|
|
follow := c.DefaultQuery("follow", "false") == "true"
|
|
|
|
// For now, return mock logs
|
|
// In production, this would stream actual build logs
|
|
logs := "Build " + buildID + " started\n"
|
|
logs += "Detecting runtime...\n"
|
|
logs += "Runtime detected: node\n"
|
|
logs += "Building image...\n"
|
|
logs += "Build completed successfully\n"
|
|
|
|
if follow {
|
|
// In production, this would use Server-Sent Events to stream logs
|
|
c.Header("Content-Type", "text/plain")
|
|
c.String(http.StatusOK, logs)
|
|
} else {
|
|
c.Header("Content-Type", "text/plain")
|
|
c.String(http.StatusOK, logs)
|
|
}
|
|
}
|
|
|
|
// GetBuildPlan gets the build plan for a service
|
|
// @Summary Get build plan
|
|
// @Description Gets the build plan for a service without actually building
|
|
// @Tags builds
|
|
// @Produce json
|
|
// @Param request body BuildRequest true "Build request"
|
|
// @Success 200 {object} build.BuildPlan
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/builds/plan [post]
|
|
func (h *BuildHandler) GetBuildPlan(c *gin.Context) {
|
|
if h.buildUnavailable(c) {
|
|
return
|
|
}
|
|
|
|
var req BuildRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Convert to internal build request
|
|
buildReq := &types.BuildRequest{
|
|
BuildType: req.BuildType,
|
|
SourcePath: req.SourcePath,
|
|
PrebuiltImage: req.PrebuiltImage,
|
|
ImageName: req.ImageName,
|
|
ImageTag: req.ImageTag,
|
|
BuildCommand: req.BuildCommand,
|
|
StartCommand: req.StartCommand,
|
|
Environment: req.Environment,
|
|
BuildArgs: req.BuildArgs,
|
|
Labels: req.Labels,
|
|
}
|
|
|
|
// Get build plan
|
|
plan, err := h.buildManager.GetBuildPlan(c.Request.Context(), buildReq)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, plan)
|
|
}
|
|
|
|
// DetectBuildType detects the build type for a given repository
|
|
// @Summary Detect build type
|
|
// @Description Detects the build type based on repository contents
|
|
// @Tags builds
|
|
// @Produce json
|
|
// @Param source_path query string true "Source path"
|
|
// @Success 200 {object} map[string]string
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/builds/detect [get]
|
|
func (h *BuildHandler) DetectBuildType(c *gin.Context) {
|
|
if h.buildUnavailable(c) {
|
|
return
|
|
}
|
|
|
|
sourcePath := c.Query("source_path")
|
|
if sourcePath == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "source_path is required"})
|
|
return
|
|
}
|
|
|
|
buildType, err := h.buildManager.DetectBuildType(c.Request.Context(), sourcePath)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"build_type": string(buildType),
|
|
})
|
|
}
|