Skip to content

Commit

Permalink
feat: an inbuilt gateway to handle kubectl host proxying (#91)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
h4ck3rk3y authored Aug 6, 2024
1 parent 98cb3f3 commit d57503a
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 20 deletions.
49 changes: 47 additions & 2 deletions kardinal-cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import (
)

const (
projectName = "kardinal"

kontrolBaseURLTmpl = "%s://%s"
kontrolClusterResourcesEndpointTmpl = "%s/tenant/%s/cluster-resources"

Expand Down Expand Up @@ -185,6 +183,52 @@ var dashboardCmd = &cobra.Command{
},
}

var gatewayCmd = &cobra.Command{
Use: "gateway <flow-id>",
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" {
Expand All @@ -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)

Expand Down
164 changes: 164 additions & 0 deletions kardinal-cli/deployment/gateway.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions kardinal-cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions kardinal-cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -64,13 +69,17 @@ 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=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
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=
Expand Down
12 changes: 6 additions & 6 deletions kardinal-manager/gomod2nix.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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="
Expand Down Expand Up @@ -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="
Expand Down
12 changes: 6 additions & 6 deletions libs/cli-kontrol-api/gomod2nix.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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="
Expand Down Expand Up @@ -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="
Expand Down
12 changes: 6 additions & 6 deletions libs/manager-kontrol-api/gomod2nix.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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="
Expand Down Expand Up @@ -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="
Expand Down

0 comments on commit d57503a

Please sign in to comment.