updated memory calculation and logo

This commit is contained in:
2026-02-14 12:41:08 +01:00
parent 6a7737ee1c
commit 44a7d0de2c
10 changed files with 365 additions and 47 deletions
+59 -20
View File
@@ -10,6 +10,7 @@ import (
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
"gopkg.in/yaml.v3"
)
@@ -28,18 +29,20 @@ type DeployRequest struct {
Values map[string]string `json:"values"` // env_var -> user-provided value
}
// DeployStack handles first-time deployment of an app:
// 1. Load metadata (.felhom.yml) to know what fields exist
// 2. Auto-generate secrets for secret fields (hidden from user)
// 3. Auto-fill domain from controller config
// 4. Validate all user-provided values (password, path, required fields)
// 5. Save app.yaml
// 6. Run docker compose up -d with env vars
// 7. Update in-memory stack state
func (m *Manager) DeployStack(req DeployRequest) error {
// DeployStack handles first-time deployment of an app.
// Returns a warning message (empty if none) and an error if deployment is blocked.
// 1. Check available memory against app requirements
// 2. Load metadata (.felhom.yml) to know what fields exist
// 3. Auto-generate secrets for secret fields (hidden from user)
// 4. Auto-fill domain from controller config
// 5. Validate all user-provided values (password, path, required fields)
// 6. Save app.yaml
// 7. Run docker compose up -d with env vars
// 8. Update in-memory stack state
func (m *Manager) DeployStack(req DeployRequest) (string, error) {
stack, ok := m.GetStack(req.StackName)
if !ok {
return fmt.Errorf("stack %q not found", req.StackName)
return "", fmt.Errorf("stack %q not found", req.StackName)
}
stackDir := filepath.Dir(stack.ComposePath)
@@ -48,7 +51,43 @@ func (m *Manager) DeployStack(req DeployRequest) error {
// Check if already deployed
existing := LoadAppConfig(stackDir)
if existing != nil && existing.Deployed {
return fmt.Errorf("stack %q is already deployed; use update instead", req.StackName)
return "", fmt.Errorf("stack %q is already deployed; use update instead", req.StackName)
}
// --- Memory validation ---
var deployWarning string
reservedMB := m.cfg.System.ReservedMemoryMB
totalMB, memErr := system.GetTotalMemoryMB()
if memErr != nil {
m.logger.Printf("[WARN] Cannot read system memory: %v — skipping memory check", memErr)
} else {
usableMB := totalMB - reservedMB
currentReqMB, currentLimitMB := m.CommittedMemory()
newReqMB := ParseMemoryMB(meta.Resources.MemRequest)
newLimitMB := ParseMemoryMB(meta.Resources.MemLimit)
m.logger.Printf("[INFO] Memory check: total=%dMB, reserved=%dMB, usable=%dMB, committed_req=%dMB, new_req=%dMB, remaining=%dMB",
totalMB, reservedMB, usableMB, currentReqMB, newReqMB, usableMB-currentReqMB-newReqMB)
// Hard block: requests exceed usable memory
if newReqMB > 0 && currentReqMB+newReqMB > usableMB {
return "", fmt.Errorf(
"Nincs elég memória az alkalmazás telepítéséhez. "+
"Szükséges: %d MB, Elérhető: %d MB "+
"(összesen: %d MB, ebből %d MB már foglalt, %d MB rendszer számára fenntartva)",
newReqMB,
usableMB-currentReqMB,
totalMB,
currentReqMB,
reservedMB,
)
}
// Soft warning: limits exceed total (overcommit)
if newLimitMB > 0 && currentLimitMB+newLimitMB > totalMB {
deployWarning = "Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát. " +
"Normál használat mellett ez nem okoz problémát."
}
}
// Debug: log received values (redact passwords/secrets)
@@ -77,7 +116,7 @@ func (m *Manager) DeployStack(req DeployRequest) error {
// Always auto-generate, user never sees these
generated, err := generateValue(field.Generate)
if err != nil {
return fmt.Errorf("generating %s: %w", field.EnvVar, err)
return "", fmt.Errorf("generating %s: %w", field.EnvVar, err)
}
value = generated
@@ -87,7 +126,7 @@ func (m *Manager) DeployStack(req DeployRequest) error {
if userVal, ok := req.Values[field.EnvVar]; ok && userVal != "" {
value = userVal
} else {
return fmt.Errorf("a(z) %q mező kitöltése kötelező — használja a Generálás gombot vagy írjon be egy jelszót", field.Label)
return "", fmt.Errorf("a(z) %q mező kitöltése kötelező — használja a Generálás gombot vagy írjon be egy jelszót", field.Label)
}
default:
@@ -101,13 +140,13 @@ func (m *Manager) DeployStack(req DeployRequest) error {
// Validate required fields
if field.Required && value == "" {
return fmt.Errorf("a(z) %q (%s) mező kitöltése kötelező", field.Label, field.EnvVar)
return "", fmt.Errorf("a(z) %q (%s) mező kitöltése kötelező", field.Label, field.EnvVar)
}
// Validate path fields exist on disk (inside the container's filesystem)
if field.Type == "path" && value != "" {
if _, err := os.Stat(value); os.IsNotExist(err) {
return fmt.Errorf("path %q does not exist for field %q", value, field.Label)
return "", fmt.Errorf("path %q does not exist for field %q", value, field.Label)
}
}
@@ -129,7 +168,7 @@ func (m *Manager) DeployStack(req DeployRequest) error {
}
if err := SaveAppConfig(stackDir, appCfg); err != nil {
return fmt.Errorf("saving app config: %w", err)
return "", fmt.Errorf("saving app config: %w", err)
}
// Debug: log final env var keys (not values)
@@ -140,12 +179,12 @@ func (m *Manager) DeployStack(req DeployRequest) error {
m.logger.Printf("[INFO] Deploying stack %s with %d env vars: [%s]", req.StackName, len(env), strings.Join(envKeys, ", "))
// Run docker compose up -d
_, err := m.composeExecWithEnv(stackDir, env, "up", "-d")
if err != nil {
_, composeErr := m.composeExecWithEnv(stackDir, env, "up", "-d")
if composeErr != nil {
// Deployment failed — keep app.yaml for debugging but mark as not deployed
appCfg.Deployed = false
_ = SaveAppConfig(stackDir, appCfg)
return fmt.Errorf("docker compose up failed: %w", err)
return "", fmt.Errorf("docker compose up failed: %w", composeErr)
}
// Update in-memory stack state immediately so the UI reflects the deployment
@@ -158,7 +197,7 @@ func (m *Manager) DeployStack(req DeployRequest) error {
m.mu.Unlock()
m.logger.Printf("[INFO] Stack %s deployed successfully", req.StackName)
return m.RefreshStatus()
return deployWarning, m.RefreshStatus()
}
// UpdateStackConfig updates non-locked fields for a deployed stack.
+64
View File
@@ -8,6 +8,7 @@ import (
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
@@ -516,4 +517,67 @@ func (m *Manager) execCommand(name string, args ...string) (string, error) {
}
return stdout.String(), nil
}
// --- Memory helpers ---
// ParseMemoryMB parses a memory string like "500M", "1G", "1.5G", "1024M", "768"
// into megabytes. Returns 0 for empty or unparseable values. Case-insensitive.
func ParseMemoryMB(s string) int {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
upper := strings.ToUpper(s)
if strings.HasSuffix(upper, "GB") {
val, err := strconv.ParseFloat(strings.TrimSuffix(upper, "GB"), 64)
if err != nil {
return 0
}
return int(val * 1024)
}
if strings.HasSuffix(upper, "G") {
val, err := strconv.ParseFloat(strings.TrimSuffix(upper, "G"), 64)
if err != nil {
return 0
}
return int(val * 1024)
}
if strings.HasSuffix(upper, "MB") {
val, err := strconv.ParseFloat(strings.TrimSuffix(upper, "MB"), 64)
if err != nil {
return 0
}
return int(val)
}
if strings.HasSuffix(upper, "M") {
val, err := strconv.ParseFloat(strings.TrimSuffix(upper, "M"), 64)
if err != nil {
return 0
}
return int(val)
}
// Plain number — assume MB
val, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return int(val)
}
// CommittedMemory returns the sum of mem_request and mem_limit across all deployed stacks.
func (m *Manager) CommittedMemory() (requestMB int, limitMB int) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, s := range m.stacks {
if !s.Deployed {
continue
}
requestMB += ParseMemoryMB(s.Meta.Resources.MemRequest)
limitMB += ParseMemoryMB(s.Meta.Resources.MemLimit)
}
return
}