feat: deployed app removal + missing field injection (v0.19.0)

Add "Eltávolítás" to remove deployed (non-orphaned) stacks — reverts
them to "Nincs telepítve" while preserving templates for redeploy.
Modal offers HDD data and backup data cleanup choices.

Auto-inject missing deploy fields (secrets, domains) into existing
app.yaml when templates are updated via sync or on controller startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 11:01:21 +01:00
parent 99bf3ca7a8
commit 8130c344cc
10 changed files with 518 additions and 21 deletions
+82
View File
@@ -2,6 +2,7 @@ package stacks
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
@@ -424,6 +425,16 @@ func generateValue(spec string) (string, error) {
return "", fmt.Errorf("reading random bytes: %w", err)
}
return hex.EncodeToString(b), nil
case "base64key":
byteLen := 0
if _, err := fmt.Sscanf(parts[1], "%d", &byteLen); err != nil || byteLen <= 0 {
return "", fmt.Errorf("invalid base64key length: %q", parts[1])
}
b := make([]byte, byteLen)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("reading random bytes: %w", err)
}
return "base64:" + base64.StdEncoding.EncodeToString(b), nil
case "static":
return parts[1], nil
default:
@@ -431,6 +442,77 @@ func generateValue(spec string) (string, error) {
}
}
// InjectMissingFields checks deployed stacks for new deploy_fields that are not
// yet in app.yaml and auto-generates values for secret/domain fields.
// Called after sync (for updated stacks) and on startup (for all deployed stacks).
func (m *Manager) InjectMissingFields(stackNames []string) {
for _, name := range stackNames {
stack, ok := m.GetStack(name)
if !ok {
continue
}
stackDir := filepath.Dir(stack.ComposePath)
meta := LoadMetadata(stackDir)
appCfg := LoadAppConfig(stackDir)
if appCfg == nil || !appCfg.Deployed {
continue
}
var injected []string
for _, field := range meta.DeployFields {
if _, exists := appCfg.Env[field.EnvVar]; exists {
continue // already present
}
switch field.Type {
case "secret":
if field.Generate == "" {
m.logger.Printf("[WARN] Stack %s: new secret field %s has no generator — skipping", name, field.EnvVar)
continue
}
value, err := generateValue(field.Generate)
if err != nil {
m.logger.Printf("[ERROR] Stack %s: failed to generate %s: %v", name, field.EnvVar, err)
continue
}
appCfg.Env[field.EnvVar] = value
if field.LockedAfterDeploy {
appCfg.LockedFields = append(appCfg.LockedFields, field.EnvVar)
}
injected = append(injected, field.EnvVar)
case "domain":
appCfg.Env[field.EnvVar] = m.cfg.Customer.Domain
if field.LockedAfterDeploy && !containsStr(appCfg.LockedFields, field.EnvVar) {
appCfg.LockedFields = append(appCfg.LockedFields, field.EnvVar)
}
injected = append(injected, field.EnvVar)
default:
m.logger.Printf("[WARN] Stack %s: new field %s (type=%s) requires manual configuration", name, field.EnvVar, field.Type)
}
}
if len(injected) > 0 {
if err := SaveAppConfig(stackDir, appCfg); err != nil {
m.logger.Printf("[ERROR] Stack %s: failed to save app.yaml after injection: %v", name, err)
continue
}
m.logger.Printf("[SYNC] Stack %s: injected missing fields: %s", name, strings.Join(injected, ", "))
}
}
}
func containsStr(slice []string, s string) bool {
for _, v := range slice {
if v == s {
return true
}
}
return false
}
func randomAlphanumeric(length int) (string, error) {
result := make([]byte, length)
for i := range result {