diff --git a/workspaces/controller/test/e2e/e2e_suite_test.go b/workspaces/controller/test/e2e/e2e_suite_test.go index c7515dae..4f4975a2 100644 --- a/workspaces/controller/test/e2e/e2e_suite_test.go +++ b/workspaces/controller/test/e2e/e2e_suite_test.go @@ -104,7 +104,7 @@ var _ = BeforeSuite(func() { } } By("checking that istioctl is available") - Expect(utils.WaitIstioctlAvailable()).To(Succeed(), "istioctl is not available") + Expect(utils.WaitIstioAvailable()).To(Succeed(), "istioctl is not available") }) diff --git a/workspaces/controller/test/utils/utils.go b/workspaces/controller/test/utils/utils.go index 91655b14..fc3194c6 100644 --- a/workspaces/controller/test/utils/utils.go +++ b/workspaces/controller/test/utils/utils.go @@ -17,10 +17,10 @@ limitations under the License. package utils import ( - "errors" "fmt" "os" "os/exec" + "runtime" "strings" . "github.com/onsi/ginkgo/v2" //nolint:golint @@ -30,7 +30,7 @@ const ( // use LTS version of istioctl istioctlVersion = "1.27.0" - istioctlURL = "" + istioctlURL = "" // use LTS version of prometheus-operator prometheusOperatorVersion = "v0.72.0" @@ -66,55 +66,215 @@ func Run(cmd *exec.Cmd) (string, error) { return string(output), nil } -// UninstallPrometheusOperator uninstalls the prometheus func UninstallIstioctl() { - url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) - cmd := exec.Command("kubectl", "delete", "-f", url) - if _, err := Run(cmd); err != nil { - warnError(err) - } + // First, uninstall Istio components if they exist + if IsIstioInstalled() { + fmt.Println("Uninstalling Istio components...") + cmd := exec.Command("istioctl", "uninstall", "--purge", "-y") + if _, err := Run(cmd); err != nil { + warnError(fmt.Errorf("failed to uninstall Istio components: %w", err)) + } + + // Delete istio-system namespace + cmd = exec.Command("kubectl", "delete", "namespace", "istio-system", "--ignore-not-found") + if _, err := Run(cmd); err != nil { + warnError(fmt.Errorf("failed to delete istio-system namespace: %w", err)) + } + } + + // Remove istioctl binary from system + osName := runtime.GOOS + var rmCmd *exec.Cmd + + if osName == "windows" { + // Windows: Remove from C:\istioctl + rmCmd = exec.Command("del", "/f", "/q", "C:\\istioctl\\istioctl.exe") + } else { + // Unix-like: Remove from /usr/local/bin + rmCmd = exec.Command("sudo", "rm", "-f", "/usr/local/bin/istioctl") + } + + if _, err := Run(rmCmd); err != nil { + warnError(fmt.Errorf("failed to remove istioctl binary: %w", err)) + } + + fmt.Println("Istioctl uninstalled successfully") } // InstallIstioctl installs the istioctl to be used to manage istio resources. -func InstallIstioctl() error{ - url := fmt.Sprintf("https://github.com/istio/istio/releases/download/%[1]s/istioctl-%[1]s-linux-amd64.tar.gz", istioctlVersion) - downloadCmd := exec.Command("curl", "-L", url, "-o", "istioctl.tar.gz") - extractCmd := exec.Command("tar", "-xzf", "istioctl.tar.gz") - chmodCmd := exec.Command("chmod", "+x", "istioctl") - moveCmd := exec.Command("sudo", "mv", "istioctl", "~/usr/local/bin/istioctl") +func InstallIstioctl() error { + osName := runtime.GOOS + archName := runtime.GOARCH - downloadCmd.Stdout, downloadCmd.Stderr = os.Stdout, os.Stderr - extractCmd.Stdout, extractCmd.Stderr = os.Stdout, os.Stderr - chmodCmd.Stdout, chmodCmd.Stderr = os.Stdout, os.Stderr - moveCmd.Stdout, moveCmd.Stderr = os.Stdout, os.Stderr + // Map Go architecture names to Istio release names + switch archName { + case "amd64": + archName = "amd64" + case "arm64": + archName = "arm64" + case "386": + return fmt.Errorf("32-bit architectures are not supported by Istio") + default: + return fmt.Errorf("unsupported architecture: %s", archName) + } - if err := downloadCmd.Run(); err != nil { - return fmt.Errorf("failed to download istioctl: %w", err) - } - if err := extractCmd.Run(); err != nil { - return fmt.Errorf("failed to extract istioctl: %w", err) - } - if err := chmodCmd.Run(); err != nil { - return fmt.Errorf("failed to make istioctl executable: %w", err) - } - if err := moveCmd.Run(); err != nil { - return fmt.Errorf("failed to move istioctl to /usr/local/bin: %w", err) - } - return nil + // Map Go OS names to Istio release names and determine file extension + var fileExt string + switch osName { + case "linux": + osName = "linux" + fileExt = "tar.gz" + case "darwin": + osName = "osx" + fileExt = "tar.gz" + case "windows": + osName = "win" + fileExt = "zip" + default: + return fmt.Errorf("unsupported operating system: %s", osName) + } + + // Construct the download URL dynamically + fileName := fmt.Sprintf("istioctl-%s-%s-%s.%s", istioctlVersion, osName, archName, fileExt) + url := fmt.Sprintf("https://github.com/istio/istio/releases/download/%s/%s", istioctlVersion, fileName) + + // Set the binary name based on OS + binaryName := "istioctl" + if osName == "win" { + binaryName = "istioctl.exe" + } + + // Download the file using platform-appropriate method with fallbacks + downloadSuccess := false + + // Try primary download method + var primaryCmd *exec.Cmd + var fallbackCmd *exec.Cmd + + if osName == "win" { + // Windows: PowerShell first, curl as fallback + // Use proper PowerShell syntax with double quotes and escape handling + primaryCmd = exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-Command", + fmt.Sprintf(`Invoke-WebRequest -Uri "%s" -OutFile "%s"`, url, fileName)) + fallbackCmd = exec.Command("curl", "-L", url, "-o", fileName) + } else { + // Unix-like: curl first, wget as fallback + primaryCmd = exec.Command("curl", "-L", url, "-o", fileName) + fallbackCmd = exec.Command("wget", "-O", fileName, url) + } + + // Try primary method + primaryCmd.Stdout, primaryCmd.Stderr = os.Stdout, os.Stderr + if err := primaryCmd.Run(); err == nil { + downloadSuccess = true + } else { + // Try fallback method + fallbackCmd.Stdout, fallbackCmd.Stderr = os.Stdout, os.Stderr + if err := fallbackCmd.Run(); err == nil { + downloadSuccess = true + } + } + + if !downloadSuccess { + if osName == "win" { + return fmt.Errorf("failed to download istioctl from %s using both PowerShell and curl", url) + } else { + return fmt.Errorf("failed to download istioctl from %s using both curl and wget", url) + } + } + + // Extract based on file type + var extractCmd *exec.Cmd + switch fileExt { + case "tar.gz": + extractCmd = exec.Command("tar", "-xzf", fileName) + case "zip": + extractCmd = exec.Command("unzip", "-q", fileName) + default: + return fmt.Errorf("unsupported file extension: %s", fileExt) + } + extractCmd.Stdout, extractCmd.Stderr = os.Stdout, os.Stderr + + if err := extractCmd.Run(); err != nil { + return fmt.Errorf("failed to extract %s: %w", fileName, err) + } + + // Find the extracted binary (it could be in various subdirectories) + findCmd := exec.Command("find", ".", "-name", binaryName, "-type", "f") + output, err := findCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to find istioctl binary after extraction: %w", err) + } + + binaryPath := strings.TrimSpace(string(output)) + if binaryPath == "" { + return fmt.Errorf("istioctl binary not found in extracted files") + } + + // Use the first found binary if multiple exist + if strings.Contains(binaryPath, "\n") { + binaryPath = strings.Split(binaryPath, "\n")[0] + } + + // Copy the binary to current directory with standard name + if binaryPath != binaryName { + var cpCmd *exec.Cmd + if osName == "win" { + cpCmd = exec.Command("copy", binaryPath, binaryName) + } else { + cpCmd = exec.Command("cp", binaryPath, binaryName) + } + if err := cpCmd.Run(); err != nil { + return fmt.Errorf("failed to copy istioctl binary: %w", err) + } + } + + // Make executable (not needed on Windows) + if osName != "win" { + chmodCmd := exec.Command("chmod", "+x", binaryName) + if err := chmodCmd.Run(); err != nil { + return fmt.Errorf("failed to make istioctl executable: %w", err) + } + } + + // Move to appropriate bin directory + var moveCmd *exec.Cmd + if osName == "win" { + // Try to create and use a local bin directory on Windows + mkdirCmd := exec.Command("mkdir", "-p", "C:\\istioctl") + mkdirCmd.Run() // Ignore errors if directory exists + moveCmd = exec.Command("move", binaryName, "C:\\istioctl\\istioctl.exe") + } else { + moveCmd = exec.Command("sudo", "mv", binaryName, "/usr/local/bin/istioctl") + } + + moveCmd.Stdout, moveCmd.Stderr = os.Stdout, os.Stderr + if err := moveCmd.Run(); err != nil { + return fmt.Errorf("failed to move istioctl to bin directory: %w", err) + } + + // Clean up downloaded files + cleanupCmd := exec.Command("rm", "-f", fileName) + if osName == "win" { + cleanupCmd = exec.Command("del", "/f", fileName) + } + cleanupCmd.Run() // Ignore cleanup errors + + return nil } // InstallIstioMinimalWithIngress installs Istio with minimal profile and ingressgateway enabled. func InstallIstioMinimalWithIngress(namespace string) error { - cmd := exec.Command("istioctl", - "install", - "--set", "profile=minimal", - "--set", "values.gateways.istio-ingressgateway.enabled=true", - "--set", fmt.Sprintf("values.global.istioNamespace=%s", namespace), - "-y", - ) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + cmd := exec.Command("istioctl", + "install", + "--set", "profile=minimal", + "--set", "values.gateways.istio-ingressgateway.enabled=true", + "--set", fmt.Sprintf("values.global.istioNamespace=%s", namespace), + "-y", + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() } // TODO: @@ -124,15 +284,161 @@ func IsIstioInstalled() bool { return err == nil } -// WaitIstioctlAvailable checks if 'istioctl' is available in PATH. -// Returns nil if found, or an error if not found. -func WaitIstioctlAvailable() error { - if _, err := exec.LookPath("istioctl"); err != nil { - return errors.New("istioctl binary not found in PATH") +// WaitIstioAvailable waits for Istio to be available and running. +// Returns nil if Istio is ready, or an error if not ready within timeout. +func WaitIstioAvailable() error { + // Wait for istio-system namespace to exist + cmd := exec.Command("kubectl", "wait", + "--for=condition=Ready", + "namespace/istio-system", + "--timeout=300s") + if _, err := Run(cmd); err != nil { + return fmt.Errorf("istio-system namespace not ready: %w", err) + } + + // Wait for Istio control plane (istiod) pods to be ready + cmd = exec.Command("kubectl", "wait", + "--for=condition=Ready", + "pods", + "-l", "app=istiod", + "-n", "istio-system", + "--timeout=300s") + if _, err := Run(cmd); err != nil { + return fmt.Errorf("istiod pods not ready: %w", err) + } + + // Wait for Istio ingress gateway pods to be ready + cmd = exec.Command("kubectl", "wait", + "--for=condition=Ready", + "pods", + "-l", "app=istio-ingressgateway", + "-n", "istio-system", + "--timeout=300s") + if _, err := Run(cmd); err != nil { + return fmt.Errorf("istio-ingressgateway pods not ready: %w", err) + } + + // Wait for Istio egress gateway pods to be ready (if present) + // Note: egress gateway is optional, so we don't fail if it's not found + cmd = exec.Command("kubectl", "get", + "pods", + "-l", "app=istio-egressgateway", + "-n", "istio-system", + "--no-headers") + output, err := Run(cmd) + if err == nil && len(strings.TrimSpace(output)) > 0 { + // Egress gateway exists, wait for it to be ready + cmd = exec.Command("kubectl", "wait", + "--for=condition=Ready", + "pods", + "-l", "app=istio-egressgateway", + "-n", "istio-system", + "--timeout=300s") + if _, err := Run(cmd); err != nil { + return fmt.Errorf("istio-egressgateway pods not ready: %w", err) + } + } + + // Verify istioctl can analyze (optional validation) + cmd = exec.Command("istioctl", "analyze", "--all-namespaces") + if _, err := Run(cmd); err != nil { + return fmt.Errorf("istioctl analyze failed: %w", err) + } + + return nil +} + +// IsIstioIngressGatewayInstalled checks if istio-ingressgateway is installed +func IsIstioIngressGatewayInstalled() bool { + cmd := exec.Command("kubectl", "get", "deployment", + "-n", "istio-system", + "istio-ingressgateway", + "--ignore-not-found") + output, err := Run(cmd) + if err != nil { + return false } + return len(strings.TrimSpace(output)) > 0 +} + +// InstallIstioIngressGateway installs the istio-ingressgateway +func InstallIstioIngressGateway() error { + // Check if Istio is installed first + if !IsIstioInstalled() { + return fmt.Errorf("istio must be installed before installing ingress gateway") + } + + // Install ingress gateway using istioctl + cmd := exec.Command("istioctl", "install", + "--set", "components.ingressGateways[0].enabled=true", + "--set", "components.ingressGateways[0].name=istio-ingressgateway", + "-y") + + _, err := Run(cmd) + return err +} + +// WaitIstioIngressGatewayReady waits for istio-ingressgateway to be ready +func WaitIstioIngressGatewayReady() error { + // Wait for the deployment to be available + cmd := exec.Command("kubectl", "wait", + "--for=condition=Available", + "deployment/istio-ingressgateway", + "-n", "istio-system", + "--timeout=300s") + + if _, err := Run(cmd); err != nil { + return fmt.Errorf("istio-ingressgateway deployment not available: %w", err) + } + + // Wait for the pods to be ready + cmd = exec.Command("kubectl", "wait", + "--for=condition=Ready", + "pods", + "-l", "app=istio-ingressgateway", + "-n", "istio-system", + "--timeout=300s") + + if _, err := Run(cmd); err != nil { + return fmt.Errorf("istio-ingressgateway pods not ready: %w", err) + } + + // Wait for the service to have external IP (if LoadBalancer type) + cmd = exec.Command("kubectl", "get", "service", + "istio-ingressgateway", + "-n", "istio-system", + "-o", "jsonpath={.status.loadBalancer.ingress[0].ip}") + + // Note: This might not apply for all environments (like kind/minikube) + // so we don't fail if external IP is not assigned + Run(cmd) // Ignore error for external IP check + return nil } +// EnsureIstioIngressGateway checks, installs, and waits for istio-ingressgateway +func EnsureIstioIngressGateway() error { + // Check if already installed + if IsIstioIngressGatewayInstalled() { + fmt.Println("Istio ingress gateway is already installed") + } else { + fmt.Println("Installing Istio ingress gateway...") + if err := InstallIstioIngressGateway(); err != nil { + return fmt.Errorf("failed to install istio-ingressgateway: %w", err) + } + } + + // Wait for it to be ready + fmt.Println("Waiting for Istio ingress gateway to be ready...") + if err := WaitIstioIngressGatewayReady(); err != nil { + return fmt.Errorf("istio-ingressgateway failed to become ready: %w", err) + } + + fmt.Println("Istio ingress gateway is ready!") + return nil +} + + // UninstallPrometheusOperator uninstalls the prometheus func UninstallPrometheusOperator() { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)