From d57503af53632c5d547c445e2ae4858cd2b87ed0 Mon Sep 17 00:00:00 2001 From: Gyanendra Mishra Date: Tue, 6 Aug 2024 16:14:01 +0100 Subject: [PATCH] feat: an inbuilt gateway to handle kubectl host proxying (#91) this is useful in a few places 1. you can't run minikube tunnel -> say you are using a remote cluster 2. you are on codespaces and you can't run minikube tunnel --- kardinal-cli/cmd/root.go | 49 ++++++- kardinal-cli/deployment/gateway.go | 164 ++++++++++++++++++++++++ kardinal-cli/go.mod | 3 + kardinal-cli/go.sum | 9 ++ kardinal-manager/gomod2nix.toml | 12 +- libs/cli-kontrol-api/gomod2nix.toml | 12 +- libs/manager-kontrol-api/gomod2nix.toml | 12 +- 7 files changed, 241 insertions(+), 20 deletions(-) create mode 100644 kardinal-cli/deployment/gateway.go diff --git a/kardinal-cli/cmd/root.go b/kardinal-cli/cmd/root.go index a30502e0..f39300c0 100644 --- a/kardinal-cli/cmd/root.go +++ b/kardinal-cli/cmd/root.go @@ -28,8 +28,6 @@ import ( ) const ( - projectName = "kardinal" - kontrolBaseURLTmpl = "%s://%s" kontrolClusterResourcesEndpointTmpl = "%s/tenant/%s/cluster-resources" @@ -185,6 +183,52 @@ var dashboardCmd = &cobra.Command{ }, } +var gatewayCmd = &cobra.Command{ + Use: "gateway ", + Short: "Opens a gateway to the given flow", + Args: cobra.MatchAll(cobra.ExactArgs(1)), + Run: func(cmr *cobra.Command, args []string) { + flowId := args[0] + + tenantUuid, err := tenant.GetOrCreateUserTenantUUID() + if err != nil { + log.Fatal("Error getting or creating user tenant UUID", err) + } + + ctx := context.Background() + client := getKontrolServiceClient() + + resp, err := client.GetTenantUuidFlowsWithResponse(ctx, tenantUuid.String()) + if err != nil { + log.Fatalf("Failed to list flows: %v", err) + } + + if resp == nil { + log.Fatalf("List flow response is empty") + } + + var host string + + for _, flow := range *resp.JSON200 { + if flow.FlowId == flowId { + if len(flow.FlowUrls) > 0 { + host = flow.FlowUrls[0] + } else { + log.Fatalf("Flow '%s' has no hosts", flowId) + } + } + } + + if host == "" { + log.Fatalf("Couldn't find flow with id '%s'", flowId) + } + + if err := deployment.StartGateway(host); err != nil { + log.Fatal("An error occurred while creating a gateway", err) + } + }, +} + func init() { devMode = false if os.Getenv("KARDINAL_CLI_DEV_MODE") == "TRUE" { @@ -195,6 +239,7 @@ func init() { rootCmd.AddCommand(managerCmd) rootCmd.AddCommand(deployCmd) rootCmd.AddCommand(dashboardCmd) + rootCmd.AddCommand(gatewayCmd) flowCmd.AddCommand(listCmd, createCmd, deleteCmd) managerCmd.AddCommand(deployManagerCmd, removeManagerCmd) diff --git a/kardinal-cli/deployment/gateway.go b/kardinal-cli/deployment/gateway.go new file mode 100644 index 00000000..3346553e --- /dev/null +++ b/kardinal-cli/deployment/gateway.go @@ -0,0 +1,164 @@ +package deployment + +import ( + "context" + "fmt" + "io" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/signal" + "strings" + "syscall" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" +) + +const ( + namespace = "istio-system" + service = "istio-ingressgateway" + localPortForIstio = 9080 + istioGatewayPodPort = 8080 + proxyServerPort = 9060 +) + +func StartGateway(host string) error { + log.Printf("Starting gateway for host: %s", host) + + client, err := createKubernetesClient() + if err != nil { + return fmt.Errorf("an error occurred while creating a kubernetes client:\n %v", err) + } + + // Find a pod for the service + pod, err := findPodForService(client.clientSet) + if err != nil { + return fmt.Errorf("failed to find pod for service: %v", err) + } + + // Start port forwarding + stopChan := make(chan struct{}, 1) + readyChan := make(chan struct{}) + go func() { + for { + err := portForwardPod(client.config, pod, stopChan, readyChan) + if err != nil { + log.Printf("Port forwarding failed: %v. Retrying in 5 seconds...", err) + time.Sleep(5 * time.Second) + continue + } + break + } + }() + + // Wait for port forwarding to be ready + <-readyChan + + // Start proxy server + proxy := createProxy(host) + server := &http.Server{ + Addr: fmt.Sprintf(":%d", proxyServerPort), + Handler: proxy, + } + + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start proxy server: %v", err) + } + }() + + log.Printf("Proxy server for host %s started on http://localhost:%d", host, proxyServerPort) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + log.Println("Shutting down...") + close(stopChan) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + log.Printf("Server shutdown error: %v", err) + } + + return nil +} + +func findPodForService(client *kubernetes.Clientset) (string, error) { + svc, err := client.CoreV1().Services(namespace).Get(context.Background(), service, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("error getting service: %v", err) + } + + var labelSelectors []string + for key, value := range svc.Spec.Selector { + labelSelectors = append(labelSelectors, fmt.Sprintf("%s=%s", key, value)) + } + selector := strings.Join(labelSelectors, ",") + + pods, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: selector}) + if err != nil { + return "", fmt.Errorf("error listing pods: %v", err) + } + + if len(pods.Items) == 0 { + return "", fmt.Errorf("no pods found for service %s", service) + } + + podName := pods.Items[0].Name + return podName, nil +} + +func portForwardPod(config *rest.Config, podName string, stopChan <-chan struct{}, readyChan chan struct{}) error { + roundTripper, upgrader, err := spdy.RoundTripperFor(config) + if err != nil { + return fmt.Errorf("failed to create round tripper: %v", err) + } + + path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, podName) + hostIP := strings.TrimLeft(config.Host, "htps:/") + + serverURL, err := url.Parse(fmt.Sprintf("https://%s%s", hostIP, path)) + if err != nil { + return fmt.Errorf("failed to parse URL: %v", err) + } + + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, serverURL) + + ports := []string{fmt.Sprintf("%d:%d", localPortForIstio, istioGatewayPodPort)} + forwarder, err := portforward.New(dialer, ports, stopChan, readyChan, io.Discard, os.Stderr) + if err != nil { + return fmt.Errorf("failed to create port forwarder: %v", err) + } + + return forwarder.ForwardPorts() +} + +func createProxy(host string) *httputil.ReverseProxy { + target, _ := url.Parse(fmt.Sprintf("http://localhost:%d", localPortForIstio)) + proxy := httputil.NewSingleHostReverseProxy(target) + + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + req.Host = host // Set the Host header to the provided host + req.Header.Set("X-Forwarded-Host", host) + } + + proxy.ModifyResponse = func(resp *http.Response) error { + // Set cache-control headers + resp.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0") + resp.Header.Set("Pragma", "no-cache") + resp.Header.Set("Expires", "0") + return nil + } + + return proxy +} diff --git a/kardinal-cli/go.mod b/kardinal-cli/go.mod index 960c975e..1602a829 100644 --- a/kardinal-cli/go.mod +++ b/kardinal-cli/go.mod @@ -27,12 +27,15 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/oapi-codegen/runtime v1.1.1 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect diff --git a/kardinal-cli/go.sum b/kardinal-cli/go.sum index 949efe23..faebfd2f 100644 --- a/kardinal-cli/go.sum +++ b/kardinal-cli/go.sum @@ -3,6 +3,8 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -42,6 +44,9 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8 github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -64,6 +69,8 @@ github.com/kurtosis-tech/stacktrace v0.0.0-20211028211901-1c67a77b5409 h1:YQTATi github.com/kurtosis-tech/stacktrace v0.0.0-20211028211901-1c67a77b5409/go.mod h1:y5weVs5d9wXXHcDA1awRxkIhhHC1xxYJN8a7aXnE6S8= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -71,6 +78,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= diff --git a/kardinal-manager/gomod2nix.toml b/kardinal-manager/gomod2nix.toml index 7ad25b6b..2e60c91d 100644 --- a/kardinal-manager/gomod2nix.toml +++ b/kardinal-manager/gomod2nix.toml @@ -359,8 +359,8 @@ schema = 3 version = "v2.1.0" hash = "sha256-R+84l1si8az5yDqd5CYcFrTyNZ1eSYlpXKq6nFt4OTQ=" [mod."github.com/samber/lo"] - version = "v1.39.0" - hash = "sha256-bFJXbzFpUQM2xoNrHrlzn0RJZujd21H00zGBgo/JEb0=" + version = "v1.46.0" + hash = "sha256-ZvyiOnjqh3nt8OxofUPbXxN14j5bHcmT9TqOCPdwAVQ=" [mod."github.com/schollz/closestmatch"] version = "v2.1.0+incompatible" hash = "sha256-SpWqGfqlMkZPQ6TSf7NTaYMbQllBaBgPM8oxTBOTn7w=" @@ -458,14 +458,14 @@ schema = 3 version = "v0.20.0" hash = "sha256-kU+OVJbYktTIn4ZTAdomsOjL069Vj45sdroEMRKaRDI=" [mod."golang.org/x/text"] - version = "v0.15.0" - hash = "sha256-pBnj0AEkfkvZf+3bN7h6epCD2kurw59clDP7yWvxKlk=" + version = "v0.16.0" + hash = "sha256-hMTO45upjEuA4sJzGplJT+La2n3oAfHccfYWZuHcH+8=" [mod."golang.org/x/time"] version = "v0.5.0" hash = "sha256-W6RgwgdYTO3byIPOFxrP2IpAZdgaGowAaVfYby7AULU=" [mod."golang.org/x/tools"] - version = "v0.21.0" - hash = "sha256-TU0gAxUX410AYc/nMxxZiaqXeORih1cXbKh3sxKufVg=" + version = "v0.21.1-0.20240508182429-e35e4ccd0d2d" + hash = "sha256-KfnS+3fREPAWQUBoUedPupQp9yLrugxMmmEoHvyzKNE=" [mod."golang.org/x/xerrors"] version = "v0.0.0-20220907171357-04be3eba64a2" hash = "sha256-6+zueutgefIYmgXinOflz8qGDDDj0Zhv+2OkGhBTKno=" diff --git a/libs/cli-kontrol-api/gomod2nix.toml b/libs/cli-kontrol-api/gomod2nix.toml index 7ad25b6b..2e60c91d 100644 --- a/libs/cli-kontrol-api/gomod2nix.toml +++ b/libs/cli-kontrol-api/gomod2nix.toml @@ -359,8 +359,8 @@ schema = 3 version = "v2.1.0" hash = "sha256-R+84l1si8az5yDqd5CYcFrTyNZ1eSYlpXKq6nFt4OTQ=" [mod."github.com/samber/lo"] - version = "v1.39.0" - hash = "sha256-bFJXbzFpUQM2xoNrHrlzn0RJZujd21H00zGBgo/JEb0=" + version = "v1.46.0" + hash = "sha256-ZvyiOnjqh3nt8OxofUPbXxN14j5bHcmT9TqOCPdwAVQ=" [mod."github.com/schollz/closestmatch"] version = "v2.1.0+incompatible" hash = "sha256-SpWqGfqlMkZPQ6TSf7NTaYMbQllBaBgPM8oxTBOTn7w=" @@ -458,14 +458,14 @@ schema = 3 version = "v0.20.0" hash = "sha256-kU+OVJbYktTIn4ZTAdomsOjL069Vj45sdroEMRKaRDI=" [mod."golang.org/x/text"] - version = "v0.15.0" - hash = "sha256-pBnj0AEkfkvZf+3bN7h6epCD2kurw59clDP7yWvxKlk=" + version = "v0.16.0" + hash = "sha256-hMTO45upjEuA4sJzGplJT+La2n3oAfHccfYWZuHcH+8=" [mod."golang.org/x/time"] version = "v0.5.0" hash = "sha256-W6RgwgdYTO3byIPOFxrP2IpAZdgaGowAaVfYby7AULU=" [mod."golang.org/x/tools"] - version = "v0.21.0" - hash = "sha256-TU0gAxUX410AYc/nMxxZiaqXeORih1cXbKh3sxKufVg=" + version = "v0.21.1-0.20240508182429-e35e4ccd0d2d" + hash = "sha256-KfnS+3fREPAWQUBoUedPupQp9yLrugxMmmEoHvyzKNE=" [mod."golang.org/x/xerrors"] version = "v0.0.0-20220907171357-04be3eba64a2" hash = "sha256-6+zueutgefIYmgXinOflz8qGDDDj0Zhv+2OkGhBTKno=" diff --git a/libs/manager-kontrol-api/gomod2nix.toml b/libs/manager-kontrol-api/gomod2nix.toml index 7ad25b6b..2e60c91d 100644 --- a/libs/manager-kontrol-api/gomod2nix.toml +++ b/libs/manager-kontrol-api/gomod2nix.toml @@ -359,8 +359,8 @@ schema = 3 version = "v2.1.0" hash = "sha256-R+84l1si8az5yDqd5CYcFrTyNZ1eSYlpXKq6nFt4OTQ=" [mod."github.com/samber/lo"] - version = "v1.39.0" - hash = "sha256-bFJXbzFpUQM2xoNrHrlzn0RJZujd21H00zGBgo/JEb0=" + version = "v1.46.0" + hash = "sha256-ZvyiOnjqh3nt8OxofUPbXxN14j5bHcmT9TqOCPdwAVQ=" [mod."github.com/schollz/closestmatch"] version = "v2.1.0+incompatible" hash = "sha256-SpWqGfqlMkZPQ6TSf7NTaYMbQllBaBgPM8oxTBOTn7w=" @@ -458,14 +458,14 @@ schema = 3 version = "v0.20.0" hash = "sha256-kU+OVJbYktTIn4ZTAdomsOjL069Vj45sdroEMRKaRDI=" [mod."golang.org/x/text"] - version = "v0.15.0" - hash = "sha256-pBnj0AEkfkvZf+3bN7h6epCD2kurw59clDP7yWvxKlk=" + version = "v0.16.0" + hash = "sha256-hMTO45upjEuA4sJzGplJT+La2n3oAfHccfYWZuHcH+8=" [mod."golang.org/x/time"] version = "v0.5.0" hash = "sha256-W6RgwgdYTO3byIPOFxrP2IpAZdgaGowAaVfYby7AULU=" [mod."golang.org/x/tools"] - version = "v0.21.0" - hash = "sha256-TU0gAxUX410AYc/nMxxZiaqXeORih1cXbKh3sxKufVg=" + version = "v0.21.1-0.20240508182429-e35e4ccd0d2d" + hash = "sha256-KfnS+3fREPAWQUBoUedPupQp9yLrugxMmmEoHvyzKNE=" [mod."golang.org/x/xerrors"] version = "v0.0.0-20220907171357-04be3eba64a2" hash = "sha256-6+zueutgefIYmgXinOflz8qGDDDj0Zhv+2OkGhBTKno="