feat: add cross-platform istioctl installation and Istio gateway management

- Add InstallIstioctl() with OS/arch detection for Linux, macOS, Windows
- Add comprehensive Istio gateway functions (install, check, wait)
- Enhance WaitIstioAvailable() to verify all Istio components
- Add UninstallIstioctl() for complete cleanup
- Use platform-specific download methods with fallbacks

Signed-off-by: Yash Pal <yashpal2104@gmail.com>
This commit is contained in:
Yash Pal 2025-09-06 16:33:46 +05:30
parent 95f18da965
commit 6b345ffe48
2 changed files with 353 additions and 47 deletions

View File

@ -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")
})

View File

@ -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)