mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-07 22:24:42 +08:00
first commit
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"clicd/internal/config"
|
||||
"clicd/internal/lxc"
|
||||
)
|
||||
|
||||
var lxcManager = lxc.NewManager()
|
||||
|
||||
// HandleContainers handles container list and creation
|
||||
func HandleContainers(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
listContainers(w, r)
|
||||
case http.MethodPost:
|
||||
createContainer(w, r)
|
||||
default:
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSingleContainer handles individual container operations by ID or name: /api/containers/{id-or-name}/...
|
||||
func HandleSingleContainer(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/containers/")
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
c := containerByIdentifier(parts[0])
|
||||
if c == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
id := c.ID
|
||||
action := ""
|
||||
if len(parts) > 1 {
|
||||
action = parts[1]
|
||||
}
|
||||
|
||||
switch {
|
||||
case action == "start" && r.Method == http.MethodPost:
|
||||
HandleSingleTaskAction(w, r, id, "start")
|
||||
case action == "stop" && r.Method == http.MethodPost:
|
||||
HandleSingleTaskAction(w, r, id, "stop")
|
||||
case action == "restart" && r.Method == http.MethodPost:
|
||||
HandleSingleTaskAction(w, r, id, "restart")
|
||||
case action == "reinstall" && r.Method == http.MethodPost:
|
||||
HandleSingleTaskAction(w, r, id, "reinstall")
|
||||
case action == "delete" && r.Method == http.MethodDelete:
|
||||
HandleSingleTaskAction(w, r, id, "delete")
|
||||
case action == "reset-password" && r.Method == http.MethodPost:
|
||||
resetSSHPassword(w, r, id)
|
||||
case action == "usage" && r.Method == http.MethodGet:
|
||||
getUsage(w, r, id)
|
||||
case action == "traffic" && r.Method == http.MethodGet:
|
||||
getTraffic(w, r, id)
|
||||
case action == "traffic-reset" && r.Method == http.MethodPost:
|
||||
resetTraffic(w, r, id)
|
||||
case action == "traffic-limit" && r.Method == http.MethodPut:
|
||||
updateTrafficLimit(w, r, id)
|
||||
case action == "resource-limit" && r.Method == http.MethodPut:
|
||||
updateResourceLimit(w, r, id)
|
||||
case action == "random-port" && r.Method == http.MethodGet:
|
||||
getRandomPort(w, r, id)
|
||||
case action == "expiry" && r.Method == http.MethodPut:
|
||||
updateExpiry(w, r, id)
|
||||
case action == "ipv6" && r.Method == http.MethodPost:
|
||||
assignIPv6(w, r, id)
|
||||
case action == "port-mappings" && r.Method == http.MethodPost:
|
||||
addPortMapping(w, r, id)
|
||||
case strings.HasPrefix(action, "port-mappings/") && r.Method == http.MethodPut:
|
||||
updatePortMapping(w, r, id, strings.TrimPrefix(action, "port-mappings/"))
|
||||
case strings.HasPrefix(action, "port-mappings/") && r.Method == http.MethodDelete:
|
||||
deletePortMapping(w, r, id, strings.TrimPrefix(action, "port-mappings/"))
|
||||
case r.Method == http.MethodGet:
|
||||
getContainer(w, r, id)
|
||||
default:
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Action not found"})
|
||||
}
|
||||
}
|
||||
|
||||
func listContainers(w http.ResponseWriter, r *http.Request) {
|
||||
containers, err := lxcManager.ListContainers()
|
||||
if err != nil {
|
||||
containers = config.AppConfig.Containers
|
||||
}
|
||||
containers = filterContainersForRequest(r, containers)
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: containers})
|
||||
}
|
||||
|
||||
func createContainer(w http.ResponseWriter, r *http.Request) {
|
||||
var cfg lxc.ContainerConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
|
||||
return
|
||||
}
|
||||
if cfg.Name == "" {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Container name is required"})
|
||||
return
|
||||
}
|
||||
if cfg.TemplateID == "" {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Template is required"})
|
||||
return
|
||||
}
|
||||
if cfg.VCPU <= 0 {
|
||||
cfg.VCPU = 1
|
||||
}
|
||||
if cfg.RAMMB < 128 {
|
||||
cfg.RAMMB = 512
|
||||
}
|
||||
if cfg.DiskGB < 1 {
|
||||
cfg.DiskGB = 5
|
||||
}
|
||||
if cfg.PortMappingCount < 2 {
|
||||
cfg.PortMappingCount = 2
|
||||
}
|
||||
if cfg.PortMappingCount > 64 {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Port mapping count cannot exceed 64"})
|
||||
return
|
||||
}
|
||||
if err := validateContainerResourceRequest(cfg.VCPU, cfg.RAMMB, cfg.DiskGB); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if cfg.ExpiresAt != "" {
|
||||
expiresAt, ok := lxc.ParseExpiration(cfg.ExpiresAt)
|
||||
if !ok {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid expiration date"})
|
||||
return
|
||||
}
|
||||
if !time.Now().Before(expiresAt) {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Expiration date must be in the future"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := lxcManager.CreateContainer(cfg); err != nil {
|
||||
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusCreated, APIResponse{Success: true, Message: "Container created successfully"})
|
||||
}
|
||||
|
||||
func getContainer(w http.ResponseWriter, r *http.Request, id int) {
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: c})
|
||||
}
|
||||
|
||||
func getUsage(w http.ResponseWriter, r *http.Request, id int) {
|
||||
usage, err := lxcManager.GetResourceUsage(id)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: usage})
|
||||
}
|
||||
|
||||
func getTraffic(w http.ResponseWriter, r *http.Request, id int) {
|
||||
info := lxcManager.GetTrafficInfo(id)
|
||||
if info == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: info})
|
||||
}
|
||||
|
||||
func updateExpiry(w http.ResponseWriter, r *http.Request, id int) {
|
||||
var req struct {
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request"})
|
||||
return
|
||||
}
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
c.ExpiresAt = req.ExpiresAt
|
||||
config.SaveConfig()
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Expiry updated"})
|
||||
}
|
||||
|
||||
func resetTraffic(w http.ResponseWriter, r *http.Request, id int) {
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
c.TrafficUsedRX = 0
|
||||
c.TrafficUsedTX = 0
|
||||
c.TrafficResetDate = time.Now().Format("2006-01")
|
||||
config.SaveConfig()
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Traffic reset"})
|
||||
}
|
||||
|
||||
func updateTrafficLimit(w http.ResponseWriter, r *http.Request, id int) {
|
||||
var req struct {
|
||||
Mode string `json:"traffic_mode"`
|
||||
MonthlyGB int `json:"monthly_traffic_gb"`
|
||||
TrafficInGB int `json:"traffic_in_gb"`
|
||||
TrafficOutGB int `json:"traffic_out_gb"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request"})
|
||||
return
|
||||
}
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
c.TrafficMode = req.Mode
|
||||
c.MonthlyTrafficGB = req.MonthlyGB
|
||||
c.TrafficInGB = req.TrafficInGB
|
||||
c.TrafficOutGB = req.TrafficOutGB
|
||||
config.SaveConfig()
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Traffic limit updated"})
|
||||
}
|
||||
|
||||
func updateResourceLimit(w http.ResponseWriter, r *http.Request, id int) {
|
||||
var req struct {
|
||||
VCPU float64 `json:"vcpu"`
|
||||
RAMMB int `json:"ram_mb"`
|
||||
IOMBps int `json:"io_speed_mbps"`
|
||||
BWMbps int `json:"network_bw_mbps"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request"})
|
||||
return
|
||||
}
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update config
|
||||
nextVCPU := c.VCPU
|
||||
nextRAMMB := c.RAMMB
|
||||
if req.VCPU > 0 {
|
||||
nextVCPU = req.VCPU
|
||||
}
|
||||
if req.RAMMB > 0 {
|
||||
nextRAMMB = req.RAMMB
|
||||
}
|
||||
if err := validateContainerResourceRequest(nextVCPU, nextRAMMB, c.DiskGB); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.VCPU = nextVCPU
|
||||
c.RAMMB = nextRAMMB
|
||||
c.IOSpeedMBps = req.IOMBps
|
||||
c.NetworkBWMbps = req.BWMbps
|
||||
config.SaveConfig()
|
||||
|
||||
// Re-apply resource limits to running container
|
||||
if c.Status == "running" {
|
||||
if err := lxcManager.ApplyContainerLimits(c); err != nil {
|
||||
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Resource limits updated"})
|
||||
}
|
||||
|
||||
func getRandomPort(w http.ResponseWriter, r *http.Request, id int) {
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
// Find a random unused port between 10000-65535
|
||||
used := map[int]bool{}
|
||||
for _, pm := range c.PortMappings {
|
||||
used[pm.HostPort] = true
|
||||
}
|
||||
// Also check all containers
|
||||
for _, oc := range config.AppConfig.Containers {
|
||||
if oc.ID == id {
|
||||
continue
|
||||
}
|
||||
for _, pm := range oc.PortMappings {
|
||||
used[pm.HostPort] = true
|
||||
}
|
||||
}
|
||||
// Try random ports
|
||||
for tries := 0; tries < 100; tries++ {
|
||||
port := 10000 + (int(time.Now().UnixNano()) % 55535)
|
||||
if !used[port] {
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: map[string]int{"port": port}})
|
||||
return
|
||||
}
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: map[string]int{"port": 0}})
|
||||
}
|
||||
|
||||
// HandleTemplates returns available LXC templates
|
||||
func HandleTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
return
|
||||
}
|
||||
templates := lxc.GetTemplates()
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: templates})
|
||||
}
|
||||
|
||||
// HandleDashboard returns dashboard stats
|
||||
func HandleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
return
|
||||
}
|
||||
containers, err := lxcManager.ListContainers()
|
||||
if err != nil {
|
||||
containers = config.AppConfig.Containers
|
||||
}
|
||||
running := 0
|
||||
stopped := 0
|
||||
for _, c := range containers {
|
||||
if c.Status == "running" {
|
||||
running++
|
||||
} else {
|
||||
stopped++
|
||||
}
|
||||
}
|
||||
stats := map[string]interface{}{
|
||||
"total_containers": len(containers),
|
||||
"running": running,
|
||||
"stopped": stopped,
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: stats})
|
||||
}
|
||||
|
||||
// HandleHostInfo returns host machine resource info
|
||||
func HandleHostInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||
return
|
||||
}
|
||||
info := getHostInfo()
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: info})
|
||||
}
|
||||
|
||||
func resetSSHPassword(w http.ResponseWriter, r *http.Request, id int) {
|
||||
c := config.FindContainer(id)
|
||||
if c != nil && lxc.IsExpired(*c) {
|
||||
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "容器已到期,不允许此操作"})
|
||||
return
|
||||
}
|
||||
newPassword, err := lxcManager.ResetSSHPassword(id)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{
|
||||
Success: true,
|
||||
Message: "SSH password reset successfully",
|
||||
Data: map[string]string{"password": newPassword},
|
||||
})
|
||||
}
|
||||
|
||||
func addPortMapping(w http.ResponseWriter, r *http.Request, id int) {
|
||||
var pm config.PortMapping
|
||||
if err := json.NewDecoder(r.Body).Decode(&pm); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
|
||||
return
|
||||
}
|
||||
mappings, err := lxcManager.AddPortMapping(id, pm)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: mappings})
|
||||
}
|
||||
|
||||
func updatePortMapping(w http.ResponseWriter, r *http.Request, id int, indexStr string) {
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid port mapping index"})
|
||||
return
|
||||
}
|
||||
var pm config.PortMapping
|
||||
if err := json.NewDecoder(r.Body).Decode(&pm); err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
|
||||
return
|
||||
}
|
||||
if isSubUserRequest(r) {
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
||||
return
|
||||
}
|
||||
if index < 0 || index >= len(c.PortMappings) {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid port mapping index"})
|
||||
return
|
||||
}
|
||||
if pm.ContainerPort < 1 || pm.ContainerPort > 65535 {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "container port must be 1-65535"})
|
||||
return
|
||||
}
|
||||
existing := c.PortMappings[index]
|
||||
pm = config.PortMapping{
|
||||
ContainerPort: pm.ContainerPort,
|
||||
HostPort: existing.HostPort,
|
||||
Protocol: existing.Protocol,
|
||||
Description: existing.Description,
|
||||
}
|
||||
}
|
||||
mappings, err := lxcManager.UpdatePortMapping(id, index, pm)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: mappings})
|
||||
}
|
||||
|
||||
func deletePortMapping(w http.ResponseWriter, r *http.Request, id int, indexStr string) {
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid port mapping index"})
|
||||
return
|
||||
}
|
||||
mappings, err := lxcManager.DeletePortMapping(id, index)
|
||||
if err != nil {
|
||||
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: mappings})
|
||||
}
|
||||
Reference in New Issue
Block a user