Files
wild-cloud/wild-cli/cmd/wild/app/deploy.go

224 lines
6.1 KiB
Go

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 <name>",
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
}