mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-09-01 00:57:17 +00:00
The chart answered three unrelated questions with one switch: whether the Hub gates its API, whether the dashboard must log in to fetch a licence key, and which product features exist. So a default install rendered AUTH_ENABLED=true purely to make the cloud login reachable, and tap.auth.enabled=true with type=saml rendered AUTH_ENABLED=false, shipping an unauthenticated Hub to an operator who had asked for SSO. kubeshark.authEnabled is now tap.auth.enabled verbatim. AUTH_TYPE is the configured type verbatim, instead of being rewritten to 'default' whenever cloud licensing or demo mode was on. Settings that cannot work now fail the render rather than being quietly rewritten into something that does. The dashboard's REACT_APP_AUTH_ENABLED was a second, hand-maintained copy of the old expression with an extra internetConnectivity term, so air-gapped installs rendered a gated Hub against a dashboard that never mounted a login. It uses the same helper now. The worker hub token is projected unconditionally. Gating it on the auth decision coupled a DaemonSet rollout to that decision, so anything enabling auth without re-rendering workers left them holding no token against a Hub that required one. The CLI no longer writes AUTH_ENABLED, AUTH_TYPE or AUTH_SAML_IDP_METADATA_URL. Those come from the values the release was installed with; writing them from the CLI's own config meant a plain 'kubeshark tap' against an SSO release could silently reconfigure or disable its authentication.
489 lines
14 KiB
Go
489 lines
14 KiB
Go
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/rs/zerolog/log"
|
|
|
|
"github.com/kubeshark/kubeshark/config"
|
|
"github.com/kubeshark/kubeshark/config/configStructs"
|
|
"github.com/kubeshark/kubeshark/errormessage"
|
|
"github.com/kubeshark/kubeshark/kubernetes"
|
|
)
|
|
|
|
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"
|
|
}
|
|
|
|
_, _ = 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))
|
|
|
|
// AUTH_ENABLED / AUTH_TYPE / AUTH_SAML_IDP_METADATA_URL are deliberately
|
|
// not written here. They are rendered by the Helm chart from the values
|
|
// the release was installed with, and this function runs on the
|
|
// "existing installation" path, where the CLI's own config is a separate
|
|
// source that may never have carried auth settings at all. Writing them
|
|
// meant a plain `kubeshark tap` against a release installed with SSO
|
|
// silently reconfigured, or disabled, its authentication.
|
|
//
|
|
// Change auth with `helm upgrade`, or `kubeshark tap` a fresh release.
|
|
}
|