package app import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "github.com/wild-cloud/wild-cli/internal/config" "github.com/wild-cloud/wild-cli/internal/environment" "github.com/wild-cloud/wild-cli/internal/external" "github.com/wild-cloud/wild-cli/internal/output" ) var ( force bool dryRun bool ) func newDeployCommand() *cobra.Command { cmd := &cobra.Command{ Use: "deploy ", Short: "Deploy an application to the cluster", Long: `Deploy an application to the Kubernetes cluster. This processes the app templates with current configuration and deploys them using kubectl and kustomize. Examples: wild app deploy nextcloud wild app deploy postgresql --force wild app deploy myapp --dry-run`, Args: cobra.ExactArgs(1), RunE: runDeploy, } cmd.Flags().BoolVar(&force, "force", false, "force deployment (replace existing resources)") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would be deployed without making changes") return cmd } func runDeploy(cmd *cobra.Command, args []string) error { appName := args[0] output.Header("Deploying Application") output.Info("App: " + appName) // Initialize environment env := environment.New() if err := env.RequiresProject(); err != nil { return err } // Check if app exists in project appDir := filepath.Join(env.AppsDir(), appName) if _, err := os.Stat(appDir); os.IsNotExist(err) { return fmt.Errorf("app '%s' not found in project. Run 'wild app add %s' first", appName, appName) } // Check external tools toolManager := external.NewManager() if err := toolManager.CheckTools(cmd.Context(), []string{"kubectl"}); err != nil { return fmt.Errorf("required tools not available: %w", err) } // Load configuration configMgr := config.NewManager(env.ConfigPath(), env.SecretsPath()) // Check if app is enabled enabledValue, err := configMgr.Get("apps." + appName + ".enabled") if err != nil || enabledValue == nil { output.Warning("App '" + appName + "' is not configured") output.Info("Run: wild config set apps." + appName + ".enabled true") return nil } enabled, ok := enabledValue.(bool) if !ok || !enabled { output.Warning("App '" + appName + "' is disabled") output.Info("Run: wild config set apps." + appName + ".enabled true") return nil } // Process templates with configuration output.Info("Processing templates...") processedDir := filepath.Join(env.WildCloudDir(), "processed", appName) if err := os.RemoveAll(processedDir); err != nil { return fmt.Errorf("cleaning processed directory: %w", err) } if err := processAppTemplates(appDir, processedDir, configMgr); err != nil { return fmt.Errorf("processing templates: %w", err) } // Deploy secrets if required if err := deployAppSecrets(cmd.Context(), appName, appDir, configMgr, toolManager.Kubectl()); err != nil { return fmt.Errorf("deploying secrets: %w", err) } // Deploy using kubectl + kustomize output.Info("Deploying to cluster...") kubectl := toolManager.Kubectl() if err := kubectl.ApplyKustomize(cmd.Context(), processedDir, "", dryRun); err != nil { return fmt.Errorf("deploying with kubectl: %w", err) } if dryRun { output.Success("Dry run completed - no changes made") } else { output.Success("App '" + appName + "' deployed successfully") } // Show next steps output.Info("") output.Info("Monitor deployment:") output.Info(" kubectl get pods -n " + appName) output.Info(" kubectl logs -f deployment/" + appName + " -n " + appName) return nil } // processAppTemplates processes app templates with configuration func processAppTemplates(appDir, processedDir string, configMgr *config.Manager) error { // Create template engine engine, err := config.NewTemplateEngine(configMgr) if err != nil { return fmt.Errorf("creating template engine: %w", err) } // Walk through app directory return filepath.Walk(appDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Calculate relative path relPath, err := filepath.Rel(appDir, path) if err != nil { return err } destPath := filepath.Join(processedDir, relPath) if info.IsDir() { return os.MkdirAll(destPath, info.Mode()) } // Read file content content, err := os.ReadFile(path) if err != nil { return err } // Process as template if it's a YAML file if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") { processed, err := engine.Process(string(content)) if err != nil { return fmt.Errorf("processing template %s: %w", relPath, err) } content = []byte(processed) } // Write processed content if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { return err } return os.WriteFile(destPath, content, info.Mode()) }) } // deployAppSecrets deploys application secrets func deployAppSecrets(ctx context.Context, appName, appDir string, configMgr *config.Manager, kubectl *external.KubectlTool) error { // Check for manifest.yaml with required secrets manifestPath := filepath.Join(appDir, "manifest.yaml") manifestData, err := os.ReadFile(manifestPath) if os.IsNotExist(err) { return nil // No manifest, no secrets needed } if err != nil { return fmt.Errorf("reading manifest: %w", err) } var manifest struct { RequiredSecrets []string `yaml:"requiredSecrets"` } if err := yaml.Unmarshal(manifestData, &manifest); err != nil { return fmt.Errorf("parsing manifest: %w", err) } if len(manifest.RequiredSecrets) == 0 { return nil // No secrets required } output.Info("Deploying secrets...") // Collect secret data secretData := make(map[string]string) for _, secretPath := range manifest.RequiredSecrets { value, err := configMgr.GetSecret(secretPath) if err != nil || value == nil { return fmt.Errorf("required secret '%s' not found", secretPath) } secretData[secretPath] = fmt.Sprintf("%v", value) } // Create secret in cluster secretName := appName + "-secrets" if err := kubectl.CreateSecret(ctx, secretName, appName, secretData); err != nil { return fmt.Errorf("creating secret: %w", err) } output.Success("Secrets deployed") return nil }