Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: an inbuilt gateway to handle kubectl host proxying #91

Merged
merged 8 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading