-
-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Rewrite stunnerctl and turncat to use the CDS API, fix #81
So far stunnerctl and turncat have used the ConfigMap rendered by the operator to load the running config of the requested Gateway. Since the ConfigMap is slated to be removed, this rewrite makes sure that the CLI tools access dataplane configs from the CDS API served by the operator.
- Loading branch information
Showing
22 changed files
with
1,095 additions
and
789 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,74 @@ | ||
# stunnerctl: Command line toolbox for STUNner | ||
|
||
A CLI tool to simplify the interaction with STUNner. | ||
A CLI tool to simplify the interaction with STUNner. | ||
The prominent use of `stunnerctl` is to load or watch STUNner dataplane configurations from a Kubernetes cluster for debugging and troubleshooting, or just for checking whether everything is configured the way it should be. | ||
|
||
## Installation | ||
|
||
Install the `stunnerctl` binary using the standard Go toolchain and add it to `$PATH`. | ||
|
||
```console | ||
go install github.com/l7mp/stunner/cmd/stunnerctl@latest | ||
``` | ||
|
||
You can also enforce a specific OS, CPU architecture, and STUNner version: | ||
|
||
```console | ||
GOOS=windows GOARCH=amd64 go install github.com/l7mp/stunner/cmd/[email protected] | ||
``` | ||
|
||
Building from source is as easy as it usually gets with Go: | ||
|
||
```console | ||
cd stunner | ||
go build -o stunnerctl cmd/stunnerctl/main.go | ||
``` | ||
|
||
## Usage | ||
|
||
Dump the running config of a STUNner gateway in human-readable format. | ||
Type `stunnerctl` to get a glimpse of the features provided. Below are some common usage examples. | ||
|
||
- Dump a summary of the running config of the STUNner gateway called `udp-gateway` deployed into the `stunner` namespace: | ||
|
||
```console | ||
stunnerctl -n stunner config udp-gateway | ||
Gateway: stunner/udp-gateway (loglevel: "all:INFO") | ||
Authentication type: static, username/password: user-1/pass-1 | ||
Listeners: | ||
- Name: stunner/udp-gateway/udp-listener | ||
Protocol: TURN-UDP | ||
Public address:port: 34.118.88.91:9001 | ||
Routes: [stunner/iperf-server] | ||
Endpoints: [10.76.1.3, 10.80.7.104] | ||
``` | ||
|
||
- Dump a the running config of all gateways in the `stunner` namespace in JSON format (YAML is also available using `-o yaml`): | ||
|
||
The below will select the Gateway called `tcp-gateway` in the `stunner` namespace: | ||
```console | ||
stunnerctl -n stunner config -o json | ||
{"version":"v1","admin":{"name":"stunner/tcp-gateway",...}} | ||
{"version":"v1","admin":{"name":"stunner/udp-gateway",...}}} | ||
``` | ||
|
||
- Watch all STUNner configs as they are being refreshed and dump only the name of the STUNner gateway whose config changes: | ||
|
||
```console | ||
stunnerctl config --all-namespaces -o jsonpath='{.admin.name}' -w | ||
stunner/tcp-gateway | ||
stunner/udp-gateway | ||
... | ||
``` | ||
|
||
## Fallback | ||
|
||
For those who don't have the Go toolchain available to run `go install`, STUNner provides a minimalistic `stunnerctl` replacement called `stunnerctl.sh`. | ||
This script requires nothing else than `bash`, `kubectl`, `curl` and `jq` to work. | ||
|
||
The below will dump the running config of `tcp-gateway` deployed into the `stunner` namespace: | ||
|
||
```console | ||
cmd/stunnerctl/stunnerctl running-config stunner/stunner-gateway | ||
cd stunner | ||
cmd/stunnerctl/stunnerctl.sh running-config stunner/stunner-gateway | ||
STUN/TURN authentication type: static | ||
STUN/TURN username: user-1 | ||
STUN/TURN password: pass-1 | ||
|
@@ -21,6 +80,30 @@ Listener 1 | |
Public port: 3478 | ||
``` | ||
|
||
## Last resort | ||
|
||
You can use `kubectl port-forward` to load or watch STUNner configs manually. | ||
Open a port-forwarded connection to the STUNner gateway operator: | ||
|
||
``` console | ||
export CDS_SERVER_NAME=$(kubectl get pods -l stunner.l7mp.io/config-discovery-service=enabled --all-namespaces -o jsonpath='{.items[0].metadata.name}') | ||
export CDS_SERVER_NAMESPACE=$(kubectl get pods -l stunner.l7mp.io/config-discovery-service=enabled --all-namespaces -o jsonpath='{.items[0].metadata.namespace}') | ||
kubectl -n $CDS_SERVER_NAMESPACE port-forward pod/${CDS_SERVER_NAME} 63478:13478 & | ||
``` | ||
|
||
If all goes well, you can now connect to the STUNner config discovery API served by the gateway operator directly, just using `curl`. | ||
The below will load the config of the `udp-gateway` in the `stunner` namespace: | ||
|
||
``` console | ||
curl -s http://127.0.0.1:63478/api/v1/configs/stunner/udp-gateway | ||
``` | ||
|
||
If you happen to have a WebSocket client like the wonderful [`websocat`](https://github.com/vi/websocat) tool installed, you can also watch the configs as they are being rendered by the operator en live. | ||
|
||
``` console | ||
websocat ws://127.0.0.1:63478/api/v1/configs/stunner/udp-gateway?watch=true - | ||
``` | ||
|
||
## License | ||
|
||
Copyright 2021-2023 by its authors. Some rights reserved. See [AUTHORS](../../AUTHORS). | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"os/signal" | ||
"regexp" | ||
"strings" | ||
"syscall" | ||
|
||
"github.com/pion/logging" | ||
"github.com/spf13/cobra" | ||
cliopt "k8s.io/cli-runtime/pkg/genericclioptions" | ||
"k8s.io/client-go/util/jsonpath" | ||
"sigs.k8s.io/yaml" | ||
|
||
stnrv1 "github.com/l7mp/stunner/pkg/apis/v1" | ||
cdsclient "github.com/l7mp/stunner/pkg/config/client" | ||
"github.com/l7mp/stunner/pkg/logger" | ||
) | ||
|
||
// list all configs: stunnerctl get config --all-namespaces | ||
// watch all configs in namesapce stunner: stunnerctl -n stunner get config --watch | ||
// get short-form config for stunner/udp-gateway: stunnerctl -n stunner get config udp-gateway | ||
// get config for stunner/udp-gateway in yaml format: stunnerctl -n stunner get config udp-gateway --output yaml | ||
|
||
var ( | ||
output string | ||
watch, all, verbose bool | ||
jsonQuery *jsonpath.JSONPath | ||
k8sConfigFlags *cliopt.ConfigFlags | ||
cdsConfigFlags *cdsclient.CDSConfigFlags | ||
loggerFactory *logger.LeveledLoggerFactory | ||
log logging.LeveledLogger | ||
|
||
rootCmd = &cobra.Command{ | ||
Use: "stunnerctl", | ||
Short: "A command line utility to inspect STUNner dataplane configurations.", | ||
Long: "The stunnerctl tool is a CLI for inspecting, watching and troublehssooting the configuration of STUNner gateways", | ||
DisableAutoGenTag: true, | ||
} | ||
) | ||
|
||
var ( | ||
configCmd = &cobra.Command{ | ||
Use: "config", | ||
Aliases: []string{"stunner-config"}, | ||
Short: "Gets or watches STUNner configs", | ||
Args: cobra.RangeArgs(0, 1), | ||
DisableAutoGenTag: true, | ||
Run: func(cmd *cobra.Command, args []string) { | ||
if err := runConfig(cmd, args); err != nil { | ||
fmt.Println(err) | ||
os.Exit(1) | ||
} | ||
}, | ||
} | ||
) | ||
|
||
func init() { | ||
rootCmd.PersistentFlags().BoolVarP(&watch, "watch", "w", false, "Watch for config updates from server") | ||
rootCmd.PersistentFlags().BoolVarP(&all, "all-namespaces", "a", false, "Consider all namespaces") | ||
rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "summary", "Output format") | ||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging, identical to -l all:DEBUG") | ||
|
||
// Kubernetes config flags | ||
k8sConfigFlags = cliopt.NewConfigFlags(true) | ||
k8sConfigFlags.AddFlags(rootCmd.PersistentFlags()) | ||
|
||
// CDS server discovery flags | ||
cdsConfigFlags = cdsclient.NewCDSConfigFlags() | ||
cdsConfigFlags.AddFlags(rootCmd.PersistentFlags()) | ||
|
||
rootCmd.AddCommand(configCmd) | ||
} | ||
|
||
func main() { | ||
if err := rootCmd.Execute(); err != nil { | ||
fmt.Fprintf(os.Stderr, "Whoops. There was an error while executing your CLI '%s'", err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func runConfig(cmd *cobra.Command, args []string) error { | ||
loglevel := "all:WARN" | ||
if verbose { | ||
loglevel = "all:TRACE" | ||
} | ||
loggerFactory = logger.NewLoggerFactory(loglevel) | ||
log = loggerFactory.NewLogger("stunnerctl") | ||
|
||
gwNs := "default" | ||
if k8sConfigFlags.Namespace != nil { | ||
gwNs = *k8sConfigFlags.Namespace | ||
} | ||
|
||
if strings.HasPrefix(output, "jsonpath") { | ||
as := strings.Split(output, "=") | ||
if len(as) != 2 || as[0] != "jsonpath" { | ||
return fmt.Errorf("invalid jsonpath output definition %q", output) | ||
} | ||
|
||
jsonQuery = jsonpath.New("output") | ||
|
||
// Parse and print jsonpath | ||
fields, err := RelaxedJSONPathExpression(as[1]) | ||
if err != nil { | ||
return fmt.Errorf("invalid jsonpath query %w", err) | ||
} | ||
|
||
if err := jsonQuery.Parse(fields); err != nil { | ||
return fmt.Errorf("cannor parse jsonpath query %w", err) | ||
} | ||
output = "jsonpath" | ||
} | ||
|
||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
log.Debug("Searching for CDS server") | ||
cdsAddr, err := cdsclient.DiscoverK8sCDSServer(ctx, k8sConfigFlags, cdsConfigFlags, | ||
loggerFactory.NewLogger("cds-fwd")) | ||
if err != nil { | ||
return fmt.Errorf("error searching for CDS server: %w", err) | ||
} | ||
|
||
var cds cdsclient.CdsApi | ||
cdslog := loggerFactory.NewLogger("cds-client") | ||
if all { | ||
cds, err = cdsclient.NewAllConfigsAPI(cdsAddr, cdslog) | ||
} else if len(args) == 0 { | ||
cds, err = cdsclient.NewConfigsNamespaceAPI(cdsAddr, gwNs, cdslog) | ||
} else { | ||
gwName := args[0] | ||
cds, err = cdsclient.NewConfigNamespaceNameAPI(cdsAddr, gwNs, gwName, cdslog) | ||
} | ||
|
||
if err != nil { | ||
return fmt.Errorf("error creating CDS client: %w", err) | ||
} | ||
|
||
confChan := make(chan *stnrv1.StunnerConfig, 8) | ||
if watch { | ||
err := cds.Watch(ctx, confChan) | ||
if err != nil { | ||
close(confChan) | ||
return err | ||
} | ||
|
||
go func() { | ||
sigs := make(chan os.Signal, 1) | ||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) | ||
<-sigs | ||
close(confChan) | ||
}() | ||
} else { | ||
resp, err := cds.Get(ctx) | ||
if err != nil { | ||
close(confChan) | ||
return err | ||
} | ||
for _, c := range resp { | ||
confChan <- c | ||
} | ||
|
||
close(confChan) | ||
} | ||
|
||
for c := range confChan { | ||
switch output { | ||
case "yaml": | ||
if out, err := yaml.Marshal(c); err != nil { | ||
return err | ||
} else { | ||
fmt.Println(string(out)) | ||
} | ||
case "json": | ||
if out, err := json.Marshal(c); err != nil { | ||
return err | ||
} else { | ||
fmt.Println(string(out)) | ||
} | ||
case "jsonpath": | ||
values, err := jsonQuery.FindResults(c) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if len(values) == 0 || len(values[0]) == 0 { | ||
fmt.Println("<none>") | ||
} | ||
|
||
for arrIx := range values { | ||
for valIx := range values[arrIx] { | ||
fmt.Printf("%v\n", values[arrIx][valIx].Interface()) | ||
} | ||
} | ||
case "summary": | ||
fmt.Print(string(c.Summary())) | ||
case "status": | ||
fallthrough | ||
default: | ||
fmt.Println(c.String()) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
var jsonRegexp = regexp.MustCompile(`^\{\.?([^{}]+)\}$|^\.?([^{}]+)$`) | ||
|
||
// k8s.io/kubectl/pkg/cmd/get | ||
func RelaxedJSONPathExpression(pathExpression string) (string, error) { | ||
if len(pathExpression) == 0 { | ||
return pathExpression, nil | ||
} | ||
submatches := jsonRegexp.FindStringSubmatch(pathExpression) | ||
if submatches == nil { | ||
return "", fmt.Errorf("unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'") | ||
} | ||
if len(submatches) != 3 { | ||
return "", fmt.Errorf("unexpected submatch list: %v", submatches) | ||
} | ||
var fieldSpec string | ||
if len(submatches[1]) != 0 { | ||
fieldSpec = submatches[1] | ||
} else { | ||
fieldSpec = submatches[2] | ||
} | ||
return fmt.Sprintf("{.%s}", fieldSpec), nil | ||
} |
Oops, something went wrong.