mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat: service exec console
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>
This commit is contained in:
1 parent
6d237eccf7
commit
f357c7adee
7 files changed
+317
-4
No files matched your search
@@ -0,0 +1,79 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user