package cluster import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" "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 ( servicesSkipInstall bool ) func newServicesCommand() *cobra.Command { cmd := &cobra.Command{ Use: "services", Short: "Manage cluster services", Long: `Deploy and manage essential cluster services. This command provides cluster service management including generation and deployment. Examples: wild cluster services deploy wild cluster services deploy --skip-install`, } cmd.AddCommand( newServicesDeployCommand(), ) return cmd } func newServicesDeployCommand() *cobra.Command { cmd := &cobra.Command{ Use: "deploy", Short: "Deploy cluster services", Long: `Deploy essential cluster services like ingress, DNS, and monitoring. This generates service configurations and installs core Kubernetes services including MetalLB, Traefik, cert-manager, and others. Examples: wild cluster services deploy wild cluster services deploy --skip-install`, RunE: runServicesDeploy, } cmd.Flags().BoolVar(&servicesSkipInstall, "skip-install", false, "generate service configs but skip installation") return cmd } func runServicesDeploy(cmd *cobra.Command, args []string) error { output.Header("Cluster Services Deployment") // Initialize environment env := environment.New() if err := env.RequiresProject(); err != nil { return err } // 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 cluster configuration clusterName, err := getRequiredConfig(configMgr, "cluster.name", "") if err != nil { return fmt.Errorf("cluster configuration is missing: %w", err) } output.Info("Cluster: " + clusterName) // Check kubectl connectivity kubectl := toolManager.Kubectl() if err := checkKubectlConnectivity(cmd.Context(), kubectl); err != nil { return fmt.Errorf("kubectl is not configured or cluster is not accessible: %w", err) } output.Success("Cluster is accessible") // Phase 1: Generate cluster services setup files output.Info("\n=== Phase 1: Generating Service Configurations ===") if err := generateClusterServices(cmd.Context(), env, configMgr); err != nil { return fmt.Errorf("generating service configurations: %w", err) } // Phase 2: Install cluster services if !servicesSkipInstall { output.Info("\n=== Phase 2: Installing Cluster Services ===") if err := installClusterServices(cmd.Context(), env, kubectl); err != nil { return fmt.Errorf("installing cluster services: %w", err) } } else { output.Info("Skipping cluster services installation (--skip-install specified)") output.Info("You can install them later with: wild cluster services deploy") } // Summary output output.Success("Cluster Services Deployment Complete!") output.Info("") if !servicesSkipInstall { // Get internal domain for next steps internalDomain, err := configMgr.Get("cloud.internalDomain") domain := "your-internal-domain" if err == nil && internalDomain != nil { if domainStr, ok := internalDomain.(string); ok { domain = domainStr } } output.Info("Next steps:") output.Info(" 1. Access the dashboard at: https://dashboard." + domain) output.Info(" 2. Get the dashboard token with: wild dashboard token") output.Info("") output.Info("To verify components, run:") output.Info(" - kubectl get pods -n cert-manager") output.Info(" - kubectl get pods -n externaldns") output.Info(" - kubectl get pods -n kubernetes-dashboard") output.Info(" - kubectl get clusterissuers") } else { output.Info("Next steps:") output.Info(" 1. Ensure your cluster is running and kubectl is configured") output.Info(" 2. Install services with: wild cluster services deploy") output.Info(" 3. Verify components are running correctly") } return nil } // checkKubectlConnectivity checks if kubectl can connect to the cluster func checkKubectlConnectivity(ctx context.Context, kubectl *external.KubectlTool) error { // Try to get cluster info _, err := kubectl.Execute(ctx, "cluster-info") if err != nil { return fmt.Errorf("cluster not accessible: %w", err) } return nil } // generateClusterServices generates cluster service configurations func generateClusterServices(ctx context.Context, env *environment.Environment, configMgr *config.Manager) error { // This function replicates wild-cluster-services-generate functionality output.Info("Generating cluster services setup files...") wcRoot := env.WCRoot() if wcRoot == "" { return fmt.Errorf("WC_ROOT not set") } sourceDir := filepath.Join(wcRoot, "setup", "cluster-services") destDir := filepath.Join(env.WildCloudDir(), "setup", "cluster-services") // Check if source directory exists if _, err := os.Stat(sourceDir); os.IsNotExist(err) { return fmt.Errorf("cluster setup source directory not found: %s", sourceDir) } // Force regeneration, removing existing files if _, err := os.Stat(destDir); err == nil { output.Info("Force regeneration enabled, removing existing files...") if err := os.RemoveAll(destDir); err != nil { return fmt.Errorf("removing existing setup directory: %w", err) } } // Create destination directory setupBaseDir := filepath.Join(env.WildCloudDir(), "setup") if err := os.MkdirAll(setupBaseDir, 0755); err != nil { return fmt.Errorf("creating setup directory: %w", err) } // Copy README if it doesn't exist readmePath := filepath.Join(setupBaseDir, "README.md") if _, err := os.Stat(readmePath); os.IsNotExist(err) { sourceReadme := filepath.Join(wcRoot, "setup", "README.md") if _, err := os.Stat(sourceReadme); err == nil { if err := copyFile(sourceReadme, readmePath); err != nil { output.Warning("Failed to copy README.md: " + err.Error()) } } } // Create destination directory if err := os.MkdirAll(destDir, 0755); err != nil { return fmt.Errorf("creating destination directory: %w", err) } // Copy and compile cluster setup files output.Info("Copying and compiling cluster setup files from repository...") // First, copy root-level files from setup/cluster-services/ if err := copyRootServiceFiles(sourceDir, destDir); err != nil { return fmt.Errorf("copying root service files: %w", err) } // Then, process each service directory if err := processServiceDirectories(sourceDir, destDir, configMgr); err != nil { return fmt.Errorf("processing service directories: %w", err) } // Verify required configuration if err := verifyServiceConfiguration(configMgr); err != nil { output.Warning("Configuration verification warnings: " + err.Error()) } output.Success("Cluster setup files copied and compiled") output.Info("Generated setup directory: " + destDir) // List available services services, err := getAvailableServices(destDir) if err != nil { return fmt.Errorf("listing available services: %w", err) } output.Info("Available services:") for _, service := range services { output.Info(" - " + service) } return nil } // installClusterServices installs the cluster services func installClusterServices(ctx context.Context, env *environment.Environment, kubectl *external.KubectlTool) error { setupDir := filepath.Join(env.WildCloudDir(), "setup", "cluster-services") // Check if cluster setup directory exists if _, err := os.Stat(setupDir); os.IsNotExist(err) { return fmt.Errorf("cluster services setup directory not found: %s", setupDir) } output.Info("Installing cluster services...") // Install services in dependency order servicesToInstall := []string{ "metallb", "longhorn", "traefik", "coredns", "cert-manager", "externaldns", "kubernetes-dashboard", "nfs", "docker-registry", } // Filter to only include services that actually exist existingServices := []string{} for _, service := range servicesToInstall { installScript := filepath.Join(setupDir, service, "install.sh") if _, err := os.Stat(installScript); err == nil { existingServices = append(existingServices, service) } } if len(existingServices) == 0 { return fmt.Errorf("no installable services found") } output.Info(fmt.Sprintf("Services to install: %v", existingServices)) // Install services installedCount := 0 failedCount := 0 for _, service := range existingServices { output.Info(fmt.Sprintf("\n--- Installing %s ---", service)) installScript := filepath.Join(setupDir, service, "install.sh") if err := runServiceInstaller(ctx, setupDir, service, installScript); err != nil { output.Error(fmt.Sprintf("%s installation failed: %v", service, err)) failedCount++ } else { output.Success(fmt.Sprintf("%s installed successfully", service)) installedCount++ } } // Summary output.Info("\nInstallation Summary:") output.Success(fmt.Sprintf("Successfully installed: %d services", installedCount)) if failedCount > 0 { output.Warning(fmt.Sprintf("Failed to install: %d services", failedCount)) } if failedCount == 0 { output.Success("All cluster services installed successfully!") } else { return fmt.Errorf("some services failed to install") } return nil } // Helper functions (reused from setup/services.go with minimal modifications) // copyRootServiceFiles copies root-level files from source to destination func copyRootServiceFiles(sourceDir, destDir string) error { entries, err := os.ReadDir(sourceDir) if err != nil { return err } for _, entry := range entries { if !entry.IsDir() { srcPath := filepath.Join(sourceDir, entry.Name()) dstPath := filepath.Join(destDir, entry.Name()) output.Info(" Copying: " + entry.Name()) if err := copyFile(srcPath, dstPath); err != nil { return err } } } return nil } // processServiceDirectories processes each service directory func processServiceDirectories(sourceDir, destDir string, configMgr *config.Manager) error { entries, err := os.ReadDir(sourceDir) if err != nil { return err } // Create template engine engine, err := config.NewTemplateEngine(configMgr) if err != nil { return fmt.Errorf("creating template engine: %w", err) } for _, entry := range entries { if !entry.IsDir() { continue } serviceName := entry.Name() serviceDir := filepath.Join(sourceDir, serviceName) destServiceDir := filepath.Join(destDir, serviceName) output.Info("Processing service: " + serviceName) // Create destination service directory if err := os.MkdirAll(destServiceDir, 0755); err != nil { return err } // Process service files if err := processServiceFiles(serviceDir, destServiceDir, engine); err != nil { return fmt.Errorf("processing service %s: %w", serviceName, err) } } return nil } // processServiceFiles processes files in a service directory func processServiceFiles(serviceDir, destServiceDir string, engine *config.TemplateEngine) error { entries, err := os.ReadDir(serviceDir) if err != nil { return err } for _, entry := range entries { srcPath := filepath.Join(serviceDir, entry.Name()) dstPath := filepath.Join(destServiceDir, entry.Name()) if entry.Name() == "kustomize.template" { // Compile kustomize.template to kustomize directory if entry.IsDir() { output.Info(" Compiling kustomize templates") kustomizeDir := filepath.Join(destServiceDir, "kustomize") if err := processTemplateDirectory(srcPath, kustomizeDir, engine); err != nil { return err } } } else if entry.IsDir() { // Copy other directories recursively if err := copyDir(srcPath, dstPath); err != nil { return err } } else { // Process individual files if err := processServiceFile(srcPath, dstPath, engine); err != nil { return err } } } return nil } // processServiceFile processes a single service file func processServiceFile(srcPath, dstPath string, engine *config.TemplateEngine) error { content, err := os.ReadFile(srcPath) if err != nil { return err } // Check if file contains template syntax if strings.Contains(string(content), "{{") { output.Info(" Compiling: " + filepath.Base(srcPath)) processed, err := engine.Process(string(content)) if err != nil { return fmt.Errorf("processing template: %w", err) } return os.WriteFile(dstPath, []byte(processed), 0644) } else { return copyFile(srcPath, dstPath) } } // processTemplateDirectory processes an entire template directory func processTemplateDirectory(srcDir, dstDir string, engine *config.TemplateEngine) error { if err := os.RemoveAll(dstDir); err != nil && !os.IsNotExist(err) { return err } return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } relPath, err := filepath.Rel(srcDir, path) if err != nil { return err } dstPath := filepath.Join(dstDir, relPath) if info.IsDir() { return os.MkdirAll(dstPath, info.Mode()) } content, err := os.ReadFile(path) if err != nil { return err } // Process template content if strings.Contains(string(content), "{{") { processed, err := engine.Process(string(content)) if err != nil { return fmt.Errorf("processing template %s: %w", relPath, err) } content = []byte(processed) } // Create parent directory if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { return err } return os.WriteFile(dstPath, content, info.Mode()) }) } // verifyServiceConfiguration verifies required configuration func verifyServiceConfiguration(configMgr *config.Manager) error { missingConfig := []string{} // Check essential configuration values requiredConfigs := []string{ "cluster.name", "cloud.domain", "cluster.ipAddressPool", "operator.email", } for _, configPath := range requiredConfigs { if value, err := configMgr.Get(configPath); err != nil || value == nil { missingConfig = append(missingConfig, configPath) } } if len(missingConfig) > 0 { return fmt.Errorf("missing required configuration values: %v", missingConfig) } return nil } // getAvailableServices returns list of available services func getAvailableServices(setupDir string) ([]string, error) { var services []string entries, err := os.ReadDir(setupDir) if err != nil { return nil, err } for _, entry := range entries { if entry.IsDir() { installScript := filepath.Join(setupDir, entry.Name(), "install.sh") if _, err := os.Stat(installScript); err == nil { services = append(services, entry.Name()) } } } return services, nil } // runServiceInstaller runs a service installation script func runServiceInstaller(ctx context.Context, setupDir, serviceName, installScript string) error { // Change to the service directory and run install.sh serviceDir := filepath.Join(setupDir, serviceName) // Execute the install script using bash bashTool := external.NewBaseTool("bash", "bash") // Change to the service directory by setting working directory in the execution context oldDir, err := os.Getwd() if err != nil { return fmt.Errorf("getting current directory: %w", err) } if err := os.Chdir(serviceDir); err != nil { return fmt.Errorf("changing to service directory: %w", err) } defer func() { _ = os.Chdir(oldDir) }() _, err = bashTool.Execute(ctx, "install.sh") if err != nil { return fmt.Errorf("install script failed: %w", err) } return nil } // copyFile copies a single file func copyFile(src, dst string) error { data, err := os.ReadFile(src) if err != nil { return err } return os.WriteFile(dst, data, 0644) } // copyDir recursively copies a directory func copyDir(src, dst string) error { entries, err := os.ReadDir(src) if err != nil { return err } if err := os.MkdirAll(dst, 0755); err != nil { return err } for _, entry := range entries { srcPath := filepath.Join(src, entry.Name()) dstPath := filepath.Join(dst, entry.Name()) if entry.IsDir() { if err := copyDir(srcPath, dstPath); err != nil { return err } } else { if err := copyFile(srcPath, dstPath); err != nil { return err } } } return nil }