mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
POST /services/:id/exec runs a one-off sh -c command inside the service's container via ExecRun (30s ceiling, 64KB output cap), with the standard service→project ownership check. Console section on the service detail page submits commands and renders captured output with exit codes. Verified live: real exec output, stderr + exit code propagation, 404 on foreign service, 401 unauthenticated, 400 on empty command. Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
80 lines
1.9 KiB
Go
80 lines
1.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"containr/internal/database"
|
|
"containr/internal/docker"
|
|
)
|
|
|
|
// handleExecInService runs a one-off shell command inside the service's
|
|
// running container and returns captured output. Intended for debugging;
|
|
// commands run as the container's default user with a 30s ceiling.
|
|
func handleExecInService(c *gin.Context) {
|
|
serviceID := c.Param("id")
|
|
userID := c.MustGet("user_id").(string)
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
var req struct {
|
|
Command string `json:"command" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
command := strings.TrimSpace(req.Command)
|
|
if command == "" || len(command) > 4096 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "command must be 1-4096 chars"})
|
|
return
|
|
}
|
|
|
|
var ownerID string
|
|
err := db.QueryRow(
|
|
`SELECT p.owner_id FROM services s
|
|
JOIN projects p ON p.id = s.project_id
|
|
WHERE s.id = $1`,
|
|
serviceID,
|
|
).Scan(&ownerID)
|
|
if err != nil || ownerID != userID {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
|
return
|
|
}
|
|
|
|
dockerClient, _ := c.Get("docker_client")
|
|
dockerCli, _ := dockerClient.(*docker.Client)
|
|
if dockerCli == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Docker unavailable"})
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
containerID, err := findServiceContainer(ctx, dockerCli, serviceID)
|
|
if err != nil {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "No running container for this service"})
|
|
return
|
|
}
|
|
|
|
output, exitCode, runErr := dockerCli.ExecRun(ctx, containerID, []string{"sh", "-c", command})
|
|
if len(output) > 64*1024 {
|
|
output = output[:64*1024]
|
|
}
|
|
|
|
errText := ""
|
|
if runErr != nil {
|
|
errText = runErr.Error()
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"output": output,
|
|
"exit_code": exitCode,
|
|
"error": errText,
|
|
})
|
|
}
|