chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:06 +08:00
commit 60552ca0ce
144 changed files with 20883 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
package cmd
import (
"fmt"
"github.com/creasty/defaults"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/kubeshark/kubeshark/kubernetes/helm"
"github.com/kubeshark/kubeshark/misc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var cleanCmd = &cobra.Command{
Use: "clean",
Short: fmt.Sprintf("Removes all %s resources", misc.Software),
RunE: func(cmd *cobra.Command, args []string) error {
resp, err := helm.NewHelm(
config.Config.Tap.Release.Repo,
config.Config.Tap.Release.Name,
config.Config.Tap.Release.Namespace,
).Uninstall()
if err != nil {
log.Error().Err(err).Send()
} else {
log.Info().Msgf("Uninstalled the Helm release: %s", resp.Release.Name)
}
return nil
},
}
func init() {
rootCmd.AddCommand(cleanCmd)
defaultTapConfig := configStructs.TapConfig{}
if err := defaults.Set(&defaultTapConfig); err != nil {
log.Debug().Err(err).Send()
}
cleanCmd.Flags().StringP(configStructs.ReleaseNamespaceLabel, "s", defaultTapConfig.Release.Namespace, "Release namespace of Kubeshark")
}
+117
View File
@@ -0,0 +1,117 @@
package cmd
import (
"context"
"errors"
"fmt"
"path"
"regexp"
"time"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/errormessage"
"github.com/kubeshark/kubeshark/internal/connect"
"github.com/kubeshark/kubeshark/kubernetes"
"github.com/kubeshark/kubeshark/misc"
"github.com/kubeshark/kubeshark/misc/fsUtils"
"github.com/rs/zerolog/log"
)
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, ctx context.Context, serviceName string, podName string, proxyPortLabel string, srcPort uint16, dstPort uint16, healthCheck string) {
httpServer, err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.Proxy.Host, srcPort, config.Config.Tap.Release.Namespace, serviceName)
if err != nil {
log.Error().
Err(errormessage.FormatError(err)).
Msg(fmt.Sprintf("Error occurred while running K8s proxy. Try setting different port using --%s", proxyPortLabel))
return
}
connector := connect.NewConnector(kubernetes.GetProxyOnPort(srcPort), connect.DefaultRetries, connect.DefaultTimeout)
if err := connector.TestConnection(healthCheck); err != nil {
log.Warn().
Str("service", serviceName).
Msg("Couldn't connect using proxy, stopping proxy and trying to create port-forward...")
if err := httpServer.Shutdown(ctx); err != nil {
log.Error().
Err(errormessage.FormatError(err)).
Msg("Error occurred while stopping proxy.")
}
podRegex, _ := regexp.Compile(podName)
if _, err := kubernetes.NewPortForward(kubernetesProvider, config.Config.Tap.Release.Namespace, podRegex, srcPort, dstPort, ctx); err != nil {
log.Error().
Str("pod-regex", podRegex.String()).
Err(errormessage.FormatError(err)).
Msg(fmt.Sprintf("Error occurred while running port forward. Try setting different port using --%s", proxyPortLabel))
return
}
connector = connect.NewConnector(kubernetes.GetProxyOnPort(srcPort), connect.DefaultRetries, connect.DefaultTimeout)
if err := connector.TestConnection(healthCheck); err != nil {
log.Error().
Str("service", serviceName).
Err(errormessage.FormatError(err)).
Msg("Couldn't connect to service.")
return
}
}
}
func getKubernetesProviderForCli(silent bool, dontCheckVersion bool) (*kubernetes.Provider, error) {
kubeConfigPath := config.Config.KubeConfigPath()
kubernetesProvider, err := kubernetes.NewProvider(kubeConfigPath, config.Config.Kube.Context)
if err != nil {
handleKubernetesProviderError(err)
return nil, err
}
if !silent {
log.Info().Str("path", kubeConfigPath).Msg("Using kubeconfig:")
}
if err := kubernetesProvider.ValidateNotProxy(); err != nil {
handleKubernetesProviderError(err)
return nil, err
}
if !dontCheckVersion {
kubernetesVersion, err := kubernetesProvider.GetKubernetesVersion()
if err != nil {
handleKubernetesProviderError(err)
return nil, err
}
if err := kubernetes.ValidateKubernetesVersion(kubernetesVersion); err != nil {
handleKubernetesProviderError(err)
return nil, err
}
}
return kubernetesProvider, nil
}
func handleKubernetesProviderError(err error) {
var clusterBehindProxyErr *kubernetes.ClusterBehindProxyError
if ok := errors.As(err, &clusterBehindProxyErr); ok {
log.Error().Msg(fmt.Sprintf("Cannot establish http-proxy connection to the Kubernetes cluster. If youre using Lens or similar tool, please run '%s' with regular kubectl config using --%v %v=$HOME/.kube/config flag", misc.Program, config.SetCommandName, config.KubeConfigPathConfigName))
} else {
log.Error().Err(err).Send()
}
}
func finishSelfExecution(kubernetesProvider *kubernetes.Provider) {
removalCtx, cancel := context.WithTimeout(context.Background(), cleanupTimeout)
defer cancel()
dumpLogsIfNeeded(removalCtx, kubernetesProvider)
}
func dumpLogsIfNeeded(ctx context.Context, kubernetesProvider *kubernetes.Provider) {
if !config.Config.DumpLogs {
return
}
dotDir := misc.GetDotFolderPath()
filePath := path.Join(dotDir, fmt.Sprintf("%s_logs_%s.zip", misc.Program, time.Now().Format("2006_01_02__15_04_05")))
if err := fsUtils.DumpLogs(ctx, kubernetesProvider, filePath, config.Config.Logs.Grep); err != nil {
log.Error().Err(err).Msg("Failed to dump logs.")
}
}
+55
View File
@@ -0,0 +1,55 @@
package cmd
import (
"fmt"
"github.com/creasty/defaults"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/kubeshark/kubeshark/misc"
"github.com/kubeshark/kubeshark/utils"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: fmt.Sprintf("Generate %s config with default values", misc.Software),
RunE: func(cmd *cobra.Command, args []string) error {
if config.Config.Config.Regenerate {
defaultConfig := config.CreateDefaultConfig()
if err := defaults.Set(&defaultConfig); err != nil {
log.Error().Err(err).Send()
return nil
}
if err := config.WriteConfig(&defaultConfig); err != nil {
log.Error().Err(err).Msg("Failed generating config with defaults.")
return nil
}
log.Info().Str("config-path", config.ConfigFilePath).Msg("Template file written to config path.")
} else {
template, err := utils.PrettyYaml(config.Config)
if err != nil {
log.Error().Err(err).Msg("Failed converting config with defaults to YAML.")
return nil
}
log.Debug().Str("template", template).Msg("Printing template config...")
fmt.Printf("%v", template)
}
return nil
},
}
func init() {
rootCmd.AddCommand(configCmd)
defaultConfig := config.CreateDefaultConfig()
if err := defaults.Set(&defaultConfig); err != nil {
log.Debug().Err(err).Send()
}
configCmd.Flags().BoolP(configStructs.RegenerateConfigName, "r", defaultConfig.Config.Regenerate, fmt.Sprintf("Regenerate the config file with default values to path %s", config.GetConfigFilePath(nil)))
}
+156
View File
@@ -0,0 +1,156 @@
package cmd
import (
"fmt"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"time"
"github.com/creasty/defaults"
"github.com/gorilla/websocket"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/kubeshark/kubeshark/kubernetes"
"github.com/kubeshark/kubeshark/utils"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var consoleCmd = &cobra.Command{
Use: "console",
Short: "Stream the scripting console logs into shell",
RunE: func(cmd *cobra.Command, args []string) error {
runConsole()
return nil
},
}
func init() {
rootCmd.AddCommand(consoleCmd)
defaultTapConfig := configStructs.TapConfig{}
if err := defaults.Set(&defaultTapConfig); err != nil {
log.Debug().Err(err).Send()
}
consoleCmd.Flags().Uint16(configStructs.ProxyFrontPortLabel, defaultTapConfig.Proxy.Front.Port, "Provide a custom port for the Kubeshark")
consoleCmd.Flags().String(configStructs.ProxyHostLabel, defaultTapConfig.Proxy.Host, "Provide a custom host for the Kubeshark")
consoleCmd.Flags().StringP(configStructs.ReleaseNamespaceLabel, "s", defaultTapConfig.Release.Namespace, "Release namespace of Kubeshark")
}
func runConsoleWithoutProxy() {
log.Info().Msg("Starting scripting console ...")
time.Sleep(5 * time.Second)
hubUrl := kubernetes.GetHubUrl()
for {
// Attempt to connect to the Hub every second
response, err := http.Get(fmt.Sprintf("%s/echo", hubUrl))
if err != nil || response.StatusCode != 200 {
log.Info().Msg(fmt.Sprintf(utils.Yellow, "Couldn't connect to Hub."))
time.Sleep(5 * time.Second)
continue
}
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
log.Info().Str("host", config.Config.Tap.Proxy.Host).Str("url", hubUrl).Msg("Connecting to:")
u := url.URL{
Scheme: "ws",
Host: fmt.Sprintf("%s:%d", config.Config.Tap.Proxy.Host, config.Config.Tap.Proxy.Front.Port),
Path: "/api/scripts/logs",
}
headers := http.Header{}
headers.Set(utils.X_KUBESHARK_CAPTURE_HEADER_KEY, utils.X_KUBESHARK_CAPTURE_HEADER_IGNORE_VALUE)
headers.Set("License-Key", config.Config.License)
c, _, err := websocket.DefaultDialer.Dial(u.String(), headers)
if err != nil {
log.Error().Err(err).Msg("Websocket dial error, retrying in 5 seconds...")
time.Sleep(5 * time.Second) // Delay before retrying
continue
}
defer c.Close()
done := make(chan struct{})
go func() {
defer close(done)
for {
_, message, err := c.ReadMessage()
if err != nil {
log.Error().Err(err).Msg("Error reading websocket message, reconnecting...")
break // Break to reconnect
}
msg := string(message)
if strings.Contains(msg, ":ERROR]") {
msg = fmt.Sprintf(utils.Red, msg)
fmt.Fprintln(os.Stderr, msg)
} else {
fmt.Fprintln(os.Stdout, msg)
}
}
}()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
select {
case <-done:
log.Warn().Msg(fmt.Sprintf(utils.Yellow, "Connection closed, reconnecting..."))
time.Sleep(5 * time.Second) // Delay before reconnecting
continue // Reconnect after error
case <-interrupt:
err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Error().Err(err).Send()
continue
}
select {
case <-done:
case <-time.After(time.Second):
}
return
}
}
}
func runConsole() {
go runConsoleWithoutProxy()
// Create interrupt channel and setup signal handling once
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
done := make(chan struct{})
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-interrupt:
// Handle interrupt and exit gracefully
log.Warn().Msg(fmt.Sprintf(utils.Yellow, "Received interrupt, exiting..."))
select {
case <-done:
case <-time.After(time.Second):
}
return
case <-ticker.C:
// Attempt to connect to the Hub every second
hubUrl := kubernetes.GetHubUrl()
response, err := http.Get(fmt.Sprintf("%s/echo", hubUrl))
if err != nil || response.StatusCode != 200 {
log.Info().Msg(fmt.Sprintf(utils.Yellow, "Couldn't connect to Hub. Establishing proxy..."))
runProxy(false, true)
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
package cmd
import (
"fmt"
"github.com/kubeshark/kubeshark/config"
"github.com/spf13/cobra"
)
var licenseCmd = &cobra.Command{
Use: "license",
Short: "Print the license loaded string",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println(config.Config.License)
return nil
},
}
func init() {
rootCmd.AddCommand(licenseCmd)
}
+51
View File
@@ -0,0 +1,51 @@
package cmd
import (
"context"
"fmt"
"github.com/creasty/defaults"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/kubeshark/kubeshark/errormessage"
"github.com/kubeshark/kubeshark/misc"
"github.com/kubeshark/kubeshark/misc/fsUtils"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var logsCmd = &cobra.Command{
Use: "logs",
Short: "Create a ZIP file with logs for GitHub issues or troubleshooting",
RunE: func(cmd *cobra.Command, args []string) error {
kubernetesProvider, err := getKubernetesProviderForCli(false, false)
if err != nil {
return nil
}
ctx := context.Background()
if validationErr := config.Config.Logs.Validate(); validationErr != nil {
return errormessage.FormatError(validationErr)
}
log.Debug().Str("logs-path", config.Config.Logs.FilePath()).Msg("Using this logs path...")
if dumpLogsErr := fsUtils.DumpLogs(ctx, kubernetesProvider, config.Config.Logs.FilePath(), config.Config.Logs.Grep); dumpLogsErr != nil {
log.Error().Err(dumpLogsErr).Msg("Failed to dump logs.")
}
return nil
},
}
func init() {
rootCmd.AddCommand(logsCmd)
defaultLogsConfig := configStructs.LogsConfig{}
if err := defaults.Set(&defaultLogsConfig); err != nil {
log.Debug().Err(err).Send()
}
logsCmd.Flags().StringP(configStructs.FileLogsName, "f", defaultLogsConfig.FileStr, fmt.Sprintf("Path for zip file (default current <pwd>\\%s_logs.zip)", misc.Program))
logsCmd.Flags().StringP(configStructs.GrepLogsName, "g", defaultLogsConfig.Grep, "Regexp to do grepping on the logs")
}
+125
View File
@@ -0,0 +1,125 @@
package cmd
import (
"github.com/kubeshark/kubeshark/config"
"github.com/spf13/cobra"
)
var mcpURL string
var mcpKubeconfig string
var mcpListTools bool
var mcpConfig bool
var mcpAllowDestructive bool
var mcpCmd = &cobra.Command{
Use: "mcp",
Short: "Run MCP (Model Context Protocol) server for AI assistant integration",
Long: `Run an MCP server over stdio that exposes Kubeshark's L7 API visibility
to AI assistants like Claude Desktop.
TOOLS PROVIDED:
Cluster Management (work without Kubeshark running):
- check_kubeshark_status: Check if Kubeshark is running in the cluster
- start_kubeshark: Start Kubeshark to capture traffic
- stop_kubeshark: Stop Kubeshark and clean up resources
Traffic Analysis (require Kubeshark running):
- list_workloads: Discover pods, services, namespaces, and nodes with L7 traffic
- list_api_calls: Query L7 API transactions (HTTP, gRPC, etc.)
- get_api_call: Get detailed information about a specific API call
- get_api_stats: Get aggregated API statistics
CONFIGURATION:
To use with Claude Desktop, add to your claude_desktop_config.json
(typically at ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"kubeshark": {
"command": "/path/to/kubeshark",
"args": ["mcp", "--kubeconfig", "/Users/YOUR_USERNAME/.kube/config"]
}
}
}
DIRECT URL MODE:
If Kubeshark is already running and accessible via URL (e.g., exposed via ingress),
you can connect directly without needing kubectl/kubeconfig:
{
"mcpServers": {
"kubeshark": {
"command": "/path/to/kubeshark",
"args": ["mcp", "--url", "https://kubeshark.example.com"]
}
}
}
In URL mode, destructive tools (start/stop) are disabled since Kubeshark is
managed externally. The check_kubeshark_status tool remains available to confirm connectivity.
DESTRUCTIVE OPERATIONS:
By default, destructive operations (start_kubeshark, stop_kubeshark) are disabled
to prevent accidental cluster modifications. To enable them, use --allow-destructive:
{
"mcpServers": {
"kubeshark": {
"command": "/path/to/kubeshark",
"args": ["mcp", "--allow-destructive", "--kubeconfig", "/path/to/.kube/config"]
}
}
}
CUSTOM SETTINGS:
To use custom settings when starting Kubeshark, use the --set flag:
{
"mcpServers": {
"kubeshark": {
"command": "/path/to/kubeshark",
"args": ["mcp", "--set", "tap.docker.tag=v52.3"],
...
}
}
}
Multiple --set flags can be used for different settings.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Handle --mcp-config flag
if mcpConfig {
printMCPConfig(mcpURL, mcpKubeconfig)
return nil
}
// Set kubeconfig path if provided
if mcpKubeconfig != "" {
config.Config.Kube.ConfigPathStr = mcpKubeconfig
}
// Handle --list-tools flag
if mcpListTools {
listMCPTools(mcpURL)
return nil
}
setFlags, _ := cmd.Flags().GetStringSlice(config.SetCommandName)
runMCPWithConfig(setFlags, mcpURL, mcpAllowDestructive)
return nil
},
}
func init() {
rootCmd.AddCommand(mcpCmd)
mcpCmd.Flags().StringVar(&mcpURL, "url", "", "Direct URL to Kubeshark (e.g., https://kubeshark.example.com). When set, connects directly without kubectl/proxy and disables start/stop tools.")
mcpCmd.Flags().StringVar(&mcpKubeconfig, "kubeconfig", "", "Path to kubeconfig file (e.g., /Users/me/.kube/config)")
mcpCmd.Flags().BoolVar(&mcpListTools, "list-tools", false, "List available MCP tools and exit")
mcpCmd.Flags().BoolVar(&mcpConfig, "mcp-config", false, "Print MCP client configuration JSON and exit")
mcpCmd.Flags().BoolVar(&mcpAllowDestructive, "allow-destructive", false, "Enable destructive operations (start_kubeshark, stop_kubeshark). Without this flag, only read-only traffic analysis tools are available.")
}
+1231
View File
File diff suppressed because it is too large Load Diff
+688
View File
@@ -0,0 +1,688 @@
package cmd
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func newTestMCPServer() *mcpServer {
return &mcpServer{httpClient: &http.Client{}, stdin: &bytes.Buffer{}, stdout: &bytes.Buffer{}}
}
func sendRequest(s *mcpServer, method string, id any, params any) string {
req := jsonRPCRequest{
JSONRPC: "2.0",
ID: id,
Method: method,
}
if params != nil {
paramsBytes, _ := json.Marshal(params)
req.Params = paramsBytes
}
s.handleRequest(&req)
output := s.stdout.(*bytes.Buffer).String()
s.stdout.(*bytes.Buffer).Reset()
return output
}
func parseResponse(t *testing.T, output string) jsonRPCResponse {
var resp jsonRPCResponse
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &resp); err != nil {
t.Fatalf("Failed to parse response: %v\nOutput: %s", err, output)
}
return resp
}
func TestMCP_Initialize(t *testing.T) {
s := newTestMCPServer()
resp := parseResponse(t, sendRequest(s, "initialize", 1, nil))
if resp.ID != float64(1) || resp.Error != nil {
t.Fatalf("Expected ID 1 with no error, got ID=%v, error=%v", resp.ID, resp.Error)
}
result := resp.Result.(map[string]any)
if result["protocolVersion"] != "2024-11-05" {
t.Errorf("Expected protocolVersion 2024-11-05, got %v", result["protocolVersion"])
}
if result["serverInfo"].(map[string]any)["name"] != "kubeshark-mcp" {
t.Error("Expected server name kubeshark-mcp")
}
if !strings.Contains(result["instructions"].(string), "check_kubeshark_status") {
t.Error("Instructions should mention check_kubeshark_status")
}
if _, ok := result["capabilities"].(map[string]any)["prompts"]; !ok {
t.Error("Expected prompts capability")
}
}
func TestMCP_Ping(t *testing.T) {
resp := parseResponse(t, sendRequest(newTestMCPServer(), "ping", 42, nil))
if resp.ID != float64(42) || resp.Error != nil || len(resp.Result.(map[string]any)) != 0 {
t.Errorf("Expected ID 42, no error, empty result")
}
}
func TestMCP_InitializedNotification(t *testing.T) {
s := newTestMCPServer()
for _, method := range []string{"initialized", "notifications/initialized"} {
if output := sendRequest(s, method, nil, nil); output != "" {
t.Errorf("Expected no output for %s, got: %s", method, output)
}
}
}
func TestMCP_UnknownMethod(t *testing.T) {
resp := parseResponse(t, sendRequest(newTestMCPServer(), "unknown/method", 1, nil))
if resp.Error == nil || resp.Error.Code != -32601 {
t.Fatalf("Expected error code -32601, got %v", resp.Error)
}
}
func TestMCP_PromptsList(t *testing.T) {
resp := parseResponse(t, sendRequest(newTestMCPServer(), "prompts/list", 1, nil))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
prompts := resp.Result.(map[string]any)["prompts"].([]any)
if len(prompts) != 1 || prompts[0].(map[string]any)["name"] != "kubeshark_usage" {
t.Error("Expected 1 prompt named 'kubeshark_usage'")
}
}
func TestMCP_PromptsGet(t *testing.T) {
resp := parseResponse(t, sendRequest(newTestMCPServer(), "prompts/get", 1, map[string]any{"name": "kubeshark_usage"}))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
messages := resp.Result.(map[string]any)["messages"].([]any)
if len(messages) == 0 {
t.Fatal("Expected at least one message")
}
text := messages[0].(map[string]any)["content"].(map[string]any)["text"].(string)
for _, phrase := range []string{"check_kubeshark_status", "start_kubeshark", "stop_kubeshark"} {
if !strings.Contains(text, phrase) {
t.Errorf("Prompt should contain '%s'", phrase)
}
}
}
func TestMCP_PromptsGet_UnknownPrompt(t *testing.T) {
resp := parseResponse(t, sendRequest(newTestMCPServer(), "prompts/get", 1, map[string]any{"name": "unknown"}))
if resp.Error == nil || resp.Error.Code != -32602 {
t.Fatalf("Expected error code -32602, got %v", resp.Error)
}
}
func TestMCP_ToolsList_CLIOnly(t *testing.T) {
resp := parseResponse(t, sendRequest(newTestMCPServer(), "tools/list", 1, nil))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
tools := resp.Result.(map[string]any)["tools"].([]any)
// Should have check_kubeshark_status + get_file_url + download_file = 3 tools
if len(tools) != 3 {
t.Errorf("Expected 3 tools, got %d", len(tools))
}
toolNames := make(map[string]bool)
for _, tool := range tools {
toolNames[tool.(map[string]any)["name"].(string)] = true
}
for _, expected := range []string{"check_kubeshark_status", "get_file_url", "download_file"} {
if !toolNames[expected] {
t.Errorf("Missing expected tool: %s", expected)
}
}
}
func TestMCP_ToolsList_WithDestructive(t *testing.T) {
s := &mcpServer{httpClient: &http.Client{}, stdin: &bytes.Buffer{}, stdout: &bytes.Buffer{}, allowDestructive: true}
resp := parseResponse(t, sendRequest(s, "tools/list", 1, nil))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
tools := resp.Result.(map[string]any)["tools"].([]any)
toolNames := make(map[string]bool)
for _, tool := range tools {
toolNames[tool.(map[string]any)["name"].(string)] = true
}
for _, expected := range []string{"check_kubeshark_status", "start_kubeshark", "stop_kubeshark"} {
if !toolNames[expected] {
t.Errorf("Missing expected tool: %s", expected)
}
}
}
func TestMCP_ToolsList_WithHubBackend(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" || r.URL.Path == "" {
_, _ = w.Write([]byte(`{"name":"hub","tools":[{"name":"list_workloads","description":"","inputSchema":{}},{"name":"list_api_calls","description":"","inputSchema":{}}]}`))
}
}))
defer mockServer.Close()
s := &mcpServer{httpClient: &http.Client{}, stdin: &bytes.Buffer{}, stdout: &bytes.Buffer{}, hubBaseURL: mockServer.URL, backendInitialized: true, allowDestructive: true}
resp := parseResponse(t, sendRequest(s, "tools/list", 1, nil))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
tools := resp.Result.(map[string]any)["tools"].([]any)
// Should have CLI tools (3) + file tools (2) + Hub tools (2) = 7 tools
if len(tools) < 7 {
t.Errorf("Expected at least 7 tools, got %d", len(tools))
}
}
func TestMCP_ToolsCallUnknownTool(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "unknown"}))
if !resp.Result.(map[string]any)["isError"].(bool) {
t.Error("Expected isError=true for unknown tool")
}
}
func TestMCP_ToolsCallInvalidParams(t *testing.T) {
s := newTestMCPServer()
req := jsonRPCRequest{JSONRPC: "2.0", ID: 1, Method: "tools/call", Params: json.RawMessage(`"invalid"`)}
s.handleRequest(&req)
resp := parseResponse(t, s.stdout.(*bytes.Buffer).String())
if resp.Error == nil || resp.Error.Code != -32602 {
t.Fatalf("Expected error code -32602")
}
}
func TestMCP_CheckKubesharkStatus(t *testing.T) {
for _, tc := range []struct {
name string
args map[string]any
}{
{"no_config", map[string]any{}},
{"with_namespace", map[string]any{"release_namespace": "custom-ns"}},
} {
t.Run(tc.name, func(t *testing.T) {
resp := parseResponse(t, sendRequest(newTestMCPServer(), "tools/call", 1, mcpCallToolParams{Name: "check_kubeshark_status", Arguments: tc.args}))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
content := resp.Result.(map[string]any)["content"].([]any)
if len(content) == 0 || content[0].(map[string]any)["text"].(string) == "" {
t.Error("Expected non-empty response")
}
})
}
}
func newTestMCPServerWithMockBackend(handler http.HandlerFunc) (*mcpServer, *httptest.Server) {
mockServer := httptest.NewServer(handler)
return &mcpServer{httpClient: &http.Client{}, stdin: &bytes.Buffer{}, stdout: &bytes.Buffer{}, hubBaseURL: mockServer.URL, backendInitialized: true}, mockServer
}
type hubToolCallRequest struct {
Tool string `json:"name"`
Arguments map[string]any `json:"arguments"`
}
func newMockHubHandler(t *testing.T, handler func(req hubToolCallRequest) (string, int)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tools/call" || r.Method != http.MethodPost {
w.WriteHeader(http.StatusNotFound)
return
}
var req hubToolCallRequest
_ = json.NewDecoder(r.Body).Decode(&req)
resp, status := handler(req)
w.WriteHeader(status)
_, _ = w.Write([]byte(resp))
}
}
func TestMCP_ListWorkloads(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(newMockHubHandler(t, func(req hubToolCallRequest) (string, int) {
if req.Tool != "list_workloads" {
t.Errorf("Expected tool 'list_workloads', got %s", req.Tool)
}
return `{"workloads": [{"name": "test-pod"}]}`, http.StatusOK
}))
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "list_workloads", Arguments: map[string]any{"type": "pod"}}))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
text := resp.Result.(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string)
if !strings.Contains(text, "test-pod") {
t.Errorf("Expected 'test-pod' in response")
}
}
func TestMCP_ListAPICalls(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(newMockHubHandler(t, func(req hubToolCallRequest) (string, int) {
if req.Tool != "list_api_calls" {
t.Errorf("Expected tool 'list_api_calls', got %s", req.Tool)
}
return `{"calls": [{"id": "123", "path": "/api/users"}]}`, http.StatusOK
}))
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "list_api_calls", Arguments: map[string]any{"proto": "http"}}))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
if !strings.Contains(resp.Result.(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string), "/api/users") {
t.Error("Expected '/api/users' in response")
}
}
func TestMCP_GetAPICall(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(newMockHubHandler(t, func(req hubToolCallRequest) (string, int) {
if req.Tool != "get_api_call" || req.Arguments["id"] != "abc123" {
t.Errorf("Expected get_api_call with id=abc123")
}
return `{"id": "abc123", "path": "/api/orders"}`, http.StatusOK
}))
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "get_api_call", Arguments: map[string]any{"id": "abc123"}}))
if resp.Error != nil || !strings.Contains(resp.Result.(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string), "abc123") {
t.Error("Expected response containing 'abc123'")
}
}
func TestMCP_GetAPICall_MissingID(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(newMockHubHandler(t, func(req hubToolCallRequest) (string, int) {
return `{"error": "id is required"}`, http.StatusBadRequest
}))
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "get_api_call", Arguments: map[string]any{}}))
if !resp.Result.(map[string]any)["isError"].(bool) {
t.Error("Expected isError=true")
}
}
func TestMCP_GetAPIStats(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(newMockHubHandler(t, func(req hubToolCallRequest) (string, int) {
if req.Tool != "get_api_stats" {
t.Errorf("Expected get_api_stats")
}
return `{"stats": {"total_calls": 1000}}`, http.StatusOK
}))
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "get_api_stats", Arguments: map[string]any{"ns": "prod"}}))
if resp.Error != nil || !strings.Contains(resp.Result.(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string), "total_calls") {
t.Error("Expected 'total_calls' in response")
}
}
func TestMCP_APITools_BackendError(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "list_workloads"}))
if !resp.Result.(map[string]any)["isError"].(bool) {
t.Error("Expected isError=true for backend error")
}
}
func TestMCP_APITools_BackendConnectionError(t *testing.T) {
s := &mcpServer{httpClient: &http.Client{}, stdin: &bytes.Buffer{}, stdout: &bytes.Buffer{}, hubBaseURL: "http://localhost:99999", backendInitialized: true}
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "list_workloads"}))
if !resp.Result.(map[string]any)["isError"].(bool) {
t.Error("Expected isError=true for connection error")
}
}
func TestMCP_RunLoop_ParseError(t *testing.T) {
output := &bytes.Buffer{}
s := &mcpServer{httpClient: &http.Client{}, stdin: strings.NewReader("invalid\n"), stdout: output}
s.run()
if resp := parseResponse(t, output.String()); resp.Error == nil || resp.Error.Code != -32700 {
t.Fatalf("Expected error code -32700")
}
}
func TestMCP_RunLoop_MultipleRequests(t *testing.T) {
output := &bytes.Buffer{}
s := &mcpServer{httpClient: &http.Client{}, stdin: strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"ping"}
{"jsonrpc":"2.0","id":2,"method":"ping"}
`), stdout: output}
s.run()
if lines := strings.Split(strings.TrimSpace(output.String()), "\n"); len(lines) != 2 {
t.Fatalf("Expected 2 responses, got %d", len(lines))
}
}
func TestMCP_RunLoop_EmptyLines(t *testing.T) {
output := &bytes.Buffer{}
s := &mcpServer{httpClient: &http.Client{}, stdin: strings.NewReader("\n\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\n"), stdout: output}
s.run()
if lines := strings.Split(strings.TrimSpace(output.String()), "\n"); len(lines) != 1 {
t.Fatalf("Expected 1 response, got %d", len(lines))
}
}
func TestMCP_ResponseFormat(t *testing.T) {
s := newTestMCPServer()
// Numeric ID
if resp := parseResponse(t, sendRequest(s, "ping", 123, nil)); resp.ID != float64(123) || resp.JSONRPC != "2.0" {
t.Errorf("Expected ID 123 and jsonrpc 2.0")
}
// String ID
if resp := parseResponse(t, sendRequest(s, "ping", "str", nil)); resp.ID != "str" {
t.Errorf("Expected ID 'str'")
}
}
func TestMCP_ToolCallResult_ContentFormat(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"data": "test"}`))
})
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "list_workloads"}))
content := resp.Result.(map[string]any)["content"].([]any)
if len(content) == 0 || content[0].(map[string]any)["type"] != "text" {
t.Error("Expected content with type=text")
}
}
func TestMCP_CommandArgs(t *testing.T) {
// Test start command args building
for _, tc := range []struct {
args map[string]any
expected string
}{
{map[string]any{}, "tap --set headless=true"},
{map[string]any{"pod_regex": "nginx.*"}, "tap nginx.* --set headless=true"},
{map[string]any{"namespaces": "default"}, "tap -n default --set headless=true"},
{map[string]any{"release_namespace": "ks"}, "tap -s ks --set headless=true"},
} {
cmdArgs := []string{"tap"}
if v, _ := tc.args["pod_regex"].(string); v != "" {
cmdArgs = append(cmdArgs, v)
}
if v, _ := tc.args["namespaces"].(string); v != "" {
for ns := range strings.SplitSeq(v, ",") {
cmdArgs = append(cmdArgs, "-n", strings.TrimSpace(ns))
}
}
if v, _ := tc.args["release_namespace"].(string); v != "" {
cmdArgs = append(cmdArgs, "-s", v)
}
cmdArgs = append(cmdArgs, "--set", "headless=true")
if got := strings.Join(cmdArgs, " "); got != tc.expected {
t.Errorf("Expected %q, got %q", tc.expected, got)
}
}
}
func TestMCP_PrettyPrintJSON(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"key":"value"}`))
})
defer mockServer.Close()
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "list_workloads"}))
text := resp.Result.(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string)
if !strings.Contains(text, "\n") {
t.Error("Expected pretty-printed JSON")
}
}
func TestMCP_SpecialCharsAndEdgeCases(t *testing.T) {
s, mockServer := newTestMCPServerWithMockBackend(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{}`))
})
defer mockServer.Close()
// Test special chars, empty args, nil args
for _, args := range []map[string]any{
{"path": "/api?id=123"},
{"id": "abc/123"},
{},
nil,
} {
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{Name: "list_workloads", Arguments: args}))
if resp.Error != nil {
t.Errorf("Unexpected error with args %v: %v", args, resp.Error)
}
}
}
func TestMCP_BackendInitialization_Concurrent(t *testing.T) {
s := newTestMCPServer()
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func() { s.ensureBackendConnection(); done <- true }()
}
for i := 0; i < 10; i++ {
<-done
}
}
func TestMCP_GetFileURL_ProxyMode(t *testing.T) {
s := &mcpServer{
httpClient: &http.Client{},
stdin: &bytes.Buffer{},
stdout: &bytes.Buffer{},
hubBaseURL: "http://127.0.0.1:8899/api/mcp",
backendInitialized: true,
}
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{
Name: "get_file_url",
Arguments: map[string]any{"path": "/snapshots/abc/data.pcap"},
}))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
text := resp.Result.(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string)
expected := "http://127.0.0.1:8899/api/snapshots/abc/data.pcap"
if text != expected {
t.Errorf("Expected %q, got %q", expected, text)
}
}
func TestMCP_GetFileURL_URLMode(t *testing.T) {
s := &mcpServer{
httpClient: &http.Client{},
stdin: &bytes.Buffer{},
stdout: &bytes.Buffer{},
hubBaseURL: "https://kubeshark.example.com/api/mcp",
backendInitialized: true,
urlMode: true,
directURL: "https://kubeshark.example.com",
}
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{
Name: "get_file_url",
Arguments: map[string]any{"path": "/snapshots/xyz/export.pcap"},
}))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
text := resp.Result.(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string)
expected := "https://kubeshark.example.com/api/snapshots/xyz/export.pcap"
if text != expected {
t.Errorf("Expected %q, got %q", expected, text)
}
}
func TestMCP_GetFileURL_MissingPath(t *testing.T) {
s := &mcpServer{
httpClient: &http.Client{},
stdin: &bytes.Buffer{},
stdout: &bytes.Buffer{},
hubBaseURL: "http://127.0.0.1:8899/api/mcp",
backendInitialized: true,
}
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{
Name: "get_file_url",
Arguments: map[string]any{},
}))
result := resp.Result.(map[string]any)
if !result["isError"].(bool) {
t.Error("Expected isError=true when path is missing")
}
text := result["content"].([]any)[0].(map[string]any)["text"].(string)
if !strings.Contains(text, "path") {
t.Error("Error message should mention 'path'")
}
}
func TestMCP_DownloadFile(t *testing.T) {
fileContent := "test pcap data content"
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/snapshots/abc/data.pcap" {
_, _ = w.Write([]byte(fileContent))
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer mockServer.Close()
// Use temp dir for download destination
tmpDir := t.TempDir()
dest := filepath.Join(tmpDir, "downloaded.pcap")
s := &mcpServer{
httpClient: &http.Client{},
stdin: &bytes.Buffer{},
stdout: &bytes.Buffer{},
hubBaseURL: mockServer.URL + "/api/mcp",
backendInitialized: true,
}
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{
Name: "download_file",
Arguments: map[string]any{"path": "/snapshots/abc/data.pcap", "dest": dest},
}))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
result := resp.Result.(map[string]any)
if result["isError"] != nil && result["isError"].(bool) {
t.Fatalf("Expected no error, got: %v", result["content"])
}
text := result["content"].([]any)[0].(map[string]any)["text"].(string)
var downloadResult map[string]any
if err := json.Unmarshal([]byte(text), &downloadResult); err != nil {
t.Fatalf("Failed to parse download result JSON: %v", err)
}
if downloadResult["path"] != dest {
t.Errorf("Expected path %q, got %q", dest, downloadResult["path"])
}
if downloadResult["size"].(float64) != float64(len(fileContent)) {
t.Errorf("Expected size %d, got %v", len(fileContent), downloadResult["size"])
}
// Verify the file was actually written
content, err := os.ReadFile(dest)
if err != nil {
t.Fatalf("Failed to read downloaded file: %v", err)
}
if string(content) != fileContent {
t.Errorf("Expected file content %q, got %q", fileContent, string(content))
}
}
func TestMCP_DownloadFile_CustomDest(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("data"))
}))
defer mockServer.Close()
tmpDir := t.TempDir()
customDest := filepath.Join(tmpDir, "custom-name.pcap")
s := &mcpServer{
httpClient: &http.Client{},
stdin: &bytes.Buffer{},
stdout: &bytes.Buffer{},
hubBaseURL: mockServer.URL + "/api/mcp",
backendInitialized: true,
}
resp := parseResponse(t, sendRequest(s, "tools/call", 1, mcpCallToolParams{
Name: "download_file",
Arguments: map[string]any{"path": "/snapshots/abc/export.pcap", "dest": customDest},
}))
result := resp.Result.(map[string]any)
if result["isError"] != nil && result["isError"].(bool) {
t.Fatalf("Expected no error, got: %v", result["content"])
}
text := result["content"].([]any)[0].(map[string]any)["text"].(string)
var downloadResult map[string]any
if err := json.Unmarshal([]byte(text), &downloadResult); err != nil {
t.Fatalf("Failed to parse download result JSON: %v", err)
}
if downloadResult["path"] != customDest {
t.Errorf("Expected path %q, got %q", customDest, downloadResult["path"])
}
if _, err := os.Stat(customDest); os.IsNotExist(err) {
t.Error("Expected file to exist at custom destination")
}
}
func TestMCP_ToolsList_IncludesFileTools(t *testing.T) {
s := newTestMCPServer()
resp := parseResponse(t, sendRequest(s, "tools/list", 1, nil))
if resp.Error != nil {
t.Fatalf("Unexpected error: %v", resp.Error)
}
tools := resp.Result.(map[string]any)["tools"].([]any)
toolNames := make(map[string]bool)
for _, tool := range tools {
toolNames[tool.(map[string]any)["name"].(string)] = true
}
for _, expected := range []string{"get_file_url", "download_file"} {
if !toolNames[expected] {
t.Errorf("Missing expected tool: %s", expected)
}
}
}
func TestMCP_FullConversation(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
_, _ = w.Write([]byte(`{"name":"hub","tools":[{"name":"list_workloads","description":"","inputSchema":{}}]}`))
} else if r.URL.Path == "/tools/call" {
_, _ = w.Write([]byte(`{"data":"ok"}`))
}
}))
defer mockServer.Close()
input := `{"jsonrpc":"2.0","id":1,"method":"initialize"}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_workloads","arguments":{}}}
`
output := &bytes.Buffer{}
s := &mcpServer{httpClient: &http.Client{}, stdin: strings.NewReader(input), stdout: output, hubBaseURL: mockServer.URL, backendInitialized: true}
s.run()
lines := strings.Split(strings.TrimSpace(output.String()), "\n")
if len(lines) != 3 { // 3 responses (notification has no response)
t.Errorf("Expected 3 responses, got %d", len(lines))
}
for i, line := range lines {
var resp jsonRPCResponse
if err := json.Unmarshal([]byte(line), &resp); err != nil || resp.Error != nil {
t.Errorf("Response %d: parse error or unexpected error", i)
}
}
}
+110
View File
@@ -0,0 +1,110 @@
package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/creasty/defaults"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
// pcapDumpCmd represents the consolidated pcapdump command
var pcapDumpCmd = &cobra.Command{
Use: "pcapdump",
Short: "Store all captured traffic (including decrypted TLS) in a PCAP file.",
RunE: func(cmd *cobra.Command, args []string) error {
// Retrieve the kubeconfig path from the flag
kubeconfig, _ := cmd.Flags().GetString(configStructs.PcapKubeconfig)
// If kubeconfig is not provided, use the default location
if kubeconfig == "" {
if home := homedir.HomeDir(); home != "" {
kubeconfig = filepath.Join(home, ".kube", "config")
} else {
return errors.New("kubeconfig flag not provided and no home directory available for default config location")
}
}
debugEnabled, _ := cmd.Flags().GetBool("debug")
if debugEnabled {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
log.Debug().Msg("Debug logging enabled")
} else {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
// Use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return fmt.Errorf("Error building kubeconfig: %w", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return fmt.Errorf("Error creating Kubernetes client: %w", err)
}
// Parse the `--time` flag
timeIntervalStr, _ := cmd.Flags().GetString("time")
var cutoffTime *time.Time // Use a pointer to distinguish between provided and not provided
if timeIntervalStr != "" {
duration, err := time.ParseDuration(timeIntervalStr)
if err != nil {
return fmt.Errorf("Invalid format %w", err)
}
tempCutoffTime := time.Now().Add(-duration)
cutoffTime = &tempCutoffTime
}
// Test the dest dir if provided
destDir, _ := cmd.Flags().GetString(configStructs.PcapDest)
if destDir != "" {
info, err := os.Stat(destDir)
if os.IsNotExist(err) {
return fmt.Errorf("Directory does not exist: %s", destDir)
}
if err != nil {
return fmt.Errorf("Error checking dest directory: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("Dest path is not a directory: %s", destDir)
}
tempFile, err := os.CreateTemp(destDir, "write-test-*")
if err != nil {
return fmt.Errorf("Directory %s is not writable", destDir)
}
_ = os.Remove(tempFile.Name())
}
log.Info().Msg("Copying PCAP files")
err = copyPcapFiles(clientset, config, destDir, cutoffTime)
if err != nil {
return err
}
return nil
},
}
func init() {
rootCmd.AddCommand(pcapDumpCmd)
defaultPcapDumpConfig := configStructs.PcapDumpConfig{}
if err := defaults.Set(&defaultPcapDumpConfig); err != nil {
log.Debug().Err(err).Send()
}
pcapDumpCmd.Flags().String(configStructs.PcapTime, "", "Time interval (e.g., 10m, 1h) in the past for which the pcaps are copied")
pcapDumpCmd.Flags().String(configStructs.PcapDest, "", "Local destination path for copied PCAP files (can not be used together with --enabled)")
pcapDumpCmd.Flags().String(configStructs.PcapKubeconfig, "", "Path for kubeconfig (if not provided the default location will be checked)")
pcapDumpCmd.Flags().Bool("debug", false, "Enable debug logging")
}
+370
View File
@@ -0,0 +1,370 @@
package cmd
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/kubeshark/gopacket/pcapgo"
"github.com/rs/zerolog/log"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
)
const (
label = "app.kubeshark.com/app=worker"
srcDir = "pcapdump"
maxSnaplen uint32 = 262144
maxTimePerFile = time.Minute * 5
)
// PodFileInfo represents information about a pod, its namespace, and associated files
type PodFileInfo struct {
Pod corev1.Pod
SrcDir string
Files []string
CopiedFiles []string
}
// listWorkerPods fetches all worker pods from multiple namespaces
func listWorkerPods(ctx context.Context, clientset *kubernetes.Clientset, namespaces []string) ([]*PodFileInfo, error) {
var podFileInfos []*PodFileInfo
var errs []error
labelSelector := label
for _, namespace := range namespaces {
// List all pods matching the label in the current namespace
pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
errs = append(errs, fmt.Errorf("failed to list worker pods in namespace %s: %w", namespace, err))
continue
}
for _, pod := range pods.Items {
podFileInfos = append(podFileInfos, &PodFileInfo{
Pod: pod,
})
}
}
return podFileInfos, errors.Join(errs...)
}
// listFilesInPodDir lists all files in the specified directory inside the pod across multiple namespaces
func listFilesInPodDir(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, pod *PodFileInfo, cutoffTime *time.Time) error {
nodeName := pod.Pod.Spec.NodeName
srcFilePath := filepath.Join("data", nodeName, srcDir)
cmd := []string{"ls", srcFilePath}
req := clientset.CoreV1().RESTClient().Post().
Resource("pods").
Name(pod.Pod.Name).
Namespace(pod.Pod.Namespace).
SubResource("exec").
Param("container", "sniffer").
Param("stdout", "true").
Param("stderr", "true").
Param("command", cmd[0]).
Param("command", cmd[1])
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
return err
}
var stdoutBuf bytes.Buffer
var stderrBuf bytes.Buffer
// Execute the command to list files
err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{
Stdout: &stdoutBuf,
Stderr: &stderrBuf,
})
if err != nil {
return err
}
// Split the output (file names) into a list
files := strings.Split(strings.TrimSpace(stdoutBuf.String()), "\n")
if len(files) == 0 {
// No files were found in the target dir for this pod
return nil
}
var filteredFiles []string
var fileProcessingErrs []error
// Filter files based on cutoff time if provided
for _, file := range files {
if cutoffTime != nil {
parts := strings.Split(file, "-")
if len(parts) < 2 {
continue
}
timestampStr := parts[len(parts)-2] + parts[len(parts)-1][:6] // Extract YYYYMMDDHHMMSS
fileTime, err := time.Parse("20060102150405", timestampStr)
if err != nil {
fileProcessingErrs = append(fileProcessingErrs, fmt.Errorf("failed parse file timestamp %s: %w", file, err))
continue
}
if fileTime.Before(*cutoffTime) {
continue
}
}
// Add file to filtered list
filteredFiles = append(filteredFiles, file)
}
pod.SrcDir = srcDir
pod.Files = filteredFiles
return errors.Join(fileProcessingErrs...)
}
// copyFileFromPod copies a single file from a pod to a local destination
func copyFileFromPod(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, pod *PodFileInfo, srcFile, destFile string) error {
// Construct the complete path using /data, the node name, srcDir, and srcFile
nodeName := pod.Pod.Spec.NodeName
srcFilePath := filepath.Join("data", nodeName, srcDir, srcFile)
// Execute the `cat` command to read the file at the srcFilePath
cmd := []string{"cat", srcFilePath}
req := clientset.CoreV1().RESTClient().Post().
Resource("pods").
Name(pod.Pod.Name).
Namespace(pod.Pod.Namespace).
SubResource("exec").
Param("container", "sniffer").
Param("stdout", "true").
Param("stderr", "true").
Param("command", cmd[0]).
Param("command", cmd[1])
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
return fmt.Errorf("failed to initialize executor for pod %s in namespace %s: %w", pod.Pod.Name, pod.Pod.Namespace, err)
}
// Create the local file to write the content to
outFile, err := os.Create(destFile)
if err != nil {
return fmt.Errorf("failed to create destination file: %w", err)
}
defer outFile.Close()
// Capture stderr for error logging
var stderrBuf bytes.Buffer
// Stream the file content from the pod to the local file
err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{
Stdout: outFile,
Stderr: &stderrBuf,
})
if err != nil {
return err
}
return nil
}
func mergePCAPs(outputFile string, inputFiles []string) error {
// Create the output file
f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
bufWriter := bufio.NewWriterSize(f, 4*1024*1024)
defer bufWriter.Flush()
// Create the PCAP writer
writer := pcapgo.NewWriter(bufWriter)
err = writer.WriteFileHeader(maxSnaplen, 1)
if err != nil {
return fmt.Errorf("failed to write PCAP file header: %w", err)
}
var mergingErrs []error
for _, inputFile := range inputFiles {
// Open the input file
file, err := os.Open(inputFile)
if err != nil {
mergingErrs = append(mergingErrs, fmt.Errorf("failed to open %s: %w", inputFile, err))
continue
}
fileInfo, err := file.Stat()
if err != nil {
mergingErrs = append(mergingErrs, fmt.Errorf("failed to stat file %s: %w", inputFile, err))
file.Close()
continue
}
if fileInfo.Size() == 0 {
// Skip empty files
log.Debug().Msgf("Skipped empty file: %s", inputFile)
file.Close()
continue
}
// Create the PCAP reader for the input file
reader, err := pcapgo.NewReader(file)
if err != nil {
mergingErrs = append(mergingErrs, fmt.Errorf("failed to create pcapng reader for %v: %w", file.Name(), err))
file.Close()
continue
}
for {
// Read packet data
data, ci, err := reader.ReadPacketData()
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
break
}
mergingErrs = append(mergingErrs, fmt.Errorf("error reading packet from file %s: %w", file.Name(), err))
break
}
// Write the packet to the output file
err = writer.WritePacket(ci, data)
if err != nil {
log.Error().Err(err).Msgf("Error writing packet to output file")
mergingErrs = append(mergingErrs, fmt.Errorf("error writing packet to output file: %w", err))
break
}
}
file.Close()
}
log.Debug().Err(errors.Join(mergingErrs...))
return nil
}
func copyPcapFiles(clientset *kubernetes.Clientset, config *rest.Config, destDir string, cutoffTime *time.Time) error {
// List all namespaces
namespaceList, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
var targetNamespaces []string
for _, ns := range namespaceList.Items {
targetNamespaces = append(targetNamespaces, ns.Name)
}
// List all worker pods
workerPods, err := listWorkerPods(context.Background(), clientset, targetNamespaces)
if err != nil {
if len(workerPods) == 0 {
return err
}
log.Debug().Err(err).Msg("error while listing worker pods")
}
var wg sync.WaitGroup
// Launch a goroutine for each pod
for _, pod := range workerPods {
wg.Add(1)
go func(pod *PodFileInfo) {
defer wg.Done()
// List files for the current pod
err := listFilesInPodDir(context.Background(), clientset, config, pod, cutoffTime)
if err != nil {
log.Debug().Err(err).Msgf("error listing files in pod %s", pod.Pod.Name)
return
}
// Copy files from the pod
for _, file := range pod.Files {
destFile := filepath.Join(destDir, file)
// Add a timeout context for file copy
ctx, cancel := context.WithTimeout(context.Background(), maxTimePerFile)
err := copyFileFromPod(ctx, clientset, config, pod, file, destFile)
cancel()
if err != nil {
log.Debug().Err(err).Msgf("error copying file %s from pod %s in namespace %s", file, pod.Pod.Name, pod.Pod.Namespace)
continue
}
log.Info().Msgf("Copied file %s from pod %s to %s", file, pod.Pod.Name, destFile)
pod.CopiedFiles = append(pod.CopiedFiles, destFile)
}
}(pod)
}
// Wait for all goroutines to complete
wg.Wait()
var copiedFiles []string
for _, pod := range workerPods {
copiedFiles = append(copiedFiles, pod.CopiedFiles...)
}
if len(copiedFiles) == 0 {
log.Info().Msg("No pcaps available to copy on the workers")
return nil
}
// Generate a temporary filename for the merged file
tempMergedFile := copiedFiles[0] + "_temp"
// Merge PCAP files
err = mergePCAPs(tempMergedFile, copiedFiles)
if err != nil {
os.Remove(tempMergedFile)
return fmt.Errorf("error merging files: %w", err)
}
// Remove the original files after merging
for _, file := range copiedFiles {
if err = os.Remove(file); err != nil {
log.Debug().Err(err).Msgf("error removing file %s", file)
}
}
clusterID, err := getClusterID(clientset)
if err != nil {
return fmt.Errorf("failed to get cluster ID: %w", err)
}
timestamp := time.Now().Format("2006-01-02_15-04")
// Rename the temp file to the final name
finalMergedFile := filepath.Join(destDir, fmt.Sprintf("%s-%s.pcap", clusterID, timestamp))
err = os.Rename(tempMergedFile, finalMergedFile)
if err != nil {
return err
}
log.Info().Msgf("Merged file created: %s", finalMergedFile)
return nil
}
func getClusterID(clientset *kubernetes.Clientset) (string, error) {
namespace, err := clientset.CoreV1().Namespaces().Get(context.TODO(), "kube-system", metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("failed to get kube-system namespace UID: %w", err)
}
return string(namespace.UID), nil
}
+32
View File
@@ -0,0 +1,32 @@
package cmd
import (
"github.com/creasty/defaults"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var pprofCmd = &cobra.Command{
Use: "pprof",
Short: "Select a Kubeshark container and open the pprof web UI in the browser",
RunE: func(cmd *cobra.Command, args []string) error {
runPprof()
return nil
},
}
func init() {
rootCmd.AddCommand(pprofCmd)
defaultTapConfig := configStructs.TapConfig{}
if err := defaults.Set(&defaultTapConfig); err != nil {
log.Debug().Err(err).Send()
}
pprofCmd.Flags().Uint16(configStructs.ProxyFrontPortLabel, defaultTapConfig.Proxy.Front.Port, "Provide a custom port for the proxy/port-forward")
pprofCmd.Flags().String(configStructs.ProxyHostLabel, defaultTapConfig.Proxy.Host, "Provide a custom host for the proxy/port-forward")
pprofCmd.Flags().StringP(configStructs.ReleaseNamespaceLabel, "s", defaultTapConfig.Release.Namespace, "Release namespace of Kubeshark")
pprofCmd.Flags().Uint16(configStructs.PprofPortLabel, defaultTapConfig.Pprof.Port, "Provide a custom port for the pprof server")
pprofCmd.Flags().String(configStructs.PprofViewLabel, defaultTapConfig.Pprof.View, "Change the default view of the pprof web interface")
}
+176
View File
@@ -0,0 +1,176 @@
package cmd
import (
"context"
"fmt"
"github.com/go-cmd/cmd"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/kubernetes"
"github.com/kubeshark/kubeshark/utils"
"github.com/rivo/tview"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
)
func runPprof() {
runProxy(false, true)
provider, err := getKubernetesProviderForCli(false, false)
if err != nil {
return
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hubPods, err := provider.ListPodsByAppLabel(ctx, config.Config.Tap.Release.Namespace, map[string]string{kubernetes.AppLabelKey: "hub"})
if err != nil {
log.Error().
Err(err).
Msg("Failed to list hub pods!")
cancel()
return
}
workerPods, err := provider.ListPodsByAppLabel(ctx, config.Config.Tap.Release.Namespace, map[string]string{kubernetes.AppLabelKey: "worker"})
if err != nil {
log.Error().
Err(err).
Msg("Failed to list worker pods!")
cancel()
return
}
fullscreen := true
app := tview.NewApplication()
list := tview.NewList()
var currentCmd *cmd.Cmd
i := 48
for _, pod := range hubPods {
for _, container := range pod.Spec.Containers {
log.Info().Str("pod", pod.Name).Str("container", container.Name).Send()
homeUrl := fmt.Sprintf("%s/debug/pprof/", kubernetes.GetHubUrl())
modal := buildNewModal(
pod,
container,
homeUrl,
app,
list,
fullscreen,
currentCmd,
)
list.AddItem(fmt.Sprintf("pod: %s container: %s", pod.Name, container.Name), pod.Spec.NodeName, rune(i), func() {
app.SetRoot(modal, fullscreen)
})
i++
}
}
for _, pod := range workerPods {
for _, container := range pod.Spec.Containers {
log.Info().Str("pod", pod.Name).Str("container", container.Name).Send()
homeUrl := fmt.Sprintf("%s/pprof/%s/%s/", kubernetes.GetHubUrl(), pod.Status.HostIP, container.Name)
modal := buildNewModal(
pod,
container,
homeUrl,
app,
list,
fullscreen,
currentCmd,
)
list.AddItem(fmt.Sprintf("pod: %s container: %s", pod.Name, container.Name), pod.Spec.NodeName, rune(i), func() {
app.SetRoot(modal, fullscreen)
})
i++
}
}
list.AddItem("Quit", "Press to exit", 'q', func() {
if currentCmd != nil {
err = currentCmd.Stop()
if err != nil {
log.Error().Err(err).Str("name", currentCmd.Name).Msg("Failed to stop process!")
}
}
app.Stop()
})
if err := app.SetRoot(list, fullscreen).EnableMouse(true).Run(); err != nil {
panic(err)
}
}
func buildNewModal(
pod v1.Pod,
container v1.Container,
homeUrl string,
app *tview.Application,
list *tview.List,
fullscreen bool,
currentCmd *cmd.Cmd,
) *tview.Modal {
return tview.NewModal().
SetText(fmt.Sprintf("pod: %s container: %s", pod.Name, container.Name)).
AddButtons([]string{
"Open Debug Home Page",
"Profile: CPU",
"Profile: Memory",
"Profile: Goroutine",
"Cancel",
}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
var err error
port := fmt.Sprintf(":%d", config.Config.Tap.Pprof.Port)
view := fmt.Sprintf("http://localhost%s/ui/%s", port, config.Config.Tap.Pprof.View)
switch buttonLabel {
case "Open Debug Home Page":
utils.OpenBrowser(homeUrl)
case "Profile: CPU":
if currentCmd != nil {
err = currentCmd.Stop()
if err != nil {
log.Error().Err(err).Str("name", currentCmd.Name).Msg("Failed to stop process!")
}
}
currentCmd = cmd.NewCmd("go", "tool", "pprof", "-http", port, "-no_browser", fmt.Sprintf("%sprofile", homeUrl))
currentCmd.Start()
utils.OpenBrowser(view)
case "Profile: Memory":
if currentCmd != nil {
err = currentCmd.Stop()
if err != nil {
log.Error().Err(err).Str("name", currentCmd.Name).Msg("Failed to stop process!")
}
}
currentCmd = cmd.NewCmd("go", "tool", "pprof", "-http", port, "-no_browser", fmt.Sprintf("%sheap", homeUrl))
currentCmd.Start()
utils.OpenBrowser(view)
case "Profile: Goroutine":
if currentCmd != nil {
err = currentCmd.Stop()
if err != nil {
log.Error().Err(err).Str("name", currentCmd.Name).Msg("Failed to stop process!")
}
}
currentCmd = cmd.NewCmd("go", "tool", "pprof", "-http", port, "-no_browser", fmt.Sprintf("%sgoroutine", homeUrl))
currentCmd.Start()
utils.OpenBrowser(view)
case "Cancel":
if currentCmd != nil {
err = currentCmd.Stop()
if err != nil {
log.Error().Err(err).Str("name", currentCmd.Name).Msg("Failed to stop process!")
}
}
fallthrough
default:
app.SetRoot(list, fullscreen)
}
})
}
+30
View File
@@ -0,0 +1,30 @@
package cmd
import (
"github.com/creasty/defaults"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var proxyCmd = &cobra.Command{
Use: "proxy",
Short: "Open the web UI (front-end) in the browser via proxy/port-forward",
RunE: func(cmd *cobra.Command, args []string) error {
runProxy(true, false)
return nil
},
}
func init() {
rootCmd.AddCommand(proxyCmd)
defaultTapConfig := configStructs.TapConfig{}
if err := defaults.Set(&defaultTapConfig); err != nil {
log.Debug().Err(err).Send()
}
proxyCmd.Flags().Uint16(configStructs.ProxyFrontPortLabel, defaultTapConfig.Proxy.Front.Port, "Provide a custom port for the proxy/port-forward")
proxyCmd.Flags().String(configStructs.ProxyHostLabel, defaultTapConfig.Proxy.Host, "Provide a custom host for the proxy/port-forward")
proxyCmd.Flags().StringP(configStructs.ReleaseNamespaceLabel, "s", defaultTapConfig.Release.Namespace, "Release namespace of Kubeshark")
}
+107
View File
@@ -0,0 +1,107 @@
package cmd
import (
"context"
"fmt"
"net/http"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/kubeshark/kubeshark/internal/connect"
"github.com/kubeshark/kubeshark/kubernetes"
"github.com/kubeshark/kubeshark/misc"
"github.com/kubeshark/kubeshark/utils"
"github.com/rs/zerolog/log"
)
func runProxy(block bool, noBrowser bool) {
kubernetesProvider, err := getKubernetesProviderForCli(false, false)
if err != nil {
return
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
exists, err := kubernetesProvider.DoesServiceExist(ctx, config.Config.Tap.Release.Namespace, kubernetes.FrontServiceName)
if err != nil {
log.Error().
Str("service", kubernetes.FrontServiceName).
Err(err).
Msg("Failed to found service!")
cancel()
return
}
if !exists {
log.Error().
Str("service", kubernetes.FrontServiceName).
Str("command", fmt.Sprintf("%s %s", misc.Program, tapCmd.Use)).
Msg("Service not found! You should run the command first:")
cancel()
return
}
exists, err = kubernetesProvider.DoesServiceExist(ctx, config.Config.Tap.Release.Namespace, kubernetes.HubServiceName)
if err != nil {
log.Error().
Str("service", kubernetes.HubServiceName).
Err(err).
Msg("Failed to found service!")
cancel()
return
}
if !exists {
log.Error().
Str("service", kubernetes.HubServiceName).
Str("command", fmt.Sprintf("%s %s", misc.Program, tapCmd.Use)).
Msg("Service not found! You should run the command first:")
cancel()
return
}
var establishedProxy bool
frontUrl := kubernetes.GetProxyOnPort(config.Config.Tap.Proxy.Front.Port)
response, err := http.Get(fmt.Sprintf("%s/", frontUrl))
if err == nil && response.StatusCode == 200 {
log.Info().
Str("service", kubernetes.FrontServiceName).
Int("port", int(config.Config.Tap.Proxy.Front.Port)).
Msg("Found a running service.")
okToOpen("Kubeshark", frontUrl, noBrowser)
} else {
startProxyReportErrorIfAny(
kubernetesProvider,
ctx,
kubernetes.FrontServiceName,
kubernetes.FrontPodName,
configStructs.ProxyFrontPortLabel,
config.Config.Tap.Proxy.Front.Port,
configStructs.ContainerPort,
"",
)
connector := connect.NewConnector(frontUrl, connect.DefaultRetries, connect.DefaultTimeout)
if err := connector.TestConnection(""); err != nil {
log.Error().Msg(fmt.Sprintf(utils.Red, "Couldn't connect to Front."))
return
}
establishedProxy = true
okToOpen("Kubeshark", frontUrl, noBrowser)
}
if establishedProxy && block {
utils.WaitForTermination(ctx, cancel)
}
}
func okToOpen(name string, url string, noBrowser bool) {
log.Info().Str("url", url).Msg(fmt.Sprintf(utils.Green, fmt.Sprintf("%s is available at:", name)))
if !config.Config.HeadlessMode && !noBrowser {
utils.OpenBrowser(url)
}
}
+43
View File
@@ -0,0 +1,43 @@
package cmd
import (
"fmt"
"github.com/creasty/defaults"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/misc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "kubeshark",
Short: fmt.Sprintf("%s: %s", misc.Software, misc.Description),
Long: fmt.Sprintf(`%s: %s
An extensible Kubernetes-aware network sniffer and kernel tracer.
For more info: %s`, misc.Software, misc.Description, misc.Website),
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := config.InitConfig(cmd); err != nil {
log.Fatal().Err(err).Send()
}
return nil
},
}
func init() {
defaultConfig := config.CreateDefaultConfig()
if err := defaults.Set(&defaultConfig); err != nil {
log.Debug().Err(err).Send()
}
rootCmd.PersistentFlags().StringSlice(config.SetCommandName, []string{}, fmt.Sprintf("Override values using --%s", config.SetCommandName))
rootCmd.PersistentFlags().BoolP(config.DebugFlag, "d", false, "Enable debug mode")
rootCmd.PersistentFlags().String(config.ConfigPathFlag, "", fmt.Sprintf("Set the config path, default: %s", config.GetConfigFilePath(nil)))
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the tapCmd.
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
+383
View File
@@ -0,0 +1,383 @@
package cmd
import (
"context"
"encoding/json"
"errors"
"os"
"os/signal"
"strings"
"sync"
"time"
"github.com/creasty/defaults"
"github.com/fsnotify/fsnotify"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/kubeshark/kubeshark/kubernetes"
"github.com/kubeshark/kubeshark/misc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
)
var scriptsCmd = &cobra.Command{
Use: "scripts",
Short: "Watch the `scripting.source` and/or `scripting.sources` folders for changes and update the scripts",
RunE: func(cmd *cobra.Command, args []string) error {
runScripts()
return nil
},
}
func init() {
rootCmd.AddCommand(scriptsCmd)
defaultTapConfig := configStructs.TapConfig{}
if err := defaults.Set(&defaultTapConfig); err != nil {
log.Debug().Err(err).Send()
}
scriptsCmd.Flags().Uint16(configStructs.ProxyFrontPortLabel, defaultTapConfig.Proxy.Front.Port, "Provide a custom port for the Kubeshark")
scriptsCmd.Flags().String(configStructs.ProxyHostLabel, defaultTapConfig.Proxy.Host, "Provide a custom host for the Kubeshark")
scriptsCmd.Flags().StringP(configStructs.ReleaseNamespaceLabel, "s", defaultTapConfig.Release.Namespace, "Release namespace of Kubeshark")
}
func runScripts() {
if config.Config.Scripting.Source == "" && len(config.Config.Scripting.Sources) == 0 {
log.Error().Msg("Both `scripting.source` and `scripting.sources` fields are empty.")
return
}
kubernetesProvider, err := getKubernetesProviderForCli(false, false)
if err != nil {
log.Error().Err(err).Send()
return
}
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
wg.Add(1)
go func() {
defer wg.Done()
watchConfigMap(ctx, kubernetesProvider)
}()
wg.Add(1)
go func() {
defer wg.Done()
watchScripts(ctx, kubernetesProvider, true)
}()
go func() {
<-signalChan
log.Debug().Msg("Received interrupt, stopping watchers.")
cancel()
}()
wg.Wait()
}
func createScript(provider *kubernetes.Provider, script misc.ConfigMapScript) (index int64, err error) {
const maxRetries = 5
var scripts map[int64]misc.ConfigMapScript
for i := 0; i < maxRetries; i++ {
scripts, err = kubernetes.ConfigGetScripts(provider)
if err != nil {
return
}
script.Active = kubernetes.IsActiveScript(provider, script.Title)
index = 0
if script.Title != "New Script" {
for i, v := range scripts {
if index <= i {
index = i + 1
}
if v.Title == script.Title {
index = int64(i)
}
}
}
scripts[index] = script
log.Info().Str("title", script.Title).Bool("Active", script.Active).Int64("Index", index).Msg("Creating script")
var data []byte
data, err = json.Marshal(scripts)
if err != nil {
return
}
_, err = kubernetes.SetConfig(provider, kubernetes.CONFIG_SCRIPTING_SCRIPTS, string(data))
if err == nil {
return index, nil
}
if k8serrors.IsConflict(err) {
log.Debug().Err(err).Msg("Conflict detected, retrying update...")
time.Sleep(500 * time.Millisecond)
continue
}
return 0, err
}
log.Error().Msg("Max retries reached for creating script due to conflicts.")
return 0, errors.New("max retries reached due to conflicts while creating script")
}
func updateScript(provider *kubernetes.Provider, index int64, script misc.ConfigMapScript) (err error) {
var scripts map[int64]misc.ConfigMapScript
scripts, err = kubernetes.ConfigGetScripts(provider)
if err != nil {
return
}
script.Active = kubernetes.IsActiveScript(provider, script.Title)
scripts[index] = script
var data []byte
data, err = json.Marshal(scripts)
if err != nil {
return
}
_, err = kubernetes.SetConfig(provider, kubernetes.CONFIG_SCRIPTING_SCRIPTS, string(data))
if err != nil {
return
}
return
}
func deleteScript(provider *kubernetes.Provider, index int64) (err error) {
var scripts map[int64]misc.ConfigMapScript
scripts, err = kubernetes.ConfigGetScripts(provider)
if err != nil {
return
}
err = kubernetes.DeleteActiveScriptByTitle(provider, scripts[index].Title)
if err != nil {
return
}
delete(scripts, index)
var data []byte
data, err = json.Marshal(scripts)
if err != nil {
return
}
_, err = kubernetes.SetConfig(provider, kubernetes.CONFIG_SCRIPTING_SCRIPTS, string(data))
if err != nil {
return
}
return
}
func watchScripts(ctx context.Context, provider *kubernetes.Provider, block bool) {
files := make(map[string]int64)
scripts, err := config.Config.Scripting.GetScripts()
if err != nil {
log.Error().Err(err).Send()
return
}
for _, script := range scripts {
index, err := createScript(provider, script.ConfigMap())
if err != nil {
log.Error().Err(err).Send()
continue
}
files[script.Path] = index
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Error().Err(err).Send()
return
}
if block {
defer watcher.Close()
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
go func() {
<-signalChan
log.Debug().Msg("Received interrupt, stopping script watch.")
cancel()
watcher.Close()
}()
if err := watcher.Add(config.Config.Scripting.Source); err != nil {
log.Error().Err(err).Msg("Failed to add scripting source to watcher")
return
}
go func() {
for {
select {
case <-ctx.Done():
log.Debug().Msg("Script watcher exiting gracefully.")
return
// watch for events
case event := <-watcher.Events:
if !strings.HasSuffix(event.Name, "js") {
log.Info().Str("file", event.Name).Msg("Ignoring file")
continue
}
switch event.Op {
case fsnotify.Create:
script, err := misc.ReadScriptFile(event.Name)
if err != nil {
log.Error().Err(err).Send()
continue
}
index, err := createScript(provider, script.ConfigMap())
if err != nil {
log.Error().Err(err).Send()
continue
}
files[script.Path] = index
case fsnotify.Write:
index := files[event.Name]
script, err := misc.ReadScriptFile(event.Name)
if err != nil {
log.Error().Err(err).Send()
continue
}
err = updateScript(provider, index, script.ConfigMap())
if err != nil {
log.Error().Err(err).Send()
continue
}
case fsnotify.Rename:
index := files[event.Name]
err := deleteScript(provider, index)
if err != nil {
log.Error().Err(err).Send()
continue
}
default:
// pass
}
case err, ok := <-watcher.Errors:
if !ok {
log.Info().Msg("Watcher errors channel closed.")
return
}
log.Error().Err(err).Msg("Watcher error encountered")
}
}
}()
if err := watcher.Add(config.Config.Scripting.Source); err != nil {
log.Error().Err(err).Send()
}
for _, source := range config.Config.Scripting.Sources {
if err := watcher.Add(source); err != nil {
log.Error().Err(err).Send()
}
}
log.Info().Str("folder", config.Config.Scripting.Source).Interface("folders", config.Config.Scripting.Sources).Msg("Watching scripts against changes:")
if block {
<-ctx.Done()
}
}
func watchConfigMap(ctx context.Context, provider *kubernetes.Provider) {
clientset := provider.GetClientSet()
configMapName := kubernetes.SELF_RESOURCES_PREFIX + kubernetes.SUFFIX_CONFIG_MAP
for {
select {
case <-ctx.Done():
log.Info().Msg("ConfigMap watcher exiting gracefully.")
return
default:
watcher, err := clientset.CoreV1().ConfigMaps(config.Config.Tap.Release.Namespace).Watch(context.TODO(), metav1.ListOptions{
FieldSelector: "metadata.name=" + configMapName,
})
if err != nil {
log.Warn().Err(err).Msg("ConfigMap not found, retrying in 5 seconds...")
time.Sleep(5 * time.Second)
continue
}
// Create a goroutine to process events
watcherClosed := make(chan struct{})
go func() {
defer close(watcherClosed)
for event := range watcher.ResultChan() {
if event.Type == watch.Added {
log.Info().Msg("ConfigMap created or modified")
runScriptsSync(provider)
} else if event.Type == watch.Deleted {
log.Warn().Msg("ConfigMap deleted, waiting for recreation...")
break
}
}
}()
// Wait for either context cancellation or watcher completion
select {
case <-ctx.Done():
watcher.Stop()
log.Info().Msg("ConfigMap watcher stopping due to context cancellation")
return
case <-watcherClosed:
log.Info().Msg("Watcher closed, restarting...")
}
time.Sleep(5 * time.Second)
}
}
}
func runScriptsSync(provider *kubernetes.Provider) {
files := make(map[string]int64)
scripts, err := config.Config.Scripting.GetScripts()
if err != nil {
log.Error().Err(err).Send()
return
}
for _, script := range scripts {
index, err := createScript(provider, script.ConfigMap())
if err != nil {
log.Error().Err(err).Send()
continue
}
files[script.Path] = index
}
log.Info().Msg("Synchronized scripts with ConfigMap.")
}
+66
View File
@@ -0,0 +1,66 @@
package cmd
import (
"errors"
"github.com/creasty/defaults"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/kubeshark/kubeshark/errormessage"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var tapCmd = &cobra.Command{
Use: "tap [POD REGEX]",
Short: "Capture the network traffic in your Kubernetes cluster",
RunE: func(cmd *cobra.Command, args []string) error {
tap()
return nil
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 1 {
config.Config.Tap.PodRegexStr = args[0]
} else if len(args) > 1 {
return errors.New("unexpected number of arguments")
}
if err := config.Config.Tap.Validate(); err != nil {
return errormessage.FormatError(err)
}
return nil
},
}
func init() {
rootCmd.AddCommand(tapCmd)
defaultTapConfig := configStructs.TapConfig{}
if err := defaults.Set(&defaultTapConfig); err != nil {
log.Debug().Err(err).Send()
}
tapCmd.Flags().StringP(configStructs.DockerRegistryLabel, "r", defaultTapConfig.Docker.Registry, "The Docker registry that's hosting the images")
tapCmd.Flags().StringP(configStructs.DockerTagLabel, "t", defaultTapConfig.Docker.Tag, "The tag of the Docker images that are going to be pulled")
tapCmd.Flags().String(configStructs.DockerImagePullPolicy, defaultTapConfig.Docker.ImagePullPolicy, "ImagePullPolicy for the Docker images")
tapCmd.Flags().StringSlice(configStructs.DockerImagePullSecrets, defaultTapConfig.Docker.ImagePullSecrets, "ImagePullSecrets for the Docker images")
tapCmd.Flags().Uint16(configStructs.ProxyFrontPortLabel, defaultTapConfig.Proxy.Front.Port, "Provide a custom port for the proxy/port-forward")
tapCmd.Flags().String(configStructs.ProxyHostLabel, defaultTapConfig.Proxy.Host, "Provide a custom host for the proxy/port-forward")
tapCmd.Flags().StringSliceP(configStructs.NamespacesLabel, "n", defaultTapConfig.Namespaces, "Namespaces selector")
tapCmd.Flags().StringSliceP(configStructs.ExcludedNamespacesLabel, "e", defaultTapConfig.ExcludedNamespaces, "Excluded namespaces")
tapCmd.Flags().StringP(configStructs.ReleaseNamespaceLabel, "s", defaultTapConfig.Release.Namespace, "Release namespace of Kubeshark")
tapCmd.Flags().Bool(configStructs.PersistentStorageLabel, defaultTapConfig.PersistentStorage, "Enable persistent storage (PersistentVolumeClaim)")
tapCmd.Flags().Bool(configStructs.PersistentStorageStaticLabel, defaultTapConfig.PersistentStorageStatic, "Persistent storage static provision")
tapCmd.Flags().String(configStructs.EfsFileSytemIdAndPathLabel, defaultTapConfig.EfsFileSytemIdAndPath, "EFS file system ID")
tapCmd.Flags().String(configStructs.StorageLimitLabel, defaultTapConfig.StorageLimit, "Override the default storage limit (per node)")
tapCmd.Flags().String(configStructs.StorageClassLabel, defaultTapConfig.StorageClass, "Override the default storage class of the PersistentVolumeClaim (per node)")
tapCmd.Flags().Bool(configStructs.DryRunLabel, defaultTapConfig.DryRun, "Preview of all pods matching the regex, without tapping them")
tapCmd.Flags().Bool(configStructs.ServiceMeshLabel, defaultTapConfig.ServiceMesh, "Capture the encrypted traffic if the cluster is configured with a service mesh and with mTLS")
tapCmd.Flags().Bool(configStructs.TlsLabel, defaultTapConfig.Tls, "Capture the traffic that's encrypted with OpenSSL or Go crypto/tls libraries")
tapCmd.Flags().Bool(configStructs.IngressEnabledLabel, defaultTapConfig.Ingress.Enabled, "Enable Ingress")
tapCmd.Flags().Bool(configStructs.TelemetryEnabledLabel, defaultTapConfig.Telemetry.Enabled, "Enable/disable Telemetry")
tapCmd.Flags().Bool(configStructs.ResourceGuardEnabledLabel, defaultTapConfig.ResourceGuard.Enabled, "Enable/disable resource guard")
tapCmd.Flags().Bool(configStructs.WatchdogEnabled, defaultTapConfig.Watchdog.Enabled, "Enable/disable watchdog")
tapCmd.Flags().String(configStructs.HelmChartPathLabel, defaultTapConfig.Release.HelmChartPath, "Path to a local Helm chart folder (overrides the remote Helm repo)")
}
+487
View File
@@ -0,0 +1,487 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/kubeshark/kubeshark/kubernetes/helm"
"github.com/kubeshark/kubeshark/misc"
"github.com/kubeshark/kubeshark/utils"
core "k8s.io/api/core/v1"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/config/configStructs"
"github.com/kubeshark/kubeshark/errormessage"
"github.com/kubeshark/kubeshark/kubernetes"
"github.com/rs/zerolog/log"
)
const cleanupTimeout = time.Minute
type tapState struct {
startTime time.Time
targetNamespaces []string
}
var state tapState
type Readiness struct {
Hub bool
Front bool
Proxy bool
sync.Mutex
}
var ready *Readiness
var proxyOnce sync.Once
func tap() {
ready = &Readiness{}
proxyOnce = sync.Once{}
state.startTime = time.Now()
log.Info().Str("registry", config.Config.Tap.Docker.Registry).Str("tag", config.Config.Tap.Docker.Tag).Msg("Using Docker:")
log.Info().
Str("limit", config.Config.Tap.StorageLimit).
Msg(fmt.Sprintf("%s will store the traffic up to a limit (per node). Oldest TCP/UDP streams will be removed once the limit is reached.", misc.Software))
kubernetesProvider, err := getKubernetesProviderForCli(false, false)
if err != nil {
log.Error().Err(err).Send()
return
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel will be called when this function exits
state.targetNamespaces = kubernetesProvider.GetNamespaces()
log.Info().
Bool("enabled", config.Config.Tap.Telemetry.Enabled).
Str("notice", "Telemetry can be disabled by setting the flag: --telemetry-enabled=false").
Msg("Telemetry")
log.Info().Strs("namespaces", state.targetNamespaces).Msg("Targeting pods in:")
if err := printTargetedPodsPreview(ctx, kubernetesProvider, state.targetNamespaces); err != nil {
log.Error().Err(errormessage.FormatError(err)).Msg("Error listing pods!")
}
if config.Config.Tap.DryRun {
return
}
log.Info().Msg(fmt.Sprintf("Waiting for the creation of %s resources...", misc.Software))
rel, err := helm.NewHelm(
config.Config.Tap.Release.Repo,
config.Config.Tap.Release.Name,
config.Config.Tap.Release.Namespace,
).Install()
if err != nil {
if err.Error() != "cannot re-use a name that is still in use" {
log.Error().Err(err).Send()
os.Exit(1)
}
log.Info().Msg("Found an existing installation, skipping Helm install...")
updateConfig(kubernetesProvider)
postFrontStarted(ctx, kubernetesProvider, cancel)
} else {
log.Info().Msgf("Installed the Helm release: %s", rel.Name)
go watchHubEvents(ctx, kubernetesProvider, cancel)
go watchHubPod(ctx, kubernetesProvider, cancel)
go watchFrontPod(ctx, kubernetesProvider, cancel)
}
defer finishTapExecution(kubernetesProvider)
// block until exit signal or error
utils.WaitForTermination(ctx, cancel)
if !config.Config.Tap.Ingress.Enabled {
printProxyCommandSuggestion()
}
}
func printProxyCommandSuggestion() {
log.Warn().
Str("command", fmt.Sprintf("%s proxy", misc.Program)).
Msg(fmt.Sprintf(utils.Yellow, "To re-establish a proxy/port-forward, run:"))
}
func finishTapExecution(kubernetesProvider *kubernetes.Provider) {
finishSelfExecution(kubernetesProvider)
}
/*
This function is a bit problematic as it might be detached from the actual pods the Kubeshark that targets.
The alternative would be to wait for Hub to be ready and then query it for the pods it listens to, this has
the arguably worse drawback of taking a relatively very long time before the user sees which pods are targeted, if any.
*/
func printTargetedPodsPreview(ctx context.Context, kubernetesProvider *kubernetes.Provider, namespaces []string) error {
if matchingPods, err := kubernetesProvider.ListAllRunningPodsMatchingRegex(ctx, config.Config.Tap.PodRegex(), namespaces); err != nil {
return err
} else {
if len(matchingPods) == 0 {
printNoPodsFoundSuggestion(namespaces)
}
for _, targetedPod := range matchingPods {
log.Info().Msg(fmt.Sprintf("Targeted pod: %s", fmt.Sprintf(utils.Green, targetedPod.Name)))
}
return nil
}
}
func printNoPodsFoundSuggestion(targetNamespaces []string) {
var suggestionStr string
if !utils.Contains(targetNamespaces, kubernetes.K8sAllNamespaces) {
suggestionStr = ". You can also try selecting a different namespace with -n or target all namespaces with -A"
}
log.Warn().Msg(fmt.Sprintf("Did not find any currently running pods that match the regex argument, %s will automatically target matching pods if any are created later%s", misc.Software, suggestionStr))
}
func isPodReady(pod *core.Pod) bool {
for _, condition := range pod.Status.Conditions {
if condition.Type == core.PodReady {
return condition.Status == core.ConditionTrue
}
}
return false
}
func watchHubPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s", kubernetes.HubPodName))
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.Tap.Release.Namespace}, podWatchHelper)
podReady := false
podRunning := false
timeAfter := time.After(120 * time.Second)
for {
select {
case wEvent, ok := <-eventChan:
if !ok {
eventChan = nil
continue
}
switch wEvent.Type {
case kubernetes.EventAdded:
log.Info().Str("pod", kubernetes.HubPodName).Msg("Added:")
case kubernetes.EventDeleted:
log.Info().Str("pod", kubernetes.HubPodName).Msg("Removed:")
cancel()
return
case kubernetes.EventModified:
modifiedPod, err := wEvent.ToPod()
if err != nil {
log.Error().Str("pod", kubernetes.HubPodName).Err(err).Msg("While watching pod.")
cancel()
continue
}
log.Debug().
Str("pod", kubernetes.HubPodName).
Interface("phase", modifiedPod.Status.Phase).
Interface("containers-statuses", modifiedPod.Status.ContainerStatuses).
Msg("Watching pod.")
if isPodReady(modifiedPod) && !podReady {
podReady = true
ready.Lock()
ready.Hub = true
ready.Unlock()
log.Info().Str("pod", kubernetes.HubPodName).Msg("Ready.")
} else if modifiedPod.Status.Phase == core.PodRunning && !podRunning {
podRunning = true
log.Info().Str("pod", kubernetes.HubPodName).Msg("Waiting for readiness...")
}
ready.Lock()
hubPodReady := ready.Hub
frontPodReady := ready.Front
ready.Unlock()
if hubPodReady && frontPodReady {
proxyOnce.Do(func() {
ready.Lock()
ready.Proxy = true
ready.Unlock()
postFrontStarted(ctx, kubernetesProvider, cancel)
})
}
case kubernetes.EventBookmark:
break
case kubernetes.EventError:
break
}
case err, ok := <-errorChan:
if !ok {
errorChan = nil
continue
}
log.Error().
Str("pod", kubernetes.HubPodName).
Str("namespace", config.Config.Tap.Release.Namespace).
Err(err).
Msg("Failed creating pod.")
cancel()
case <-timeAfter:
if !podReady {
log.Error().
Str("pod", kubernetes.HubPodName).
Msg("Pod was not ready in time.")
cancel()
}
case <-ctx.Done():
log.Debug().
Str("pod", kubernetes.HubPodName).
Msg("Watching pod, context done.")
return
}
}
}
func watchFrontPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s", kubernetes.FrontPodName))
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.Tap.Release.Namespace}, podWatchHelper)
podReady := false
podRunning := false
timeAfter := time.After(120 * time.Second)
for {
select {
case wEvent, ok := <-eventChan:
if !ok {
eventChan = nil
continue
}
switch wEvent.Type {
case kubernetes.EventAdded:
log.Info().Str("pod", kubernetes.FrontPodName).Msg("Added:")
case kubernetes.EventDeleted:
log.Info().Str("pod", kubernetes.FrontPodName).Msg("Removed:")
cancel()
return
case kubernetes.EventModified:
modifiedPod, err := wEvent.ToPod()
if err != nil {
log.Error().Str("pod", kubernetes.FrontPodName).Err(err).Msg("While watching pod.")
cancel()
continue
}
log.Debug().
Str("pod", kubernetes.FrontPodName).
Interface("phase", modifiedPod.Status.Phase).
Interface("containers-statuses", modifiedPod.Status.ContainerStatuses).
Msg("Watching pod.")
if isPodReady(modifiedPod) && !podReady {
podReady = true
ready.Lock()
ready.Front = true
ready.Unlock()
log.Info().Str("pod", kubernetes.FrontPodName).Msg("Ready.")
} else if modifiedPod.Status.Phase == core.PodRunning && !podRunning {
podRunning = true
log.Info().Str("pod", kubernetes.FrontPodName).Msg("Waiting for readiness...")
}
ready.Lock()
hubPodReady := ready.Hub
frontPodReady := ready.Front
ready.Unlock()
if hubPodReady && frontPodReady {
proxyOnce.Do(func() {
ready.Lock()
ready.Proxy = true
ready.Unlock()
postFrontStarted(ctx, kubernetesProvider, cancel)
})
}
case kubernetes.EventBookmark:
break
case kubernetes.EventError:
break
}
case err, ok := <-errorChan:
if !ok {
errorChan = nil
continue
}
log.Error().
Str("pod", kubernetes.FrontPodName).
Str("namespace", config.Config.Tap.Release.Namespace).
Err(err).
Msg("Failed creating pod.")
case <-timeAfter:
if !podReady {
log.Error().
Str("pod", kubernetes.FrontPodName).
Msg("Pod was not ready in time.")
cancel()
}
case <-ctx.Done():
log.Debug().
Str("pod", kubernetes.FrontPodName).
Msg("Watching pod, context done.")
return
}
}
}
func watchHubEvents(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s", kubernetes.HubPodName))
eventWatchHelper := kubernetes.NewEventWatchHelper(kubernetesProvider, podExactRegex, "pod")
eventChan, errorChan := kubernetes.FilteredWatch(ctx, eventWatchHelper, []string{config.Config.Tap.Release.Namespace}, eventWatchHelper)
for {
select {
case wEvent, ok := <-eventChan:
if !ok {
eventChan = nil
continue
}
event, err := wEvent.ToEvent()
if err != nil {
log.Error().
Str("pod", kubernetes.HubPodName).
Err(err).
Msg("Parsing resource event.")
continue
}
if state.startTime.After(event.CreationTimestamp.Time) {
continue
}
log.Debug().
Str("pod", kubernetes.HubPodName).
Str("event", event.Name).
Time("time", event.CreationTimestamp.Time).
Str("name", event.Regarding.Name).
Str("kind", event.Regarding.Kind).
Str("reason", event.Reason).
Str("note", event.Note).
Msg("Watching events.")
switch event.Reason {
case "FailedScheduling", "Failed":
log.Error().
Str("pod", kubernetes.HubPodName).
Str("event", event.Name).
Time("time", event.CreationTimestamp.Time).
Str("name", event.Regarding.Name).
Str("kind", event.Regarding.Kind).
Str("reason", event.Reason).
Str("note", event.Note).
Msg("Watching events.")
cancel()
}
case err, ok := <-errorChan:
if !ok {
errorChan = nil
continue
}
log.Error().
Str("pod", kubernetes.HubPodName).
Err(err).
Msg("While watching events.")
case <-ctx.Done():
log.Debug().
Str("pod", kubernetes.HubPodName).
Msg("Watching pod events, context done.")
return
}
}
}
func postFrontStarted(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
startProxyReportErrorIfAny(
kubernetesProvider,
ctx,
kubernetes.FrontServiceName,
kubernetes.FrontPodName,
configStructs.ProxyFrontPortLabel,
config.Config.Tap.Proxy.Front.Port,
configStructs.ContainerPort,
"",
)
var url string
if config.Config.Tap.Ingress.Enabled {
url = fmt.Sprintf("http://%s", config.Config.Tap.Ingress.Host)
} else {
url = kubernetes.GetProxyOnPort(config.Config.Tap.Proxy.Front.Port)
}
log.Info().Str("url", url).Msg(fmt.Sprintf(utils.Green, fmt.Sprintf("%s is available at:", misc.Software)))
if !config.Config.HeadlessMode {
utils.OpenBrowser(url)
}
for !ready.Hub {
time.Sleep(100 * time.Millisecond)
}
if (config.Config.Scripting.Source != "" || len(config.Config.Scripting.Sources) > 0) && config.Config.Scripting.WatchScripts {
watchScripts(ctx, kubernetesProvider, false)
}
}
func updateConfig(kubernetesProvider *kubernetes.Provider) {
_, _ = kubernetes.SetSecret(kubernetesProvider, kubernetes.SECRET_LICENSE, config.Config.License)
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_POD_REGEX, config.Config.Tap.PodRegexStr)
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_NAMESPACES, strings.Join(config.Config.Tap.Namespaces, ","))
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_EXCLUDED_NAMESPACES, strings.Join(config.Config.Tap.ExcludedNamespaces, ","))
data, err := json.Marshal(config.Config.Scripting.Env)
if err != nil {
log.Error().Str("config", kubernetes.CONFIG_SCRIPTING_ENV).Err(err).Send()
return
} else {
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_SCRIPTING_ENV, string(data))
}
ingressEnabled := ""
if config.Config.Tap.Ingress.Enabled {
ingressEnabled = "true"
}
authEnabled := ""
if config.Config.Tap.Auth.Enabled {
authEnabled = "true"
}
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_INGRESS_ENABLED, ingressEnabled)
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_INGRESS_HOST, config.Config.Tap.Ingress.Host)
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_PROXY_FRONT_PORT, fmt.Sprint(config.Config.Tap.Proxy.Front.Port))
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_AUTH_ENABLED, authEnabled)
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_AUTH_TYPE, config.Config.Tap.Auth.Type)
_, _ = kubernetes.SetConfig(kubernetesProvider, kubernetes.CONFIG_AUTH_SAML_IDP_METADATA_URL, config.Config.Tap.Auth.Saml.IdpMetadataUrl)
}
+35
View File
@@ -0,0 +1,35 @@
package cmd
import (
"fmt"
"strconv"
"time"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/misc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version info",
RunE: func(cmd *cobra.Command, args []string) error {
timeStampInt, _ := strconv.ParseInt(misc.BuildTimestamp, 10, 0)
if config.DebugMode {
log.Info().
Str("version", misc.Ver).
Str("branch", misc.Branch).
Str("commit-hash", misc.GitCommitHash).
Time("build-time", time.Unix(timeStampInt, 0)).
Send()
} else {
fmt.Println(misc.Ver)
}
return nil
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}