This commit is contained in:
2026-02-13 21:15:00 +01:00
parent bcc7877c41
commit fd29e602e8
4 changed files with 134 additions and 124 deletions
+19 -30
View File
@@ -35,6 +35,7 @@ type DeployRequest struct {
// 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 {
stack, ok := m.GetStack(req.StackName)
if !ok {
@@ -50,7 +51,7 @@ func (m *Manager) DeployStack(req DeployRequest) error {
return fmt.Errorf("stack %q is already deployed; use update instead", req.StackName)
}
// Debug: log received values (redact passwords)
// Debug: log received values (redact passwords/secrets)
m.logger.Printf("[DEBUG] Deploy %s: received %d user values", req.StackName, len(req.Values))
for k, v := range req.Values {
if strings.Contains(strings.ToLower(k), "password") || strings.Contains(strings.ToLower(k), "secret") {
@@ -103,7 +104,7 @@ func (m *Manager) DeployStack(req DeployRequest) error {
return fmt.Errorf("a(z) %q (%s) mező kitöltése kötelező", field.Label, field.EnvVar)
}
// Validate path fields exist
// 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)
@@ -147,6 +148,15 @@ func (m *Manager) DeployStack(req DeployRequest) error {
return fmt.Errorf("docker compose up failed: %w", err)
}
// Update in-memory stack state immediately so the UI reflects the deployment
// without waiting for the next ScanStacks() cycle.
m.mu.Lock()
if s, ok := m.stacks[req.StackName]; ok {
s.Deployed = true
s.AppConfig = appCfg
}
m.mu.Unlock()
m.logger.Printf("[INFO] Stack %s deployed successfully", req.StackName)
return m.RefreshStatus()
}
@@ -164,7 +174,6 @@ func (m *Manager) UpdateStackConfig(name string, values map[string]string) error
return fmt.Errorf("stack %q is not deployed yet", name)
}
// Apply changes, respecting locked fields
lockedSet := make(map[string]bool)
for _, f := range appCfg.LockedFields {
lockedSet[f] = true
@@ -181,7 +190,6 @@ func (m *Manager) UpdateStackConfig(name string, values map[string]string) error
return fmt.Errorf("saving updated config: %w", err)
}
// Restart with new env
_, err := m.composeExecWithEnv(stackDir, appCfg.Env, "up", "-d")
if err != nil {
return fmt.Errorf("restarting with new config: %w", err)
@@ -193,14 +201,11 @@ func (m *Manager) UpdateStackConfig(name string, values map[string]string) error
// composeExecWithEnv runs a compose command with custom env vars injected.
func (m *Manager) composeExecWithEnv(dir string, env map[string]string, args ...string) (string, error) {
// Build env slice: start with os env, then add our vars
cmdEnv := os.Environ()
for k, v := range env {
cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v))
}
// Always inject DOMAIN from controller config
cmdEnv = append(cmdEnv, fmt.Sprintf("DOMAIN=%s", m.cfg.Customer.Domain))
return m.composeExecCustomEnv(dir, cmdEnv, args...)
}
@@ -220,15 +225,12 @@ func (m *Manager) GetDeployFields(name string) (*Metadata, *AppConfig, error) {
// --- App config persistence ---
// LoadAppConfig reads app.yaml from a stack directory.
// Returns nil if the file doesn't exist.
func LoadAppConfig(stackDir string) *AppConfig {
path := filepath.Join(stackDir, "app.yaml")
data, err := os.ReadFile(path)
if err != nil {
return nil
}
cfg := &AppConfig{}
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil
@@ -236,18 +238,14 @@ func LoadAppConfig(stackDir string) *AppConfig {
return cfg
}
// SaveAppConfig writes app.yaml to a stack directory.
func SaveAppConfig(stackDir string, cfg *AppConfig) error {
data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("marshaling app config: %w", err)
}
path := filepath.Join(stackDir, "app.yaml")
header := "# Auto-generated by felhom-controller — do not edit locked fields manually\n"
content := header + string(data)
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
return fmt.Errorf("writing %s: %w", path, err)
}
@@ -258,45 +256,36 @@ func SaveAppConfig(stackDir string, cfg *AppConfig) error {
const alphanumChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// generateValue creates a random value based on the generator spec.
// Formats: "password:N", "hex:N", "static:VALUE"
func generateValue(spec string) (string, error) {
if spec == "" {
return "", fmt.Errorf("empty generator spec")
}
parts := strings.SplitN(spec, ":", 2)
if len(parts) != 2 {
return "", fmt.Errorf("invalid generator spec: %q (expected type:param)", spec)
}
genType := parts[0]
param := parts[1]
switch genType {
switch parts[0] {
case "password":
length := 0
if _, err := fmt.Sscanf(param, "%d", &length); err != nil || length <= 0 {
return "", fmt.Errorf("invalid password length: %q", param)
if _, err := fmt.Sscanf(parts[1], "%d", &length); err != nil || length <= 0 {
return "", fmt.Errorf("invalid password length: %q", parts[1])
}
return randomAlphanumeric(length)
case "hex":
byteLen := 0
if _, err := fmt.Sscanf(param, "%d", &byteLen); err != nil || byteLen <= 0 {
return "", fmt.Errorf("invalid hex length: %q", param)
if _, err := fmt.Sscanf(parts[1], "%d", &byteLen); err != nil || byteLen <= 0 {
return "", fmt.Errorf("invalid hex length: %q", parts[1])
}
b := make([]byte, byteLen)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("reading random bytes: %w", err)
}
return hex.EncodeToString(b), nil
case "static":
return param, nil
return parts[1], nil
default:
return "", fmt.Errorf("unknown generator type: %q", genType)
return "", fmt.Errorf("unknown generator type: %q", parts[0])
}
}