mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
Overhaul: real metrics, Git integration UI, open-source hygiene, root cleanup
Backend: - GET /services/:id/metrics with real Docker container stats - DELETE /git/providers/:id and GET repo branches endpoint Frontend: - Service detail shows real telemetry or honest empty states - Live status distinguishes idle/polling/reconnecting - Git providers section in Settings; repo+branch pickers in Add Service - Real logo, compact Projects header, canvas fitView fix - index.css deduplicated (1725 -> 1384 lines) - Fixed smoke tests, lint errors, ToastProvider/context unification Repo hygiene: - Added LICENSE (MIT), CONTRIBUTING.md, SECURITY.md; rewrote README - Untracked .env.prod and data/letsencrypt secrets; fixed .gitignore - Removed legacy root src/internal/cmd and stale configs/docs/artifacts - Moved guides and design references under docs/ 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
355a97bab4
commit
9f3b196032
499 files changed
+4471
-94235
No files matched your search
@@ -0,0 +1,321 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"containr/internal/docker"
|
||||
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type upgradeStatusResponse struct {
|
||||
ImageRef string `json:"image_ref"`
|
||||
Registry string `json:"registry"`
|
||||
DockerAvailable bool `json:"docker_available"`
|
||||
Installed bool `json:"installed"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
AuthConfigured bool `json:"auth_configured"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type hostMonitoringResponse struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OS string `json:"os"`
|
||||
Architecture string `json:"architecture"`
|
||||
CPU hostCPU `json:"cpu"`
|
||||
Memory hostMemory `json:"memory"`
|
||||
Storage hostStorage `json:"storage"`
|
||||
Load SystemLoad `json:"load"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
DockerAvailable bool `json:"docker_available"`
|
||||
Docker map[string]any `json:"docker,omitempty"`
|
||||
CollectedAt string `json:"collected_at"`
|
||||
}
|
||||
|
||||
type hostCPU struct {
|
||||
Cores int `json:"cores"`
|
||||
}
|
||||
|
||||
type hostMemory struct {
|
||||
Total uint64 `json:"total"`
|
||||
Used uint64 `json:"used"`
|
||||
Available uint64 `json:"available"`
|
||||
UsagePct float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
type hostStorage struct {
|
||||
Path string `json:"path"`
|
||||
Total uint64 `json:"total"`
|
||||
Used uint64 `json:"used"`
|
||||
Available uint64 `json:"available"`
|
||||
UsagePct float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
func handleGetUpgradeStatus(c *gin.Context) {
|
||||
imageRef := configuredUpgradeImageRef()
|
||||
dockerClient, _ := c.Get("docker_client")
|
||||
client, _ := dockerClient.(*docker.Client)
|
||||
|
||||
response := upgradeStatusResponse{
|
||||
ImageRef: imageRef,
|
||||
Registry: registryHost(imageRef),
|
||||
DockerAvailable: client != nil,
|
||||
AuthConfigured: upgradeRegistryAuthConfigured(),
|
||||
Message: "Upgrade image auto-detected",
|
||||
}
|
||||
|
||||
if imageRef == "" {
|
||||
response.Message = "Set CONTAINR_UPGRADE_IMAGE_REF or CONTAINR_IMAGE_REF to enable image pulls"
|
||||
c.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
response.Message = "Docker client unavailable"
|
||||
c.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
info, err := client.GetImageInfo(ctx, imageRef)
|
||||
if err != nil {
|
||||
response.Message = "Image not installed locally"
|
||||
c.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
response.Installed = true
|
||||
response.Digest = info.Digest
|
||||
response.Size = info.Size
|
||||
response.Message = "Image installed locally"
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func handlePullUpgradeImage(c *gin.Context) {
|
||||
imageRef := configuredUpgradeImageRef()
|
||||
if imageRef == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Set CONTAINR_UPGRADE_IMAGE_REF or CONTAINR_IMAGE_REF before pulling"})
|
||||
return
|
||||
}
|
||||
|
||||
dockerClient, _ := c.Get("docker_client")
|
||||
client, ok := dockerClient.(*docker.Client)
|
||||
if !ok || client == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Docker client unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
reader, err := client.PullImage(ctx, imageRef, upgradeRegistryAuth())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
_, _ = io.Copy(io.Discard, reader)
|
||||
|
||||
info, infoErr := client.GetImageInfo(ctx, imageRef)
|
||||
response := gin.H{
|
||||
"image_ref": imageRef,
|
||||
"registry": registryHost(imageRef),
|
||||
"message": "Image pull completed",
|
||||
}
|
||||
if infoErr == nil {
|
||||
response["digest"] = info.Digest
|
||||
response["size"] = info.Size
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func handleGetHostMonitoring(c *gin.Context) {
|
||||
hostname, _ := os.Hostname()
|
||||
memory := readHostMemory()
|
||||
storage := readHostStorage("/")
|
||||
load := readSystemLoad()
|
||||
uptime := readSystemUptime()
|
||||
|
||||
response := hostMonitoringResponse{
|
||||
Hostname: hostname,
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
CPU: hostCPU{Cores: runtime.NumCPU()},
|
||||
Memory: memory,
|
||||
Storage: storage,
|
||||
Load: load,
|
||||
UptimeSeconds: uptime,
|
||||
CollectedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if dockerClient, ok := c.Get("docker_client"); ok {
|
||||
if client, ok := dockerClient.(*docker.Client); ok && client != nil {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
if info, err := client.GetSystemInfo(ctx); err == nil {
|
||||
response.DockerAvailable = true
|
||||
response.Docker = map[string]any{
|
||||
"containers": info.Containers,
|
||||
"images": info.Images,
|
||||
"driver": info.Driver,
|
||||
"server": info.ServerVersion,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func configuredUpgradeImageRef() string {
|
||||
for _, key := range []string{"CONTAINR_UPGRADE_IMAGE_REF", "CONTAINR_IMAGE_REF", "DOCKER_IMAGE_REF"} {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func registryHost(imageRef string) string {
|
||||
parts := strings.Split(imageRef, "/")
|
||||
if len(parts) > 1 && (strings.Contains(parts[0], ".") || strings.Contains(parts[0], ":")) {
|
||||
return parts[0]
|
||||
}
|
||||
if strings.HasPrefix(imageRef, "ghcr.io/") {
|
||||
return "ghcr.io"
|
||||
}
|
||||
return "docker.io"
|
||||
}
|
||||
|
||||
func upgradeRegistryAuthConfigured() bool {
|
||||
auth := upgradeRegistryAuth()
|
||||
return strings.TrimSpace(auth.Username) != "" || strings.TrimSpace(auth.Password) != "" || strings.TrimSpace(auth.Auth) != ""
|
||||
}
|
||||
|
||||
func upgradeRegistryAuth() registry.AuthConfig {
|
||||
username := firstConfigured("GITHUB_CONTAINER_REGISTRY_USER", "GHCR_USERNAME", "DOCKER_REGISTRY_USERNAME")
|
||||
password := firstConfigured("GITHUB_CONTAINER_REGISTRY_TOKEN", "GHCR_TOKEN", "DOCKER_REGISTRY_PASSWORD")
|
||||
return registry.AuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func firstConfigured(keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func readHostMemory() hostMemory {
|
||||
values := map[string]uint64{}
|
||||
file, err := os.Open("/proc/meminfo")
|
||||
if err != nil {
|
||||
return hostMemory{}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSuffix(fields[0], ":")
|
||||
value, parseErr := strconv.ParseUint(fields[1], 10, 64)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
values[key] = value * 1024
|
||||
}
|
||||
|
||||
total := values["MemTotal"]
|
||||
available := values["MemAvailable"]
|
||||
used := uint64(0)
|
||||
if total > available {
|
||||
used = total - available
|
||||
}
|
||||
|
||||
usage := 0.0
|
||||
if total > 0 {
|
||||
usage = float64(used) / float64(total) * 100
|
||||
}
|
||||
|
||||
return hostMemory{
|
||||
Total: total,
|
||||
Used: used,
|
||||
Available: available,
|
||||
UsagePct: usage,
|
||||
}
|
||||
}
|
||||
|
||||
func readHostStorage(path string) hostStorage {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return hostStorage{Path: path}
|
||||
}
|
||||
|
||||
total := stat.Blocks * uint64(stat.Bsize)
|
||||
available := stat.Bavail * uint64(stat.Bsize)
|
||||
used := total - available
|
||||
usage := 0.0
|
||||
if total > 0 {
|
||||
usage = float64(used) / float64(total) * 100
|
||||
}
|
||||
|
||||
return hostStorage{
|
||||
Path: path,
|
||||
Total: total,
|
||||
Used: used,
|
||||
Available: available,
|
||||
UsagePct: usage,
|
||||
}
|
||||
}
|
||||
|
||||
func readSystemLoad() SystemLoad {
|
||||
data, err := os.ReadFile("/proc/loadavg")
|
||||
if err != nil {
|
||||
return SystemLoad{}
|
||||
}
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 3 {
|
||||
return SystemLoad{}
|
||||
}
|
||||
load1, _ := strconv.ParseFloat(fields[0], 64)
|
||||
load5, _ := strconv.ParseFloat(fields[1], 64)
|
||||
load15, _ := strconv.ParseFloat(fields[2], 64)
|
||||
return SystemLoad{Load1M: load1, Load5M: load5, Load15M: load15}
|
||||
}
|
||||
|
||||
func readSystemUptime() int64 {
|
||||
data, err := os.ReadFile("/proc/uptime")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) == 0 {
|
||||
return 0
|
||||
}
|
||||
uptime, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int64(uptime)
|
||||
}
|
||||
Reference in new issue
Block a user