mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-03-06 12:42:14 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc59fb6931 | ||
|
|
793bb97e51 | ||
|
|
ceb8d714e3 | ||
|
|
8d8310ee02 | ||
|
|
0824524d62 | ||
|
|
71eff5ea04 | ||
|
|
50e404f51e | ||
|
|
ffa34039b1 | ||
|
|
d888706e1e | ||
|
|
1ef17542dd | ||
|
|
0566f63d72 | ||
|
|
6d49339e29 | ||
|
|
58f0de4d4e | ||
|
|
f175480f65 |
24
Makefile
24
Makefile
@@ -20,14 +20,14 @@ TS_SUFFIX="$(shell date '+%s')"
|
||||
GIT_BRANCH="$(shell git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]' | tr '/' '_')"
|
||||
BUCKET_PATH=static.up9.io/mizu/$(GIT_BRANCH)
|
||||
|
||||
ui: ## build UI
|
||||
ui: ## Build UI.
|
||||
@(cd ui; npm i ; npm run build; )
|
||||
@ls -l ui/build
|
||||
|
||||
cli: # build CLI
|
||||
cli: ## Build CLI.
|
||||
@echo "building cli"; cd cli && $(MAKE) build
|
||||
|
||||
agent: ## build mizuagent server
|
||||
agent: ## Build agent.
|
||||
@(echo "building mizu agent .." )
|
||||
@(cd agent; go build -o build/mizuagent main.go)
|
||||
@ls -l agent/build
|
||||
@@ -36,17 +36,17 @@ agent: ## build mizuagent server
|
||||
# @(cd tap; go build -o build/tap ./src)
|
||||
# @ls -l tap/build
|
||||
|
||||
docker: ## build Docker image
|
||||
docker: ## Build Docker image.
|
||||
@(echo "building docker image" )
|
||||
./build-push-featurebranch.sh
|
||||
|
||||
push: push-docker push-cli ## build and publish Mizu docker image & CLI
|
||||
push: push-docker push-cli ## Build and publish agent docker image & CLI.
|
||||
|
||||
push-docker:
|
||||
push-docker: ## Build and publish agent docker image.
|
||||
@echo "publishing Docker image .. "
|
||||
./build-push-featurebranch.sh
|
||||
|
||||
push-cli:
|
||||
push-cli: ## Build and publish CLI.
|
||||
@echo "publishing CLI .. "
|
||||
@cd cli; $(MAKE) build-all
|
||||
@echo "publishing file ${OUTPUT_FILE} .."
|
||||
@@ -55,17 +55,17 @@ push-cli:
|
||||
gsutil setmeta -r -h "Cache-Control:public, max-age=30" gs://${BUCKET_PATH}/\*
|
||||
|
||||
|
||||
clean: clean-ui clean-agent clean-cli clean-docker ## Clean all build artifacts
|
||||
clean: clean-ui clean-agent clean-cli clean-docker ## Clean all build artifacts.
|
||||
|
||||
clean-ui:
|
||||
clean-ui: ## Clean UI.
|
||||
@(rm -rf ui/build ; echo "UI cleanup done" )
|
||||
|
||||
clean-agent:
|
||||
clean-agent: ## Clean agent.
|
||||
@(rm -rf agent/build ; echo "agent cleanup done" )
|
||||
|
||||
clean-cli:
|
||||
clean-cli: ## Clean CLI.
|
||||
@(cd cli; make clean ; echo "CLI cleanup done" )
|
||||
|
||||
clean-docker:
|
||||
clean-docker:
|
||||
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
||||
|
||||
|
||||
19
README.md
19
README.md
@@ -1,6 +1,17 @@
|
||||
# 水 mizu
|
||||

|
||||
# The API Traffic Viewer for Kubernetes
|
||||
|
||||
A simple-yet-powerful API traffic viewer for Kubernetes to help you troubleshoot and debug your microservices. Think TCPDump and Chrome Dev Tools combined.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Simple and powerful CLI
|
||||
- Real time view of all HTTP requests, REST and gRPC API calls
|
||||
- No installation or code instrumentation
|
||||
- Works completely on premises (on-prem)
|
||||
|
||||
## Download
|
||||
|
||||
Download `mizu` for your platform and operating system
|
||||
@@ -142,10 +153,10 @@ See `examples/roles` for example `clusterroles`.
|
||||
|
||||
## How to run
|
||||
|
||||
1. Find pod you'd like to tap to in your Kubernetes cluster
|
||||
1. Find pods you'd like to tap to in your Kubernetes cluster
|
||||
2. Run `mizu tap PODNAME` or `mizu tap REGEX`
|
||||
3. Open browser on `http://localhost:8899` as instructed ..
|
||||
4. Watch the WebAPI traffic flowing ..
|
||||
3. Open browser on `http://localhost:8899/mizu` **or** as instructed in the CLI ..
|
||||
4. Watch the API traffic flowing ..
|
||||
5. Type ^C to stop
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -22,6 +22,7 @@ require (
|
||||
k8s.io/api v0.21.0
|
||||
k8s.io/apimachinery v0.21.0
|
||||
k8s.io/client-go v0.21.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
)
|
||||
|
||||
replace github.com/up9inc/mizu/shared v0.0.0 => ../shared
|
||||
|
||||
@@ -45,6 +45,8 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
|
||||
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4 h1:NJOOlc6ZJjix0A1rAU+nxruZtR8KboG1848yqpIUo4M=
|
||||
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4/go.mod h1:DQPxZS994Ld1Y8uwnJT+dRL04XPD0cElP/pHH/zEBHM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
@@ -245,6 +247,8 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 h1:fa50YL1pzKW+1SsBnJDOHppJN9stOEwS+CRWyUtyYGU=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
||||
@@ -62,8 +62,8 @@ func main() {
|
||||
panic(fmt.Sprintf("Error connecting to socket server at %s %v", *apiServerAddress, err))
|
||||
}
|
||||
|
||||
go pipeChannelToSocket(socketConnection, harOutputChannel)
|
||||
go api.StartReadingOutbound(outboundLinkOutputChannel)
|
||||
go pipeTapChannelToSocket(socketConnection, harOutputChannel)
|
||||
go pipeOutboundLinksChannelToSocket(socketConnection, outboundLinkOutputChannel)
|
||||
} else if *apiServer {
|
||||
socketHarOutChannel := make(chan *tap.OutputChannelItem, 1000)
|
||||
filteredHarChannel := make(chan *tap.OutputChannelItem)
|
||||
@@ -177,7 +177,7 @@ func isHealthCheckByUserAgent(message *tap.OutputChannelItem) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func pipeChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *tap.OutputChannelItem) {
|
||||
func pipeTapChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *tap.OutputChannelItem) {
|
||||
if connection == nil {
|
||||
panic("Websocket connection is nil")
|
||||
}
|
||||
@@ -200,3 +200,21 @@ func pipeChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pipeOutboundLinksChannelToSocket(connection *websocket.Conn, outboundLinkChannel <-chan *tap.OutboundLink) {
|
||||
for outboundLink := range outboundLinkChannel {
|
||||
if outboundLink.SuggestedProtocol == tap.TLSProtocol {
|
||||
marshaledData, err := models.CreateWebsocketOutboundLinkMessage(outboundLink)
|
||||
if err != nil {
|
||||
rlog.Infof("Error converting outbound link to json %s, (%v,%+v)", err, err, err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = connection.WriteMessage(websocket.TextMessage, marshaledData)
|
||||
if err != nil {
|
||||
rlog.Infof("error sending outbound link message through socket server %s, (%v,%+v)", err, err, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/romana/rlog"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/tap"
|
||||
"mizuserver/pkg/controllers"
|
||||
"mizuserver/pkg/models"
|
||||
"mizuserver/pkg/providers"
|
||||
"mizuserver/pkg/routes"
|
||||
"mizuserver/pkg/up9"
|
||||
"sync"
|
||||
@@ -27,9 +27,9 @@ func init() {
|
||||
|
||||
func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
|
||||
if isTapper {
|
||||
rlog.Infof("Websocket Connection event - Tapper connected: %s", socketId)
|
||||
rlog.Infof("Websocket event - Tapper connected, socket ID: %d", socketId)
|
||||
} else {
|
||||
rlog.Infof("Websocket Connection event - Browser socket connected: %s", socketId)
|
||||
rlog.Infof("Websocket event - Browser socket connected, socket ID: %d", socketId)
|
||||
socketListLock.Lock()
|
||||
browserClientSocketUUIDs = append(browserClientSocketUUIDs, socketId)
|
||||
socketListLock.Unlock()
|
||||
@@ -38,9 +38,9 @@ func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
|
||||
|
||||
func (h *RoutesEventHandlers) WebSocketDisconnect(socketId int, isTapper bool) {
|
||||
if isTapper {
|
||||
rlog.Infof("Disconnection event - Tapper connected: %s", socketId)
|
||||
rlog.Infof("Websocket event - Tapper disconnected, socket ID: %d", socketId)
|
||||
} else {
|
||||
rlog.Infof("Disconnection event - Browser socket connected: %s", socketId)
|
||||
rlog.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId)
|
||||
socketListLock.Lock()
|
||||
removeSocketUUIDFromBrowserSlice(socketId)
|
||||
socketListLock.Unlock()
|
||||
@@ -52,7 +52,7 @@ func broadcastToBrowserClients(message []byte) {
|
||||
go func(socketId int) {
|
||||
err := routes.SendToSocket(socketId, message)
|
||||
if err != nil {
|
||||
fmt.Printf("error sending message to socket id %d: %v", socketId, err)
|
||||
fmt.Printf("error sending message to socket ID %d: %v", socketId, err)
|
||||
}
|
||||
}(socketId)
|
||||
|
||||
@@ -80,15 +80,46 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
|
||||
if err != nil {
|
||||
rlog.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
||||
} else {
|
||||
controllers.TapStatus = statusMessage.TappingStatus
|
||||
providers.TapStatus.Pods = statusMessage.TappingStatus.Pods
|
||||
broadcastToBrowserClients(message)
|
||||
}
|
||||
case shared.WebsocketMessageTypeOutboundLink:
|
||||
var outboundLinkMessage models.WebsocketOutboundLinkMessage
|
||||
err := json.Unmarshal(message, &outboundLinkMessage)
|
||||
if err != nil {
|
||||
rlog.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
||||
} else {
|
||||
handleTLSLink(outboundLinkMessage)
|
||||
}
|
||||
default:
|
||||
rlog.Infof("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleTLSLink(outboundLinkMessage models.WebsocketOutboundLinkMessage) {
|
||||
resolvedName := k8sResolver.Resolve(outboundLinkMessage.Data.DstIP)
|
||||
if resolvedName != "" {
|
||||
outboundLinkMessage.Data.DstIP = resolvedName
|
||||
} else if outboundLinkMessage.Data.SuggestedResolvedName != "" {
|
||||
outboundLinkMessage.Data.DstIP = outboundLinkMessage.Data.SuggestedResolvedName
|
||||
}
|
||||
cacheKey := fmt.Sprintf("%s -> %s:%d", outboundLinkMessage.Data.Src, outboundLinkMessage.Data.DstIP, outboundLinkMessage.Data.DstPort)
|
||||
_, isInCache := providers.RecentTLSLinks.Get(cacheKey)
|
||||
if isInCache {
|
||||
return
|
||||
} else {
|
||||
providers.RecentTLSLinks.SetDefault(cacheKey, outboundLinkMessage.Data)
|
||||
}
|
||||
marshaledMessage, err := json.Marshal(outboundLinkMessage)
|
||||
if err != nil {
|
||||
rlog.Errorf("Error marshaling outbound link message for broadcasting: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Broadcasting outboundlink message %s\n", string(marshaledMessage))
|
||||
broadcastToBrowserClients(marshaledMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func removeSocketUUIDFromBrowserSlice(uuidToRemove int) {
|
||||
newUUIDSlice := make([]int, 0, len(browserClientSocketUUIDs))
|
||||
for _, uuid := range browserClientSocketUUIDs {
|
||||
|
||||
@@ -2,17 +2,19 @@ package controllers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"mizuserver/pkg/providers"
|
||||
"mizuserver/pkg/up9"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var TapStatus shared.TapStatus
|
||||
|
||||
func GetTappingStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, TapStatus)
|
||||
c.JSON(http.StatusOK, providers.TapStatus)
|
||||
}
|
||||
|
||||
func AnalyzeInformation(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, up9.GetAnalyzeInfo())
|
||||
}
|
||||
|
||||
func GetRecentTLSLinks(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, providers.GetAllRecentTLSAddresses())
|
||||
}
|
||||
|
||||
@@ -132,6 +132,11 @@ type WebSocketTappedEntryMessage struct {
|
||||
Data *tap.OutputChannelItem
|
||||
}
|
||||
|
||||
type WebsocketOutboundLinkMessage struct {
|
||||
*shared.WebSocketMessageMetadata
|
||||
Data *tap.OutboundLink
|
||||
}
|
||||
|
||||
func CreateBaseEntryWebSocketMessage(base *BaseEntryDetails) ([]byte, error) {
|
||||
message := &WebSocketEntryMessage{
|
||||
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
||||
@@ -152,6 +157,16 @@ func CreateWebsocketTappedEntryMessage(base *tap.OutputChannelItem) ([]byte, err
|
||||
return json.Marshal(message)
|
||||
}
|
||||
|
||||
func CreateWebsocketOutboundLinkMessage(base *tap.OutboundLink) ([]byte, error) {
|
||||
message := &WebsocketOutboundLinkMessage{
|
||||
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
||||
MessageType: shared.WebsocketMessageTypeOutboundLink,
|
||||
},
|
||||
Data: base,
|
||||
}
|
||||
return json.Marshal(message)
|
||||
}
|
||||
|
||||
// ExtendedHAR is the top level object of a HAR log.
|
||||
type ExtendedHAR struct {
|
||||
Log *ExtendedLog `json:"log"`
|
||||
|
||||
28
agent/pkg/providers/status_provider.go
Normal file
28
agent/pkg/providers/status_provider.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/tap"
|
||||
"time"
|
||||
)
|
||||
|
||||
const tlsLinkRetainmentTime = time.Minute * 15
|
||||
|
||||
var (
|
||||
TapStatus shared.TapStatus
|
||||
RecentTLSLinks = cache.New(tlsLinkRetainmentTime, tlsLinkRetainmentTime)
|
||||
)
|
||||
|
||||
func GetAllRecentTLSAddresses() []string {
|
||||
recentTLSLinks := make([]string, 0)
|
||||
|
||||
for _, outboundLinkItem := range RecentTLSLinks.Items() {
|
||||
outboundLink, castOk := outboundLinkItem.Object.(*tap.OutboundLink)
|
||||
if castOk {
|
||||
recentTLSLinks = append(recentTLSLinks, outboundLink.DstIP)
|
||||
}
|
||||
}
|
||||
|
||||
return recentTLSLinks
|
||||
}
|
||||
@@ -22,4 +22,5 @@ func EntriesRoutes(ginApp *gin.Engine) {
|
||||
|
||||
routeGroup.GET("/tapStatus", controllers.GetTappingStatus) // get tapping status
|
||||
routeGroup.GET("/analyzeStatus", controllers.AnalyzeInformation)
|
||||
routeGroup.GET("/recentTLSLinks", controllers.GetRecentTLSLinks)
|
||||
}
|
||||
|
||||
BIN
assets/mizu-example.png
Normal file
BIN
assets/mizu-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 811 KiB |
24
assets/mizu-logo.svg
Normal file
24
assets/mizu-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/mizu-ui.png
Normal file
BIN
assets/mizu-ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 491 KiB |
@@ -13,7 +13,7 @@ help: ## This help.
|
||||
install:
|
||||
go install mizu.go
|
||||
|
||||
build: ## build mizu CLI binary (select platform via GOOS / GOARCH env variables)
|
||||
build: ## Build mizu CLI binary (select platform via GOOS / GOARCH env variables).
|
||||
go build -ldflags="-X 'github.com/up9inc/mizu/cli/mizu.GitCommitHash=$(COMMIT_HASH)' \
|
||||
-X 'github.com/up9inc/mizu/cli/mizu.Branch=$(GIT_BRANCH)' \
|
||||
-X 'github.com/up9inc/mizu/cli/mizu.BuildTimestamp=$(BUILD_TIMESTAMP)' \
|
||||
@@ -21,7 +21,7 @@ build: ## build mizu CLI binary (select platform via GOOS / GOARCH env variables
|
||||
-o bin/mizu_$(SUFFIX) mizu.go
|
||||
(cd bin && shasum -a 256 mizu_${SUFFIX} > mizu_${SUFFIX}.sha256)
|
||||
|
||||
build-all: ## build for all supported platforms
|
||||
build-all: ## Build for all supported platforms.
|
||||
@echo "Compiling for every OS and Platform"
|
||||
@mkdir -p bin && echo "SHA256 checksums available for compiled binaries \n\nRun \`shasum -a 256 -c mizu_OS_ARCH.sha256\` to verify\n\n" > bin/README.md
|
||||
@$(MAKE) build GOOS=darwin GOARCH=amd64
|
||||
@@ -35,6 +35,6 @@ build-all: ## build for all supported platforms
|
||||
@echo "---------"
|
||||
@find ./bin -ls
|
||||
|
||||
clean: ## clean all build artifacts
|
||||
clean: ## Clean all build artifacts.
|
||||
go clean
|
||||
rm -rf ./bin/*
|
||||
|
||||
34
cli/cmd/config.go
Normal file
34
cli/cmd/config.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
var outputFileName string
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Generate example config file to stdout",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
template := mizu.GetTemplateConfig()
|
||||
if outputFileName != "" {
|
||||
data := []byte(template)
|
||||
_ = ioutil.WriteFile(outputFileName, data, 0644)
|
||||
mizu.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, outputFileName)))
|
||||
} else {
|
||||
mizu.Log.Debugf("Writing template config.\n%v", template)
|
||||
fmt.Printf("%v", template)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configCmd)
|
||||
|
||||
configCmd.Flags().StringVarP(&outputFileName, "file", "f", "", "Save content to local file")
|
||||
}
|
||||
@@ -18,7 +18,7 @@ var fetchCmd = &cobra.Command{
|
||||
Use: "fetch",
|
||||
Short: "Download recorded traffic to files",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
go mizu.ReportRun("tap", mizuTapOptions)
|
||||
go mizu.ReportRun("fetch", mizuTapOptions)
|
||||
if isCompatible, err := mizu.CheckVersionCompatibility(mizuFetchOptions.MizuPort); err != nil {
|
||||
return err
|
||||
} else if !isCompatible {
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
)
|
||||
|
||||
var commandLineFlags []string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "mizu",
|
||||
Short: "A web traffic viewer for kubernetes",
|
||||
Long: `A web traffic viewer for kubernetes
|
||||
Further info is available at https://github.com/up9inc/mizu`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := mizu.InitConfig(commandLineFlags); err != nil {
|
||||
mizu.Log.Errorf("Invalid config, Exit %s", err)
|
||||
return errors.New(fmt.Sprintf("%v", err))
|
||||
}
|
||||
prettifiedConfig := mizu.GetConfigStr()
|
||||
mizu.Log.Debugf("Final Config: %s", prettifiedConfig)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringSliceVar(&commandLineFlags, "set", []string{}, "Override values using --set")
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
|
||||
@@ -35,9 +35,7 @@ var regex *regexp.Regexp
|
||||
|
||||
const maxEntriesDBSizeFlagName = "max-entries-db-size"
|
||||
|
||||
const analysisMessageToConfirm = `NOTE: running mizu with --analysis flag will upload recorded traffic
|
||||
to UP9 cloud for further analysis and enriched presentation options.
|
||||
`
|
||||
const analysisMessageToConfirm = `NOTE: running mizu with --analysis flag will upload recorded traffic for further analysis and enriched presentation options.`
|
||||
|
||||
var tapCmd = &cobra.Command{
|
||||
Use: "tap [POD REGEX]",
|
||||
@@ -48,8 +46,15 @@ Supported protocols are HTTP and gRPC.`,
|
||||
go mizu.ReportRun("tap", mizuTapOptions)
|
||||
RunMizuTap(regex, mizuTapOptions)
|
||||
return nil
|
||||
|
||||
},
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
mizu.Log.Debugf("Getting params")
|
||||
mizuTapOptions.AnalysisDestination = mizu.GetString(mizu.ConfigurationKeyAnalyzingDestination)
|
||||
mizuTapOptions.SleepIntervalSec = uint16(mizu.GetInt(mizu.ConfigurationKeyUploadInterval))
|
||||
mizuTapOptions.MizuImage = mizu.GetString(mizu.ConfigurationKeyMizuImage)
|
||||
mizu.Log.Debugf(uiUtils.PrettyJson(mizuTapOptions))
|
||||
|
||||
if len(args) == 0 {
|
||||
return errors.New("POD REGEX argument is required")
|
||||
} else if len(args) > 1 {
|
||||
@@ -67,7 +72,7 @@ Supported protocols are HTTP and gRPC.`,
|
||||
if parseHumanDataSizeErr != nil {
|
||||
return errors.New(fmt.Sprintf("Could not parse --max-entries-db-size value %s", humanMaxEntriesDBSize))
|
||||
}
|
||||
mizu.Log.Infof("Mizu will store up to %s of traffic, old traffic will be cleared once the limit is reached.\n", units.BytesToHumanReadable(mizuTapOptions.MaxEntriesDBSizeBytes))
|
||||
mizu.Log.Infof("Mizu will store up to %s of traffic, old traffic will be cleared once the limit is reached.", units.BytesToHumanReadable(mizuTapOptions.MaxEntriesDBSizeBytes))
|
||||
|
||||
directionLowerCase := strings.ToLower(direction)
|
||||
if directionLowerCase == "any" {
|
||||
@@ -80,7 +85,7 @@ Supported protocols are HTTP and gRPC.`,
|
||||
|
||||
if mizuTapOptions.Analysis {
|
||||
mizu.Log.Infof(analysisMessageToConfirm)
|
||||
if !uiUtils.AskForConfirmation("Would you like to proceed [y/n]: ") {
|
||||
if !uiUtils.AskForConfirmation("Would you like to proceed [Y/n]: ") {
|
||||
mizu.Log.Infof("You can always run mizu without analysis, aborting")
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -95,11 +100,8 @@ func init() {
|
||||
tapCmd.Flags().Uint16VarP(&mizuTapOptions.GuiPort, "gui-port", "p", 8899, "Provide a custom port for the web interface webserver")
|
||||
tapCmd.Flags().StringVarP(&mizuTapOptions.Namespace, "namespace", "n", "", "Namespace selector")
|
||||
tapCmd.Flags().BoolVar(&mizuTapOptions.Analysis, "analysis", false, "Uploads traffic to UP9 for further analysis (Beta)")
|
||||
tapCmd.Flags().StringVar(&mizuTapOptions.AnalysisDestination, "dest", "up9.app", "Destination environment")
|
||||
tapCmd.Flags().Uint16VarP(&mizuTapOptions.SleepIntervalSec, "upload-interval", "", 10, "Interval in seconds for uploading data to UP9")
|
||||
tapCmd.Flags().BoolVarP(&mizuTapOptions.AllNamespaces, "all-namespaces", "A", false, "Tap all namespaces")
|
||||
tapCmd.Flags().StringVarP(&mizuTapOptions.KubeConfigPath, "kube-config", "k", "", "Path to kube-config file")
|
||||
tapCmd.Flags().StringVarP(&mizuTapOptions.MizuImage, "mizu-image", "", fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", mizu.Branch, mizu.SemVer), "Custom image for mizu API server")
|
||||
tapCmd.Flags().StringArrayVarP(&mizuTapOptions.PlainTextFilterRegexes, "regex-masking", "r", nil, "List of regex expressions that are used to filter matching values from text/plain http bodies")
|
||||
tapCmd.Flags().StringVarP(&direction, "direction", "", "in", "Record traffic that goes in this direction (relative to the tapped pod): in/any")
|
||||
tapCmd.Flags().BoolVar(&mizuTapOptions.HideHealthChecks, "hide-healthchecks", false, "hides requests with kube-probe or prometheus user-agent headers")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/kubernetes"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/shared/debounce"
|
||||
core "k8s.io/api/core/v1"
|
||||
@@ -39,11 +40,11 @@ func RunMizuTap(podRegexQuery *regexp.Regexp, tappingOptions *MizuTapOptions) {
|
||||
kubernetesProvider, err := kubernetes.NewProvider(tappingOptions.KubeConfigPath)
|
||||
if err != nil {
|
||||
if clientcmd.IsEmptyConfig(err) {
|
||||
mizu.Log.Infof(mizu.Red, "Couldn't find the kube config file, or file is empty. Try adding '--kube-config=<path to kube config file>'\n")
|
||||
mizu.Log.Infof(uiUtils.Red, "Couldn't find the kube config file, or file is empty. Try adding '--kube-config=<path to kube config file>'\n")
|
||||
return
|
||||
}
|
||||
if clientcmd.IsConfigurationInvalid(err) {
|
||||
mizu.Log.Infof(mizu.Red, "Invalid kube config file. Try using a different config with '--kube-config=<path to kube config file>'\n")
|
||||
mizu.Log.Infof(uiUtils.Red, "Invalid kube config file. Try using a different config with '--kube-config=<path to kube config file>'\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -66,14 +67,14 @@ func RunMizuTap(podRegexQuery *regexp.Regexp, tappingOptions *MizuTapOptions) {
|
||||
} else {
|
||||
namespacesStr = "all namespaces"
|
||||
}
|
||||
mizu.Log.Infof("Tapping pods in %s\n", namespacesStr)
|
||||
mizu.Log.Infof("Tapping pods in %s", namespacesStr)
|
||||
|
||||
if len(currentlyTappedPods) == 0 {
|
||||
var suggestionStr string
|
||||
if targetNamespace != mizu.K8sAllNamespaces {
|
||||
suggestionStr = "\nSelect a different namespace with -n or tap all namespaces with -A"
|
||||
}
|
||||
mizu.Log.Infof("Did not find any pods matching the regex argument%s\n", suggestionStr)
|
||||
mizu.Log.Infof("Did not find any pods matching the regex argument%s", suggestionStr)
|
||||
}
|
||||
|
||||
nodeToTappedPodIPMap, err := getNodeHostToTappedPodIpsMap(currentlyTappedPods)
|
||||
@@ -85,9 +86,10 @@ func RunMizuTap(podRegexQuery *regexp.Regexp, tappingOptions *MizuTapOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
urlReadyChan := make(chan string)
|
||||
mizu.CheckNewerVersion()
|
||||
go portForwardApiPod(ctx, kubernetesProvider, cancel, tappingOptions) // TODO convert this to job for built in pod ttl or have the running app handle this
|
||||
go watchPodsForTapping(ctx, kubernetesProvider, cancel, podRegexQuery, tappingOptions)
|
||||
go portForwardApiPod(ctx, kubernetesProvider, cancel, tappingOptions, urlReadyChan) // TODO convert this to job for built in pod ttl or have the running app handle this
|
||||
go watchPodsForTapping(ctx, kubernetesProvider, cancel, podRegexQuery, tappingOptions, urlReadyChan)
|
||||
go syncApiStatus(ctx, cancel, tappingOptions)
|
||||
|
||||
//block until exit signal or error
|
||||
@@ -113,7 +115,7 @@ func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
func createMizuNamespace(ctx context.Context, kubernetesProvider *kubernetes.Provider) error {
|
||||
_, err := kubernetesProvider.CreateNamespace(ctx, mizu.ResourcesNamespace)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("Error creating Namespace %s: %v\n", mizu.ResourcesNamespace, err)
|
||||
mizu.Log.Infof("Error creating Namespace %s: %v", mizu.ResourcesNamespace, err)
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -131,13 +133,13 @@ func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
}
|
||||
_, err = kubernetesProvider.CreateMizuApiServerPod(ctx, mizu.ResourcesNamespace, mizu.ApiServerPodName, tappingOptions.MizuImage, serviceAccountName, mizuApiFilteringOptions, tappingOptions.MaxEntriesDBSizeBytes)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("Error creating mizu %s pod: %v\n", mizu.ApiServerPodName, err)
|
||||
mizu.Log.Infof("Error creating mizu %s pod: %v", mizu.ApiServerPodName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
apiServerService, err = kubernetesProvider.CreateService(ctx, mizu.ResourcesNamespace, mizu.ApiServerPodName, mizu.ApiServerPodName)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("Error creating mizu %s service: %v\n", mizu.ApiServerPodName, err)
|
||||
mizu.Log.Infof("Error creating mizu %s service: %v", mizu.ApiServerPodName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -182,12 +184,12 @@ func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
||||
serviceAccountName,
|
||||
tappingOptions.TapOutgoing,
|
||||
); err != nil {
|
||||
mizu.Log.Infof("Error creating mizu tapper daemonset: %v\n", err)
|
||||
mizu.Log.Infof("Error creating mizu tapper daemonset: %v", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := kubernetesProvider.RemoveDaemonSet(ctx, mizu.ResourcesNamespace, mizu.TapperDaemonSetName); err != nil {
|
||||
mizu.Log.Infof("Error deleting mizu tapper daemonset: %v\n", err)
|
||||
mizu.Log.Infof("Error deleting mizu tapper daemonset: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -202,13 +204,13 @@ func cleanUpMizuResources(kubernetesProvider *kubernetes.Provider) {
|
||||
defer cancel()
|
||||
|
||||
if err := kubernetesProvider.RemoveNamespace(removalCtx, mizu.ResourcesNamespace); err != nil {
|
||||
mizu.Log.Infof("Error removing Namespace %s: %s (%v,%+v)\n", mizu.ResourcesNamespace, err, err, err)
|
||||
mizu.Log.Infof("Error removing Namespace %s: %s (%v,%+v)", mizu.ResourcesNamespace, err, err, err)
|
||||
return
|
||||
}
|
||||
|
||||
if mizuServiceAccountExists {
|
||||
if err := kubernetesProvider.RemoveNonNamespacedResources(removalCtx, mizu.ClusterRoleName, mizu.ClusterRoleBindingName); err != nil {
|
||||
mizu.Log.Infof("Error removing non-namespaced resources: %s (%v,%+v)\n", err, err, err)
|
||||
mizu.Log.Infof("Error removing non-namespaced resources: %s (%v,%+v)", err, err, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -223,21 +225,21 @@ func cleanUpMizuResources(kubernetesProvider *kubernetes.Provider) {
|
||||
case removalCtx.Err() == context.Canceled:
|
||||
// Do nothing. User interrupted the wait.
|
||||
case err == wait.ErrWaitTimeout:
|
||||
mizu.Log.Infof("Timeout while removing Namespace %s\n", mizu.ResourcesNamespace)
|
||||
mizu.Log.Infof("Timeout while removing Namespace %s", mizu.ResourcesNamespace)
|
||||
default:
|
||||
mizu.Log.Infof("Error while waiting for Namespace %s to be deleted: %s (%v,%+v)\n", mizu.ResourcesNamespace, err, err, err)
|
||||
mizu.Log.Infof("Error while waiting for Namespace %s to be deleted: %s (%v,%+v)", mizu.ResourcesNamespace, err, err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, podRegex *regexp.Regexp, tappingOptions *MizuTapOptions) {
|
||||
func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, podRegex *regexp.Regexp, tappingOptions *MizuTapOptions, urlReadyChan chan string) {
|
||||
targetNamespace := getNamespace(tappingOptions, kubernetesProvider)
|
||||
|
||||
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider.GetPodWatcher(ctx, targetNamespace), podRegex)
|
||||
|
||||
restartTappers := func() {
|
||||
if matchingPods, err := kubernetesProvider.GetAllPodsMatchingRegex(ctx, podRegex, targetNamespace); err != nil {
|
||||
mizu.Log.Infof("Error getting pods by regex: %s (%v,%+v)\n", err, err, err)
|
||||
mizu.Log.Infof("Error getting pods by regex: %s (%v,%+v)", err, err, err)
|
||||
cancel()
|
||||
} else {
|
||||
currentlyTappedPods = matchingPods
|
||||
@@ -245,26 +247,30 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
|
||||
nodeToTappedPodIPMap, err := getNodeHostToTappedPodIpsMap(currentlyTappedPods)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("Error building node to ips map: %s (%v,%+v)\n", err, err, err)
|
||||
mizu.Log.Infof("Error building node to ips map: %s (%v,%+v)", err, err, err)
|
||||
cancel()
|
||||
}
|
||||
|
||||
if err := updateMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap, tappingOptions); err != nil {
|
||||
mizu.Log.Infof("Error updating daemonset: %s (%v,%+v)\n", err, err, err)
|
||||
mizu.Log.Infof("Error updating daemonset: %s (%v,%+v)", err, err, err)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
restartTappersDebouncer := debounce.NewDebouncer(updateTappersDelay, restartTappers)
|
||||
timer := time.AfterFunc(time.Second*10, func() {
|
||||
mizu.Log.Debugf("Waiting for URL...")
|
||||
mizu.Log.Infof("Mizu is available at http://%s", <-urlReadyChan)
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case newTarget := <-added:
|
||||
mizu.Log.Infof(mizu.Green, fmt.Sprintf("+%s\n", newTarget.Name))
|
||||
|
||||
mizu.Log.Infof(uiUtils.Green, fmt.Sprintf("+%s", newTarget.Name))
|
||||
timer.Reset(time.Second * 2)
|
||||
case removedTarget := <-removed:
|
||||
mizu.Log.Infof(mizu.Red, fmt.Sprintf("-%s\n", removedTarget.Name))
|
||||
mizu.Log.Infof(uiUtils.Red, fmt.Sprintf("-%s", removedTarget.Name))
|
||||
timer.Reset(time.Second * 2)
|
||||
restartTappersDebouncer.SetOn()
|
||||
|
||||
case modifiedTarget := <-modified:
|
||||
// Act only if the modified pod has already obtained an IP address.
|
||||
// After filtering for IPs, on a normal pod restart this includes the following events:
|
||||
@@ -286,7 +292,7 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
}
|
||||
}
|
||||
|
||||
func portForwardApiPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, tappingOptions *MizuTapOptions) {
|
||||
func portForwardApiPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, tappingOptions *MizuTapOptions, urlReadyChan chan string) {
|
||||
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", mizu.ApiServerPodName))
|
||||
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider.GetPodWatcher(ctx, mizu.ResourcesNamespace), podExactRegex)
|
||||
isPodReady := false
|
||||
@@ -299,7 +305,7 @@ func portForwardApiPod(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
||||
case <-added:
|
||||
continue
|
||||
case <-removed:
|
||||
mizu.Log.Infof("%s removed\n", mizu.ApiServerPodName)
|
||||
mizu.Log.Infof("%s removed", mizu.ApiServerPodName)
|
||||
cancel()
|
||||
return
|
||||
case modifiedPod := <-modified:
|
||||
@@ -308,33 +314,18 @@ func portForwardApiPod(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
||||
go func() {
|
||||
err := kubernetes.StartProxy(kubernetesProvider, tappingOptions.GuiPort, mizu.ResourcesNamespace, mizu.ApiServerPodName)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("Error occured while running k8s proxy %v\n", err)
|
||||
mizu.Log.Infof("Error occurred while running k8s proxy %v", err)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(tappingOptions.GuiPort)
|
||||
mizu.Log.Infof("Mizu is available at http://%s\n", mizuProxiedUrl)
|
||||
|
||||
time.Sleep(time.Second * 5) // Waiting to be sure the proxy is ready
|
||||
if tappingOptions.Analysis {
|
||||
urlPath := fmt.Sprintf("http://%s/api/uploadEntries?dest=%s&interval=%v", mizuProxiedUrl, url.QueryEscape(tappingOptions.AnalysisDestination), tappingOptions.SleepIntervalSec)
|
||||
u, err := url.ParseRequestURI(urlPath)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed parsing the URL %v\n", err))
|
||||
}
|
||||
mizu.Log.Debugf("Sending get request to %v\n", u.String())
|
||||
if response, err := http.Get(u.String()); err != nil || response.StatusCode != 200 {
|
||||
mizu.Log.Infof("error sending upload entries req, status code: %v, err: %v\n", response.StatusCode, err)
|
||||
} else {
|
||||
mizu.Log.Infof(mizu.Purple, "Traffic is uploading to UP9 for further analysis\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
urlReadyChan <- kubernetes.GetMizuApiServerProxiedHostAndPath(tappingOptions.GuiPort)
|
||||
time.Sleep(time.Second * 5) // Waiting to be sure the proxy is ready
|
||||
requestForAnalysis(tappingOptions)
|
||||
|
||||
case <-timeAfter:
|
||||
if !isPodReady {
|
||||
mizu.Log.Infof("error: %s pod was not ready in time", mizu.ApiServerPodName)
|
||||
mizu.Log.Errorf("error: %s pod was not ready in time", mizu.ApiServerPodName)
|
||||
cancel()
|
||||
}
|
||||
|
||||
@@ -344,16 +335,35 @@ func portForwardApiPod(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
||||
}
|
||||
}
|
||||
|
||||
func requestForAnalysis(tappingOptions *MizuTapOptions) {
|
||||
if !tappingOptions.Analysis {
|
||||
return
|
||||
}
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(tappingOptions.GuiPort)
|
||||
urlPath := fmt.Sprintf("http://%s/api/uploadEntries?dest=%s&interval=%v", mizuProxiedUrl, url.QueryEscape(tappingOptions.AnalysisDestination), tappingOptions.SleepIntervalSec)
|
||||
u, err := url.ParseRequestURI(urlPath)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed parsing the URL %v\n", err))
|
||||
}
|
||||
mizu.Log.Debugf("Sending get request to %v", u.String())
|
||||
if response, err := http.Get(u.String()); err != nil || response.StatusCode != 200 {
|
||||
mizu.Log.Infof("error sending upload entries req, status code: %v, err: %v", response.StatusCode, err)
|
||||
} else {
|
||||
mizu.Log.Infof(uiUtils.Purple, "Traffic is uploading to UP9 for further analysis")
|
||||
}
|
||||
}
|
||||
|
||||
func createRBACIfNecessary(ctx context.Context, kubernetesProvider *kubernetes.Provider) bool {
|
||||
mizuRBACExists, err := kubernetesProvider.DoesServiceAccountExist(ctx, mizu.ResourcesNamespace, mizu.ServiceAccountName)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("warning: could not ensure mizu rbac resources exist %v\n", err)
|
||||
mizu.Log.Infof("warning: could not ensure mizu rbac resources exist %v", err)
|
||||
return false
|
||||
}
|
||||
if !mizuRBACExists {
|
||||
err := kubernetesProvider.CreateMizuRBAC(ctx, mizu.ResourcesNamespace, mizu.ServiceAccountName, mizu.ClusterRoleName, mizu.ClusterRoleBindingName, mizu.RBACVersion)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("warning: could not create mizu rbac resources %v\n", err)
|
||||
mizu.Log.Infof("warning: could not create mizu rbac resources %v", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -390,7 +400,7 @@ func syncApiStatus(ctx context.Context, cancel context.CancelFunc, tappingOption
|
||||
controlSocketStr := fmt.Sprintf("ws://%s/ws", kubernetes.GetMizuApiServerProxiedHostAndPath(tappingOptions.GuiPort))
|
||||
controlSocket, err := mizu.CreateControlSocket(controlSocketStr)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("error establishing control socket connection %s\n", err)
|
||||
mizu.Log.Infof("error establishing control socket connection %s", err)
|
||||
cancel()
|
||||
}
|
||||
|
||||
@@ -401,12 +411,11 @@ func syncApiStatus(ctx context.Context, cancel context.CancelFunc, tappingOption
|
||||
default:
|
||||
err = controlSocket.SendNewTappedPodsListMessage(currentlyTappedPods)
|
||||
if err != nil {
|
||||
mizu.Log.Debugf("error Sending message via control socket %v, error: %s\n", controlSocketStr, err)
|
||||
mizu.Log.Debugf("error Sending message via control socket %v, error: %s", controlSocketStr, err)
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getNamespace(tappingOptions *MizuTapOptions, kubernetesProvider *kubernetes.Provider) string {
|
||||
|
||||
@@ -20,11 +20,11 @@ var versionCmd = &cobra.Command{
|
||||
go mizu.ReportRun("version", mizuVersionOptions)
|
||||
if mizuVersionOptions.DebugInfo {
|
||||
timeStampInt, _ := strconv.ParseInt(mizu.BuildTimestamp, 10, 0)
|
||||
mizu.Log.Infof("Version: %s \nBranch: %s (%s) \n", mizu.SemVer, mizu.Branch, mizu.GitCommitHash)
|
||||
mizu.Log.Infof("Build Time: %s (%s)\n", mizu.BuildTimestamp, time.Unix(timeStampInt, 0))
|
||||
mizu.Log.Infof("Version: %s \nBranch: %s (%s)", mizu.SemVer, mizu.Branch, mizu.GitCommitHash)
|
||||
mizu.Log.Infof("Build Time: %s (%s)", mizu.BuildTimestamp, time.Unix(timeStampInt, 0))
|
||||
|
||||
} else {
|
||||
mizu.Log.Infof("Version: %s (%s)\n", mizu.SemVer, mizu.Branch)
|
||||
mizu.Log.Infof("Version: %s (%s)", mizu.SemVer, mizu.Branch)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/kubernetes"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"net/http"
|
||||
)
|
||||
@@ -13,11 +14,11 @@ func runMizuView(mizuViewOptions *MizuViewOptions) {
|
||||
kubernetesProvider, err := kubernetes.NewProvider(mizuViewOptions.KubeConfigPath)
|
||||
if err != nil {
|
||||
if clientcmd.IsEmptyConfig(err) {
|
||||
mizu.Log.Infof("Couldn't find the kube config file, or file is empty. Try adding '--kube-config=<path to kube config file>'\n")
|
||||
mizu.Log.Infof("Couldn't find the kube config file, or file is empty. Try adding '--kube-config=<path to kube config file>'")
|
||||
return
|
||||
}
|
||||
if clientcmd.IsConfigurationInvalid(err) {
|
||||
mizu.Log.Infof(mizu.Red, "Invalid kube config file. Try using a different config with '--kube-config=<path to kube config file>'\n")
|
||||
mizu.Log.Infof(uiUtils.Red, "Invalid kube config file. Try using a different config with '--kube-config=<path to kube config file>'")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -30,21 +31,21 @@ func runMizuView(mizuViewOptions *MizuViewOptions) {
|
||||
panic(err)
|
||||
}
|
||||
if !exists {
|
||||
mizu.Log.Infof("The %s service not found\n", mizu.ApiServerPodName)
|
||||
mizu.Log.Infof("The %s service not found", mizu.ApiServerPodName)
|
||||
return
|
||||
}
|
||||
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizuViewOptions.GuiPort)
|
||||
_, err = http.Get(fmt.Sprintf("http://%s/", mizuProxiedUrl))
|
||||
if err == nil {
|
||||
mizu.Log.Infof("Found a running service %s and open port %d\n", mizu.ApiServerPodName, mizuViewOptions.GuiPort)
|
||||
mizu.Log.Infof("Found a running service %s and open port %d", mizu.ApiServerPodName, mizuViewOptions.GuiPort)
|
||||
return
|
||||
}
|
||||
mizu.Log.Infof("Found service %s, creating k8s proxy\n", mizu.ApiServerPodName)
|
||||
mizu.Log.Infof("Found service %s, creating k8s proxy", mizu.ApiServerPodName)
|
||||
|
||||
mizu.Log.Infof("Mizu is available at http://%s\n", kubernetes.GetMizuApiServerProxiedHostAndPath(mizuViewOptions.GuiPort))
|
||||
err = kubernetes.StartProxy(kubernetesProvider, mizuViewOptions.GuiPort, mizu.ResourcesNamespace, mizu.ApiServerPodName)
|
||||
if err != nil {
|
||||
mizu.Log.Infof("Error occured while running k8s proxy %v\n", err)
|
||||
mizu.Log.Infof("Error occured while running k8s proxy %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ require (
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/up9inc/mizu/shared v0.0.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
k8s.io/api v0.21.2
|
||||
k8s.io/apimachinery v0.21.2
|
||||
k8s.io/client-go v0.21.2
|
||||
|
||||
@@ -367,8 +367,6 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 h1:jkvpcEatpwuMF5O5LVxTnehj6YZ/aEZN4NWD/Xml4pI=
|
||||
github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7/go.mod h1:KTrHyWpO1sevuXPZwyeZc72ddWRFqNSKDFl7uVWKpg0=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
@@ -691,8 +689,9 @@ gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -2,6 +2,7 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"k8s.io/kubectl/pkg/proxy"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -13,6 +14,7 @@ const k8sProxyApiPrefix = "/"
|
||||
const mizuServicePort = 80
|
||||
|
||||
func StartProxy(kubernetesProvider *Provider, mizuPort uint16, mizuNamespace string, mizuServiceName string) error {
|
||||
mizu.Log.Debugf("Starting proxy. namespace: [%v], service name: [%s], port: [%v]", mizuNamespace, mizuServiceName, mizuPort)
|
||||
filter := &proxy.FilterServer{
|
||||
AcceptPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathAcceptRE),
|
||||
RejectPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathRejectRE),
|
||||
|
||||
221
cli/mizu/config.go
Normal file
221
cli/mizu/config.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package mizu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const separator = "="
|
||||
|
||||
var configObj = map[string]interface{}{}
|
||||
|
||||
type CommandLineFlag struct {
|
||||
CommandLineName string
|
||||
YamlHierarchyName string
|
||||
DefaultValue interface{}
|
||||
}
|
||||
|
||||
const (
|
||||
ConfigurationKeyAnalyzingDestination = "tap.dest"
|
||||
ConfigurationKeyUploadInterval = "tap.uploadInterval"
|
||||
ConfigurationKeyMizuImage = "mizuImage"
|
||||
ConfigurationKeyTelemetry = "telemetry"
|
||||
)
|
||||
|
||||
var allowedSetFlags = []CommandLineFlag{
|
||||
{
|
||||
CommandLineName: "dest",
|
||||
YamlHierarchyName: ConfigurationKeyAnalyzingDestination,
|
||||
DefaultValue: "up9.app",
|
||||
// TODO: maybe add short description that we can show
|
||||
},
|
||||
{
|
||||
CommandLineName: "uploadInterval",
|
||||
YamlHierarchyName: ConfigurationKeyUploadInterval,
|
||||
DefaultValue: 10,
|
||||
},
|
||||
{
|
||||
CommandLineName: "mizuImage",
|
||||
YamlHierarchyName: ConfigurationKeyMizuImage,
|
||||
DefaultValue: fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", Branch, SemVer),
|
||||
},
|
||||
{
|
||||
CommandLineName: "telemetry",
|
||||
YamlHierarchyName: ConfigurationKeyTelemetry,
|
||||
DefaultValue: true,
|
||||
},
|
||||
}
|
||||
|
||||
func GetString(key string) string {
|
||||
return fmt.Sprintf("%v", getValueFromMergedConfig(key))
|
||||
}
|
||||
|
||||
func GetBool(key string) bool {
|
||||
stringVal := GetString(key)
|
||||
Log.Debugf("Found string value %v", stringVal)
|
||||
|
||||
val, err := strconv.ParseBool(stringVal)
|
||||
if err != nil {
|
||||
Log.Warningf(uiUtils.Red, fmt.Sprintf( "Invalid value %v for key %s, expected bool", stringVal, key))
|
||||
os.Exit(1)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func GetInt(key string) int {
|
||||
stringVal := GetString(key)
|
||||
Log.Debugf("Found string value %v", stringVal)
|
||||
|
||||
val, err := strconv.Atoi(stringVal)
|
||||
if err != nil {
|
||||
Log.Warningf(uiUtils.Red, fmt.Sprintf("Invalid value %v for key %s, expected int", stringVal, key))
|
||||
os.Exit(1)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func InitConfig(commandLineValues []string) error {
|
||||
Log.Debugf("Merging default values")
|
||||
mergeDefaultValues()
|
||||
Log.Debugf("Merging config file values")
|
||||
if err1 := mergeConfigFile(); err1 != nil {
|
||||
Log.Infof(fmt.Sprintf(uiUtils.Red, "Invalid config file\n"))
|
||||
return err1
|
||||
}
|
||||
Log.Debugf("Merging command line values")
|
||||
if err2 := mergeCommandLineFlags(commandLineValues); err2 != nil {
|
||||
Log.Infof(fmt.Sprintf(uiUtils.Red, "Invalid commanad argument\n"))
|
||||
return err2
|
||||
}
|
||||
finalConfigPrettified, _ := uiUtils.PrettyJson(configObj)
|
||||
Log.Debugf("Merged all config successfully\n Final config: %v", finalConfigPrettified)
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTemplateConfig() string {
|
||||
templateConfig := map[string]interface{}{}
|
||||
for _, allowedFlag := range allowedSetFlags {
|
||||
addToConfigObj(allowedFlag.YamlHierarchyName, allowedFlag.DefaultValue, templateConfig)
|
||||
}
|
||||
prettifiedConfig, _ := uiUtils.PrettyYaml(templateConfig)
|
||||
return prettifiedConfig
|
||||
}
|
||||
|
||||
func GetConfigStr() string {
|
||||
val, _ := uiUtils.PrettyYaml(configObj)
|
||||
return val
|
||||
}
|
||||
|
||||
func getValueFromMergedConfig(key string) interface{} {
|
||||
if a, ok := configObj[key]; ok {
|
||||
return a
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeDefaultValues() {
|
||||
for _, allowedFlag := range allowedSetFlags {
|
||||
Log.Debugf("Setting %v to %v", allowedFlag.YamlHierarchyName, allowedFlag.DefaultValue)
|
||||
configObj[allowedFlag.YamlHierarchyName] = allowedFlag.DefaultValue
|
||||
}
|
||||
}
|
||||
|
||||
func mergeConfigFile() error {
|
||||
Log.Debugf("Merging mizu config file values")
|
||||
home, homeDirErr := os.UserHomeDir()
|
||||
if homeDirErr != nil {
|
||||
return nil
|
||||
}
|
||||
reader, openErr := os.Open(path.Join(home, ".mizu", "config.yaml"))
|
||||
if openErr != nil {
|
||||
return nil
|
||||
}
|
||||
buf, readErr := ioutil.ReadAll(reader)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal(buf, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range m {
|
||||
addToConfig(k, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addToConfig(prefix string, value interface{}) {
|
||||
typ := reflect.TypeOf(value).Kind()
|
||||
if typ == reflect.Map {
|
||||
for k1, v1 := range value.(map[string]interface{}) {
|
||||
addToConfig(fmt.Sprintf("%s.%s", prefix, k1), v1)
|
||||
}
|
||||
} else {
|
||||
validateConfigFileKey(prefix)
|
||||
configObj[prefix] = value
|
||||
}
|
||||
}
|
||||
|
||||
func mergeCommandLineFlags(commandLineValues []string) error {
|
||||
Log.Debugf("Merging Command line flags")
|
||||
for _, e := range commandLineValues {
|
||||
if !strings.Contains(e, separator) {
|
||||
return errors.New(fmt.Sprintf("invalid set argument %s", e))
|
||||
}
|
||||
split := strings.SplitN(e, separator, 2)
|
||||
if len(split) != 2 {
|
||||
return errors.New(fmt.Sprintf("invalid set argument %s", e))
|
||||
}
|
||||
setFlagKey, argumentValue := split[0], split[1]
|
||||
argumentNameInConfig, err := flagFromAllowed(setFlagKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configObj[argumentNameInConfig] = argumentValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func flagFromAllowed(setFlagKey string) (string, error) {
|
||||
for _, allowedFlag := range allowedSetFlags {
|
||||
if strings.ToLower(allowedFlag.CommandLineName) == strings.ToLower(setFlagKey) {
|
||||
return allowedFlag.YamlHierarchyName, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New(fmt.Sprintf("invalid set argument %s", setFlagKey))
|
||||
}
|
||||
|
||||
func validateConfigFileKey(configFileKey string) {
|
||||
for _, allowedFlag := range allowedSetFlags {
|
||||
if allowedFlag.YamlHierarchyName == configFileKey {
|
||||
return
|
||||
}
|
||||
}
|
||||
Log.Info(fmt.Sprintf("Unknown argument: %s. Exit", configFileKey))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func addToConfigObj(key string, value interface{}, configObj map[string]interface{}) {
|
||||
typ := reflect.TypeOf(value).Kind()
|
||||
if typ != reflect.Map {
|
||||
if strings.Contains(key, ".") {
|
||||
split := strings.SplitN(key, ".", 2)
|
||||
firstLevelKey := split[0]
|
||||
if _, ok := configObj[firstLevelKey]; !ok {
|
||||
configObj[firstLevelKey] = map[string]interface{}{}
|
||||
}
|
||||
addToConfigObj(split[1], value, configObj[firstLevelKey].(map[string]interface{}))
|
||||
} else {
|
||||
configObj[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,3 @@ const (
|
||||
TapperDaemonSetName = "mizu-tapper-daemon-set"
|
||||
TapperPodName = "mizu-tapper"
|
||||
)
|
||||
|
||||
const (
|
||||
Black = "\033[1;30m%s\033[0m"
|
||||
Red = "\033[1;31m%s\033[0m"
|
||||
Green = "\033[1;32m%s\033[0m"
|
||||
Yellow = "\033[1;33m%s\033[0m"
|
||||
Purple = "\033[1;34m%s\033[0m"
|
||||
Magenta = "\033[1;35m%s\033[0m"
|
||||
Teal = "\033[1;36m%s\033[0m"
|
||||
White = "\033[1;37m%s\033[0m"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ var format = logging.MustStringFormatter(
|
||||
)
|
||||
|
||||
func InitLogger() {
|
||||
homeDirPath, err := os.UserHomeDir()
|
||||
homeDirPath, _ := os.UserHomeDir()
|
||||
mizuDirPath := path.Join(homeDirPath, ".mizu")
|
||||
if err := os.MkdirAll(mizuDirPath, os.ModePerm); err != nil {
|
||||
panic(fmt.Sprintf("Failed creating .mizu dir: %v, err %v", mizuDirPath, err))
|
||||
|
||||
@@ -10,6 +10,11 @@ import (
|
||||
const telemetryUrl = "https://us-east4-up9-prod.cloudfunctions.net/mizu-telemetry"
|
||||
|
||||
func ReportRun(cmd string, args interface{}) {
|
||||
if !GetBool(ConfigurationKeyTelemetry) {
|
||||
Log.Debugf("not reporting due to config value")
|
||||
return
|
||||
}
|
||||
|
||||
if Branch != "main" {
|
||||
Log.Debugf("reporting only on main branch")
|
||||
return
|
||||
@@ -26,8 +31,7 @@ func ReportRun(cmd string, args interface{}) {
|
||||
|
||||
jsonValue, _ := json.Marshal(argsMap)
|
||||
|
||||
if resp, err := http.Post(telemetryUrl,
|
||||
"application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
||||
if resp, err := http.Post(telemetryUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
||||
Log.Debugf("error sending telemetry err: %v, response %v", err, resp)
|
||||
} else {
|
||||
Log.Debugf("Successfully reported telemetry")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/go-github/v37/github"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/shared/semver"
|
||||
"io/ioutil"
|
||||
@@ -44,7 +45,7 @@ func CheckVersionCompatibility(port uint16) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
Log.Infof(Red, fmt.Sprintf("cli version (%s) is not compatible with api version (%s)\n", SemVer, apiSemVer))
|
||||
Log.Infof(uiUtils.Red, fmt.Sprintf("cli version (%s) is not compatible with api version (%s)", SemVer, apiSemVer))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ func CheckNewerVersion() {
|
||||
client := github.NewClient(nil)
|
||||
latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "up9inc", "mizu")
|
||||
if err != nil {
|
||||
Log.Debugf("Failed to get latest release")
|
||||
Log.Debugf("[ERROR] Failed to get latest release")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,26 +67,26 @@ func CheckNewerVersion() {
|
||||
}
|
||||
}
|
||||
if versionFileUrl == "" {
|
||||
Log.Debugf("Version file not found in the latest release")
|
||||
Log.Debugf("[ERROR] Version file not found in the latest release")
|
||||
return
|
||||
}
|
||||
|
||||
res, err := http.Get(versionFileUrl)
|
||||
if err != nil {
|
||||
Log.Debugf("http.Get version asset -> %v", err)
|
||||
Log.Debugf("[ERROR] Failed to get the version file %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
Log.Debugf("ioutil.ReadAll -> %v", err)
|
||||
Log.Debugf("[ERROR] Failed to read the version file -> %v", err)
|
||||
return
|
||||
}
|
||||
gitHubVersion := string(data)
|
||||
gitHubVersion = gitHubVersion[:len(gitHubVersion)-1]
|
||||
Log.Debugf("Finished version validation, took %v", time.Since(start))
|
||||
if SemVer < gitHubVersion {
|
||||
Log.Infof(Yellow, fmt.Sprintf("Update available! %v -> %v (%v)\n", SemVer, gitHubVersion, *latestRelease.HTMLURL))
|
||||
Log.Infof(uiUtils.Yellow, fmt.Sprintf("Update available! %v -> %v (%v)", SemVer, gitHubVersion, *latestRelease.HTMLURL))
|
||||
}
|
||||
}
|
||||
|
||||
13
cli/uiUtils/colors.go
Normal file
13
cli/uiUtils/colors.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package uiUtils
|
||||
|
||||
|
||||
const (
|
||||
Black = "\033[1;30m%s\033[0m"
|
||||
Red = "\033[1;31m%s\033[0m"
|
||||
Green = "\033[1;32m%s\033[0m"
|
||||
Yellow = "\033[1;33m%s\033[0m"
|
||||
Purple = "\033[1;34m%s\033[0m"
|
||||
Magenta = "\033[1;35m%s\033[0m"
|
||||
Teal = "\033[1;36m%s\033[0m"
|
||||
White = "\033[1;37m%s\033[0m"
|
||||
)
|
||||
@@ -2,7 +2,7 @@ package uiUtils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func AskForConfirmation(s string) bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
mizu.Log.Infof(mizu.Magenta, s)
|
||||
fmt.Printf(Magenta, s)
|
||||
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
|
||||
36
cli/uiUtils/prettyString.go
Normal file
36
cli/uiUtils/prettyString.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package uiUtils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
empty = ""
|
||||
tab = "\t"
|
||||
)
|
||||
|
||||
func PrettyJson(data interface{}) (string, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent(empty, tab)
|
||||
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func PrettyYaml(data interface{}) (string, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := yaml.NewEncoder(buffer)
|
||||
encoder.SetIndent(0)
|
||||
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const (
|
||||
WebSocketMessageTypeTappedEntry WebSocketMessageType = "tappedEntry"
|
||||
WebSocketMessageTypeUpdateStatus WebSocketMessageType = "status"
|
||||
WebSocketMessageTypeAnalyzeStatus WebSocketMessageType = "analyzeStatus"
|
||||
WebsocketMessageTypeOutboundLink WebSocketMessageType = "outboundLink"
|
||||
)
|
||||
|
||||
type WebSocketMessageMetadata struct {
|
||||
@@ -31,7 +32,8 @@ type WebSocketStatusMessage struct {
|
||||
}
|
||||
|
||||
type TapStatus struct {
|
||||
Pods []PodInfo `json:"pods"`
|
||||
Pods []PodInfo `json:"pods"`
|
||||
TLSLinks []TLSLinkInfo `json:"tlsLinks"`
|
||||
}
|
||||
|
||||
type PodInfo struct {
|
||||
@@ -39,6 +41,13 @@ type PodInfo struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type TLSLinkInfo struct {
|
||||
SourceIP string `json:"sourceIP"`
|
||||
DestinationAddress string `json:"destinationAddress"`
|
||||
ResolvedDestinationName string `json:"resolvedDestinationName"`
|
||||
ResolvedSourceName string `json:"resolvedSourceName"`
|
||||
}
|
||||
|
||||
func CreateWebSocketStatusMessage(tappingStatus TapStatus) WebSocketStatusMessage {
|
||||
return WebSocketStatusMessage{
|
||||
WebSocketMessageMetadata: &WebSocketMessageMetadata{
|
||||
|
||||
@@ -8,4 +8,5 @@ require (
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
||||
github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758
|
||||
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4 h1:NJOOlc6ZJjix0A1rAU+nxruZtR8KboG1848yqpIUo4M=
|
||||
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4/go.mod h1:DQPxZS994Ld1Y8uwnJT+dRL04XPD0cElP/pHH/zEBHM=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
|
||||
@@ -5,21 +5,25 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/bradleyfalzon/tlsx"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const checkTLSPacketAmount = 100
|
||||
|
||||
type httpReaderDataMsg struct {
|
||||
bytes []byte
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
type tcpID struct {
|
||||
srcIP string
|
||||
dstIP string
|
||||
srcIP string
|
||||
dstIP string
|
||||
srcPort string
|
||||
dstPort string
|
||||
}
|
||||
@@ -42,28 +46,44 @@ func (tid *tcpID) String() string {
|
||||
* Implements io.Reader interface (Read)
|
||||
*/
|
||||
type httpReader struct {
|
||||
ident string
|
||||
tcpID tcpID
|
||||
isClient bool
|
||||
isHTTP2 bool
|
||||
isOutgoing bool
|
||||
msgQueue chan httpReaderDataMsg // Channel of captured reassembled tcp payload
|
||||
data []byte
|
||||
captureTime time.Time
|
||||
hexdump bool
|
||||
parent *tcpStream
|
||||
grpcAssembler GrpcAssembler
|
||||
messageCount uint
|
||||
harWriter *HarWriter
|
||||
ident string
|
||||
tcpID tcpID
|
||||
isClient bool
|
||||
isHTTP2 bool
|
||||
isOutgoing bool
|
||||
msgQueue chan httpReaderDataMsg // Channel of captured reassembled tcp payload
|
||||
data []byte
|
||||
captureTime time.Time
|
||||
hexdump bool
|
||||
parent *tcpStream
|
||||
grpcAssembler GrpcAssembler
|
||||
messageCount uint
|
||||
harWriter *HarWriter
|
||||
packetsSeen uint
|
||||
outboundLinkWriter *OutboundLinkWriter
|
||||
}
|
||||
|
||||
func (h *httpReader) Read(p []byte) (int, error) {
|
||||
var msg httpReaderDataMsg
|
||||
|
||||
ok := true
|
||||
for ok && len(h.data) == 0 {
|
||||
msg, ok = <-h.msgQueue
|
||||
h.data = msg.bytes
|
||||
|
||||
h.captureTime = msg.timestamp
|
||||
if len(h.data) > 0 {
|
||||
h.packetsSeen += 1
|
||||
}
|
||||
if h.packetsSeen < checkTLSPacketAmount && len(msg.bytes) > 5 { // packets with less than 5 bytes cause tlsx to panic
|
||||
clientHello := tlsx.ClientHello{}
|
||||
err := clientHello.Unmarshall(msg.bytes)
|
||||
if err == nil {
|
||||
fmt.Printf("Detected TLS client hello with SNI %s\n", clientHello.SNI)
|
||||
numericPort, _ := strconv.Atoi(h.tcpID.dstPort)
|
||||
h.outboundLinkWriter.WriteOutboundLink(h.tcpID.srcIP, h.tcpID.dstIP, numericPort, clientHello.SNI, TLSProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok || len(h.data) == 0 {
|
||||
return 0, io.EOF
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
package tap
|
||||
|
||||
type OutboundLinkProtocol string
|
||||
|
||||
const (
|
||||
TLSProtocol OutboundLinkProtocol = "tls"
|
||||
)
|
||||
|
||||
type OutboundLink struct {
|
||||
Src string
|
||||
DstIP string
|
||||
DstPort int
|
||||
SuggestedResolvedName string
|
||||
SuggestedProtocol OutboundLinkProtocol
|
||||
}
|
||||
|
||||
func NewOutboundLinkWriter() *OutboundLinkWriter {
|
||||
@@ -16,11 +24,13 @@ type OutboundLinkWriter struct {
|
||||
OutChan chan *OutboundLink
|
||||
}
|
||||
|
||||
func (olw *OutboundLinkWriter) WriteOutboundLink(src string, DstIP string, DstPort int) {
|
||||
func (olw *OutboundLinkWriter) WriteOutboundLink(src string, DstIP string, DstPort int, SuggestedResolvedName string, SuggestedProtocol OutboundLinkProtocol) {
|
||||
olw.OutChan <- &OutboundLink{
|
||||
Src: src,
|
||||
DstIP: DstIP,
|
||||
DstPort: DstPort,
|
||||
SuggestedResolvedName: SuggestedResolvedName,
|
||||
SuggestedProtocol: SuggestedProtocol,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.T
|
||||
dstPort := int(tcp.DstPort)
|
||||
|
||||
if factory.shouldNotifyOnOutboundLink(dstIp, dstPort) {
|
||||
factory.outbountLinkWriter.WriteOutboundLink(net.Src().String(), dstIp, dstPort)
|
||||
factory.outbountLinkWriter.WriteOutboundLink(net.Src().String(), dstIp, dstPort, "", "")
|
||||
}
|
||||
props := factory.getStreamProps(srcIp, dstIp, dstPort)
|
||||
isHTTP := props.isTapTarget
|
||||
@@ -57,11 +57,12 @@ func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.T
|
||||
srcPort: transport.Src().String(),
|
||||
dstPort: transport.Dst().String(),
|
||||
},
|
||||
hexdump: *hexdump,
|
||||
parent: stream,
|
||||
isClient: true,
|
||||
isOutgoing: props.isOutgoing,
|
||||
harWriter: factory.harWriter,
|
||||
hexdump: *hexdump,
|
||||
parent: stream,
|
||||
isClient: true,
|
||||
isOutgoing: props.isOutgoing,
|
||||
harWriter: factory.harWriter,
|
||||
outboundLinkWriter: factory.outbountLinkWriter,
|
||||
}
|
||||
stream.server = httpReader{
|
||||
msgQueue: make(chan httpReaderDataMsg),
|
||||
@@ -72,10 +73,11 @@ func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.T
|
||||
srcPort: transport.Dst().String(),
|
||||
dstPort: transport.Src().String(),
|
||||
},
|
||||
hexdump: *hexdump,
|
||||
parent: stream,
|
||||
isOutgoing: props.isOutgoing,
|
||||
harWriter: factory.harWriter,
|
||||
hexdump: *hexdump,
|
||||
parent: stream,
|
||||
isOutgoing: props.isOutgoing,
|
||||
harWriter: factory.harWriter,
|
||||
outboundLinkWriter: factory.outbountLinkWriter,
|
||||
}
|
||||
factory.wg.Add(2)
|
||||
// Start reading from channels stream.client.bytes and stream.server.bytes
|
||||
@@ -131,6 +133,5 @@ func (factory *tcpStreamFactory) shouldNotifyOnOutboundLink(dstIP string, dstPor
|
||||
|
||||
type streamProps struct {
|
||||
isTapTarget bool
|
||||
isOutgoing bool
|
||||
isOutgoing bool
|
||||
}
|
||||
|
||||
|
||||
22900
ui/package-lock.json
generated
22900
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, {useState} from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import './App.sass';
|
||||
import logo from './components/assets/Mizu-logo.svg';
|
||||
import {Button} from "@material-ui/core";
|
||||
import {Button, Snackbar} from "@material-ui/core";
|
||||
import {HarPage} from "./components/HarPage";
|
||||
import Tooltip from "./components/Tooltip";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
import MuiAlert from '@material-ui/lab/Alert';
|
||||
import Api from "./helpers/api";
|
||||
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
@@ -15,11 +17,37 @@ const useStyles = makeStyles(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const api = new Api();
|
||||
|
||||
const App = () => {
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const [analyzeStatus, setAnalyzeStatus] = useState(null);
|
||||
const [showTLSWarning, setShowTLSWarning] = useState(false);
|
||||
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
|
||||
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const recentTLSLinks = await api.getRecentTLSLinks();
|
||||
|
||||
if (recentTLSLinks?.length > 0) {
|
||||
setAddressesWithTLS(new Set([...addressesWithTLS, ...recentTLSLinks]));
|
||||
setShowTLSWarning(true);
|
||||
}
|
||||
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onTLSDetected = (destAddress: string) => {
|
||||
addressesWithTLS.add(destAddress);
|
||||
setAddressesWithTLS(new Set(addressesWithTLS));
|
||||
|
||||
if (!userDismissedTLSWarning) {
|
||||
setShowTLSWarning(true);
|
||||
}
|
||||
};
|
||||
|
||||
const analysisMessage = analyzeStatus?.isRemoteReady ?
|
||||
<span>
|
||||
@@ -88,7 +116,12 @@ const App = () => {
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
<HarPage setAnalyzeStatus={setAnalyzeStatus}/>
|
||||
<HarPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
|
||||
<Snackbar open={showTLSWarning && !userDismissedTLSWarning}>
|
||||
<MuiAlert elevation={6} variant="filled" onClose={() => setUserDismissedTLSWarning(true)} severity="warning">
|
||||
Mizu is detecting TLS traffic{addressesWithTLS.size ? ` (directed to ${Array.from(addressesWithTLS).join(", ")})` : ''}, this type of traffic will not be displayed.
|
||||
</MuiAlert>
|
||||
</Snackbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,11 +38,12 @@ enum ConnectionStatus {
|
||||
|
||||
interface HarPageProps {
|
||||
setAnalyzeStatus: (status: any) => void;
|
||||
onTLSDetected: (destAddress: string) => void;
|
||||
}
|
||||
|
||||
const api = new Api();
|
||||
|
||||
export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus}) => {
|
||||
export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected}) => {
|
||||
|
||||
const classes = useLayoutStyles();
|
||||
|
||||
@@ -93,6 +94,9 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus}) => {
|
||||
case "analyzeStatus":
|
||||
setAnalyzeStatus(message.analyzeStatus);
|
||||
break
|
||||
case "outboundLink":
|
||||
onTLSDetected(message.Data.DstIP);
|
||||
break;
|
||||
default:
|
||||
console.error(`unsupported websocket message type, Got: ${message.messageType}`)
|
||||
}
|
||||
|
||||
@@ -42,4 +42,9 @@ export default class Api {
|
||||
const response = await this.client.get(`/entries?limit=50&operator=${operator}×tamp=${timestamp}`);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
getRecentTLSLinks = async () => {
|
||||
const response = await this.client.get("/recentTLSLinks");
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user