Skip to content

Commit

Permalink
Merge pull request #64 from Zenika/master
Browse files Browse the repository at this point in the history
Release v1.7.0
  • Loading branch information
RomainVernoux authored Oct 30, 2021
2 parents 3a474a1 + 27c6e90 commit dabc36f
Show file tree
Hide file tree
Showing 38 changed files with 3,864 additions and 2,383 deletions.
10 changes: 5 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- karto/front/build
build-back:
docker:
- image: cimg/go:1.16
- image: cimg/go:1.17
working_directory: /home/circleci/karto
steps:
- attach_workspace:
Expand Down Expand Up @@ -93,16 +93,16 @@ jobs:
command: |
docker build -t zenikalabs/karto .
docker tag zenikalabs/karto zenikalabs/karto:v1
docker tag zenikalabs/karto zenikalabs/karto:v1.6
docker tag zenikalabs/karto zenikalabs/karto:v1.6.0
docker tag zenikalabs/karto zenikalabs/karto:v1.7
docker tag zenikalabs/karto zenikalabs/karto:v1.7.0
- run:
name: Push docker image
command: |
echo "$DOCKER_PASS" | docker login --username $DOCKER_USER --password-stdin
docker push zenikalabs/karto
docker push zenikalabs/karto:v1
docker push zenikalabs/karto:v1.6
docker push zenikalabs/karto:v1.6.0
docker push zenikalabs/karto:v1.7
docker push zenikalabs/karto:v1.7.0
workflows:
version: 2
build-test-and-deploy:
Expand Down
5 changes: 2 additions & 3 deletions .run/Back.run.xml → .run/Run back.run.xml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Back" type="GoApplicationRunConfiguration" factoryName="Go Application">
<configuration default="false" name="Run back" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="karto" />
<working_directory value="$PROJECT_DIR$/back" />
<go_parameters value="-i" />
<kind value="PACKAGE" />
<filePath value="$PROJECT_DIR$/back" />
<package value="karto" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/back/main.go" />
<method v="2" />
</configuration>
</component>
37 changes: 24 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,35 @@ A simple static analysis tool to explore a Kubernetes cluster.
## Main features

The left part of the screen contains the controls for the main view:
- View: choose your view (workload or network policies)
- Filters: filter pods by namespace, labels and name
- Include ingress neighbors: display pods that can reach those in the current selection
- Include egress neighbors: display pods that can be reached by those in the current selection
- Auto refresh: refresh the view every 5 seconds
- Auto zoom: zoom automatically to fit all elements in the screen
- Show namespace prefix: include the namespace in pod names
- Highlight non isolated pods (ingress/egress): color pods with no ingress/egress network policy
- Always display large datasets: always try to display large sets of pods and routes (may slow down your browser)

The main view shows the graph of pods and allowed routes in your selection:
- View: choose your view
- Workloads: deployments, controllers, pods, services, ingresses... and how they interact with each other
- Network policies: network routes allowed between pods, based on network policy declarations
- Health: health information about the pods
- Filters: filter the items to display
- by pod namespace
- by pod labels
- by pod name
- \[Network policies view only\] Include ingress neighbors: also display pods that can reach those in the current selection
- \[Network policies view only\] Include egress neighbors: also display pods that can be reached by those in the current selection
- Display options: customize how items are displayed
- Auto-refresh: automatically refresh the view every 2 seconds
- Auto-zoom: automatically resize the view to fit all the elements to display
- Show namespace prefix: add the namespace to the name of the displayed items
- Always display large datasets: try to render the data even if the number of item is high (may slow down your browser)
- \[Network policies view only\] Highlight non isolated pods (ingress): color pods with no ingress network policy
- \[Network policies view only\] Highlight non isolated pods (egress): color pods with no egress network policy
- \[Health view only\] Highlight pods with container not running: color pods with at least one container not running
- \[Health view only\] Highlight pods with container not ready: color pods with at least one container not ready
- \[Health view only\] Highlight pods with container restarted: color pods with at least one container which restarted

The main view shows the graph or list of items, depending on the selected view, filters and display options:
- Zoom in and out by scrolling
- Drag and drop graph elements to draw the perfect map of your cluster
- Hover over any graph element to display details: name, namespace, labels, isolation (ingress/egress)... and more!

In the top left part of the screen you will find action buttons to:
- Export the current graph as PNG to use it in slides or share it
- Go fullscreen and use Karto as an office (or situation room!) dashboard
- Go fullscreen and use Karto as an office (or situation room) dashboard!

## Installation

Expand Down Expand Up @@ -98,7 +109,7 @@ Simply download the Karto binary from the [releases page](https://github.com/Zen
### Prerequisites

The following tools must be available locally:
- [Go](https://golang.org/doc/install) (tested with Go 1.16)
- [Go](https://golang.org/doc/install) (tested with Go 1.17)
- [NodeJS](https://nodejs.org/en/download/) (tested with NodeJS 14)

### Run the frontend in dev mode
Expand Down
45 changes: 45 additions & 0 deletions back/analyzer/health/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package health

import (
corev1 "k8s.io/api/core/v1"
"karto/analyzer/health/podhealth"
"karto/types"
)

type ClusterState struct {
Pods []*corev1.Pod
}

type AnalysisResult struct {
Pods []*types.PodHealth
}

type Analyzer interface {
Analyze(clusterState ClusterState) AnalysisResult
}

type analyzerImpl struct {
podHealthAnalyzer podhealth.Analyzer
}

func NewAnalyzer(podHealthAnalyzer podhealth.Analyzer) Analyzer {
return analyzerImpl{
podHealthAnalyzer: podHealthAnalyzer,
}
}

func (analyzer analyzerImpl) Analyze(clusterState ClusterState) AnalysisResult {
podHealths := analyzer.podHealthOfAllPods(clusterState.Pods)
return AnalysisResult{
Pods: podHealths,
}
}

func (analyzer analyzerImpl) podHealthOfAllPods(pods []*corev1.Pod) []*types.PodHealth {
podHealths := make([]*types.PodHealth, 0)
for _, pod := range pods {
podHealth := analyzer.podHealthAnalyzer.Analyze(pod)
podHealths = append(podHealths, podHealth)
}
return podHealths
}
104 changes: 104 additions & 0 deletions back/analyzer/health/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package health

import (
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
"karto/analyzer/health/podhealth"
"karto/testutils"
"karto/types"
"reflect"
"testing"
)

func TestAnalyze(t *testing.T) {
type args struct {
clusterState ClusterState
}
type mocks struct {
podHealth []mockPodHealthAnalyzerCall
}
k8sPod1 := testutils.NewPodBuilder().WithContainerStatus(true, false, 0).Build()
k8sPod2 := testutils.NewPodBuilder().WithContainerStatus(true, false, 0).
WithContainerStatus(false, false, 0).Build()
podRef1 := types.PodRef{Name: k8sPod1.Name, Namespace: k8sPod1.Namespace}
podRef2 := types.PodRef{Name: k8sPod2.Name, Namespace: k8sPod2.Namespace}
podHealth1 := &types.PodHealth{Pod: podRef1, Containers: 1, ContainersRunning: 1, ContainersReady: 0,
ContainersWithoutRestart: 1}
podHealth2 := &types.PodHealth{Pod: podRef2, Containers: 2, ContainersRunning: 1, ContainersReady: 0,
ContainersWithoutRestart: 2}
tests := []struct {
name string
mocks mocks
args args
expectedAnalysisResult AnalysisResult
}{
{
name: "delegates to sub-analyzers and merges results",
mocks: mocks{
podHealth: []mockPodHealthAnalyzerCall{
{
args: mockPodHealthAnalyzerCallArgs{
pod: k8sPod1,
},
returnValue: podHealth1,
},
{
args: mockPodHealthAnalyzerCallArgs{
pod: k8sPod2,
},
returnValue: podHealth2,
},
},
},
args: args{
clusterState: ClusterState{
Pods: []*corev1.Pod{k8sPod1, k8sPod2},
},
},
expectedAnalysisResult: AnalysisResult{
Pods: []*types.PodHealth{podHealth1, podHealth2},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
podHealthAnalyzer := createMockPodHealthAnalyzer(t, tt.mocks.podHealth)
analyzer := NewAnalyzer(podHealthAnalyzer)
analysisResult := analyzer.Analyze(tt.args.clusterState)
if diff := cmp.Diff(tt.expectedAnalysisResult, analysisResult); diff != "" {
t.Errorf("Analyze() result mismatch (-want +got):\n%s", diff)
}
})
}
}

type mockPodHealthAnalyzerCallArgs struct {
pod *corev1.Pod
}

type mockPodHealthAnalyzerCall struct {
args mockPodHealthAnalyzerCallArgs
returnValue *types.PodHealth
}

type mockPodHealthAnalyzer struct {
t *testing.T
calls []mockPodHealthAnalyzerCall
}

func (mock mockPodHealthAnalyzer) Analyze(pod *corev1.Pod) *types.PodHealth {
for _, call := range mock.calls {
if reflect.DeepEqual(call.args.pod, pod) {
return call.returnValue
}
}
mock.t.Fatalf("mockPodHealthAnalyzer was called with unexpected arguments:\n\tpod: %s\n", pod)
return nil
}

func createMockPodHealthAnalyzer(t *testing.T, calls []mockPodHealthAnalyzerCall) podhealth.Analyzer {
return mockPodHealthAnalyzer{
t: t,
calls: calls,
}
}
46 changes: 46 additions & 0 deletions back/analyzer/health/podhealth/analyzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package podhealth

import (
corev1 "k8s.io/api/core/v1"
"karto/types"
)

type Analyzer interface {
Analyze(pod *corev1.Pod) *types.PodHealth
}

type analyzerImpl struct{}

func NewAnalyzer() Analyzer {
return analyzerImpl{}
}

func (analyzer analyzerImpl) Analyze(pod *corev1.Pod) *types.PodHealth {
var containers, running, ready, withoutRestart int32
for _, containerStatus := range pod.Status.ContainerStatuses {
containers++
if containerStatus.State.Running != nil {
running++
}
if containerStatus.Ready {
ready++
}
if containerStatus.RestartCount == 0 {
withoutRestart++
}
}
return &types.PodHealth{
Pod: analyzer.toPodRef(pod),
Containers: containers,
ContainersRunning: running,
ContainersReady: ready,
ContainersWithoutRestart: withoutRestart,
}
}

func (analyzer analyzerImpl) toPodRef(pod *corev1.Pod) types.PodRef {
return types.PodRef{
Name: pod.Name,
Namespace: pod.Namespace,
}
}
94 changes: 94 additions & 0 deletions back/analyzer/health/podhealth/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package podhealth

import (
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
"karto/testutils"
"karto/types"
"testing"
)

func TestAnalyze(t *testing.T) {
type args struct {
pod *corev1.Pod
}
tests := []struct {
name string
args args
expectedPodHealth *types.PodHealth
}{
{
name: "pod health is the aggregation of its container statuses",
args: args{
pod: testutils.NewPodBuilder().WithName("pod1").
WithContainerStatus(true, true, 0).
WithContainerStatus(true, true, 0).
Build(),
},
expectedPodHealth: &types.PodHealth{
Pod: types.PodRef{Name: "pod1", Namespace: "default"},
Containers: 2,
ContainersRunning: 2,
ContainersReady: 2,
ContainersWithoutRestart: 2,
},
},
{
name: "only containers with a Running state are counted as running",
args: args{
pod: testutils.NewPodBuilder().WithName("pod1").
WithContainerStatus(true, true, 0).
WithContainerStatus(false, true, 0).
Build(),
},
expectedPodHealth: &types.PodHealth{
Pod: types.PodRef{Name: "pod1", Namespace: "default"},
Containers: 2,
ContainersRunning: 1,
ContainersReady: 2,
ContainersWithoutRestart: 2,
},
},
{
name: "only containers marked as ready are counted as ready",
args: args{
pod: testutils.NewPodBuilder().WithName("pod1").
WithContainerStatus(true, false, 0).
WithContainerStatus(true, true, 0).
Build(),
},
expectedPodHealth: &types.PodHealth{
Pod: types.PodRef{Name: "pod1", Namespace: "default"},
Containers: 2,
ContainersRunning: 2,
ContainersReady: 1,
ContainersWithoutRestart: 2,
},
},
{
name: "only containers with zero restarts are counted as without restarts",
args: args{
pod: testutils.NewPodBuilder().WithName("pod1").
WithContainerStatus(true, true, 0).
WithContainerStatus(true, true, 2).
Build(),
},
expectedPodHealth: &types.PodHealth{
Pod: types.PodRef{Name: "pod1", Namespace: "default"},
Containers: 2,
ContainersRunning: 2,
ContainersReady: 2,
ContainersWithoutRestart: 1,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
analyzer := NewAnalyzer()
podHealth := analyzer.Analyze(tt.args.pod)
if diff := cmp.Diff(tt.expectedPodHealth, podHealth); diff != "" {
t.Errorf("Analyze() result mismatch (-want +got):\n%s", diff)
}
})
}
}
Loading

0 comments on commit dabc36f

Please sign in to comment.