565 lines
16 KiB
Go
565 lines
16 KiB
Go
package setup
|
|
|
|
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 (
|
|
skipInstall bool
|
|
)
|
|
|
|
func newServicesCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "services",
|
|
Short: "Set up cluster services",
|
|
Long: `Set up essential cluster services like ingress, DNS, and monitoring.
|
|
|
|
This command generates service configurations and installs core Kubernetes services
|
|
including MetalLB, Traefik, cert-manager, and others.
|
|
|
|
Examples:
|
|
wild setup services
|
|
wild setup services --skip-install`,
|
|
RunE: runServices,
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&skipInstall, "skip-install", false, "generate service configs but skip installation")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runServices(cmd *cobra.Command, args []string) error {
|
|
output.Header("Wild Cloud Services Setup")
|
|
|
|
// 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 := getConfigString(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 !skipInstall {
|
|
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("Wild Cloud Services Setup Complete!")
|
|
output.Info("")
|
|
|
|
if !skipInstall {
|
|
// 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")
|
|
}
|
|
|
|
output.Success("Wild Cloud setup completed!")
|
|
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: %s", strings.Join(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
|
|
}
|
|
|
|
// 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: %s", strings.Join(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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|