Skip to content

Commit dabc36f

Browse files
Merge pull request #64 from Zenika/master
Release v1.7.0
2 parents 3a474a1 + 27c6e90 commit dabc36f

38 files changed

+3864
-2383
lines changed

.circleci/config.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
- karto/front/build
4545
build-back:
4646
docker:
47-
- image: cimg/go:1.16
47+
- image: cimg/go:1.17
4848
working_directory: /home/circleci/karto
4949
steps:
5050
- attach_workspace:
@@ -93,16 +93,16 @@ jobs:
9393
command: |
9494
docker build -t zenikalabs/karto .
9595
docker tag zenikalabs/karto zenikalabs/karto:v1
96-
docker tag zenikalabs/karto zenikalabs/karto:v1.6
97-
docker tag zenikalabs/karto zenikalabs/karto:v1.6.0
96+
docker tag zenikalabs/karto zenikalabs/karto:v1.7
97+
docker tag zenikalabs/karto zenikalabs/karto:v1.7.0
9898
- run:
9999
name: Push docker image
100100
command: |
101101
echo "$DOCKER_PASS" | docker login --username $DOCKER_USER --password-stdin
102102
docker push zenikalabs/karto
103103
docker push zenikalabs/karto:v1
104-
docker push zenikalabs/karto:v1.6
105-
docker push zenikalabs/karto:v1.6.0
104+
docker push zenikalabs/karto:v1.7
105+
docker push zenikalabs/karto:v1.7.0
106106
workflows:
107107
version: 2
108108
build-test-and-deploy:
+2-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
<component name="ProjectRunConfigurationManager">
2-
<configuration default="false" name="Run Back" type="GoApplicationRunConfiguration" factoryName="Go Application">
2+
<configuration default="false" name="Run back" type="GoApplicationRunConfiguration" factoryName="Go Application">
33
<module name="karto" />
44
<working_directory value="$PROJECT_DIR$/back" />
5-
<go_parameters value="-i" />
65
<kind value="PACKAGE" />
7-
<filePath value="$PROJECT_DIR$/back" />
86
<package value="karto" />
97
<directory value="$PROJECT_DIR$" />
8+
<filePath value="$PROJECT_DIR$/back/main.go" />
109
<method v="2" />
1110
</configuration>
1211
</component>

README.md

+24-13
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,35 @@ A simple static analysis tool to explore a Kubernetes cluster.
2424
## Main features
2525

2626
The left part of the screen contains the controls for the main view:
27-
- View: choose your view (workload or network policies)
28-
- Filters: filter pods by namespace, labels and name
29-
- Include ingress neighbors: display pods that can reach those in the current selection
30-
- Include egress neighbors: display pods that can be reached by those in the current selection
31-
- Auto refresh: refresh the view every 5 seconds
32-
- Auto zoom: zoom automatically to fit all elements in the screen
33-
- Show namespace prefix: include the namespace in pod names
34-
- Highlight non isolated pods (ingress/egress): color pods with no ingress/egress network policy
35-
- Always display large datasets: always try to display large sets of pods and routes (may slow down your browser)
36-
37-
The main view shows the graph of pods and allowed routes in your selection:
27+
- View: choose your view
28+
- Workloads: deployments, controllers, pods, services, ingresses... and how they interact with each other
29+
- Network policies: network routes allowed between pods, based on network policy declarations
30+
- Health: health information about the pods
31+
- Filters: filter the items to display
32+
- by pod namespace
33+
- by pod labels
34+
- by pod name
35+
- \[Network policies view only\] Include ingress neighbors: also display pods that can reach those in the current selection
36+
- \[Network policies view only\] Include egress neighbors: also display pods that can be reached by those in the current selection
37+
- Display options: customize how items are displayed
38+
- Auto-refresh: automatically refresh the view every 2 seconds
39+
- Auto-zoom: automatically resize the view to fit all the elements to display
40+
- Show namespace prefix: add the namespace to the name of the displayed items
41+
- Always display large datasets: try to render the data even if the number of item is high (may slow down your browser)
42+
- \[Network policies view only\] Highlight non isolated pods (ingress): color pods with no ingress network policy
43+
- \[Network policies view only\] Highlight non isolated pods (egress): color pods with no egress network policy
44+
- \[Health view only\] Highlight pods with container not running: color pods with at least one container not running
45+
- \[Health view only\] Highlight pods with container not ready: color pods with at least one container not ready
46+
- \[Health view only\] Highlight pods with container restarted: color pods with at least one container which restarted
47+
48+
The main view shows the graph or list of items, depending on the selected view, filters and display options:
3849
- Zoom in and out by scrolling
3950
- Drag and drop graph elements to draw the perfect map of your cluster
4051
- Hover over any graph element to display details: name, namespace, labels, isolation (ingress/egress)... and more!
4152

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

4657
## Installation
4758

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

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

104115
### Run the frontend in dev mode

back/analyzer/health/analyzer.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package health
2+
3+
import (
4+
corev1 "k8s.io/api/core/v1"
5+
"karto/analyzer/health/podhealth"
6+
"karto/types"
7+
)
8+
9+
type ClusterState struct {
10+
Pods []*corev1.Pod
11+
}
12+
13+
type AnalysisResult struct {
14+
Pods []*types.PodHealth
15+
}
16+
17+
type Analyzer interface {
18+
Analyze(clusterState ClusterState) AnalysisResult
19+
}
20+
21+
type analyzerImpl struct {
22+
podHealthAnalyzer podhealth.Analyzer
23+
}
24+
25+
func NewAnalyzer(podHealthAnalyzer podhealth.Analyzer) Analyzer {
26+
return analyzerImpl{
27+
podHealthAnalyzer: podHealthAnalyzer,
28+
}
29+
}
30+
31+
func (analyzer analyzerImpl) Analyze(clusterState ClusterState) AnalysisResult {
32+
podHealths := analyzer.podHealthOfAllPods(clusterState.Pods)
33+
return AnalysisResult{
34+
Pods: podHealths,
35+
}
36+
}
37+
38+
func (analyzer analyzerImpl) podHealthOfAllPods(pods []*corev1.Pod) []*types.PodHealth {
39+
podHealths := make([]*types.PodHealth, 0)
40+
for _, pod := range pods {
41+
podHealth := analyzer.podHealthAnalyzer.Analyze(pod)
42+
podHealths = append(podHealths, podHealth)
43+
}
44+
return podHealths
45+
}

back/analyzer/health/analyzer_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package health
2+
3+
import (
4+
"github.com/google/go-cmp/cmp"
5+
corev1 "k8s.io/api/core/v1"
6+
"karto/analyzer/health/podhealth"
7+
"karto/testutils"
8+
"karto/types"
9+
"reflect"
10+
"testing"
11+
)
12+
13+
func TestAnalyze(t *testing.T) {
14+
type args struct {
15+
clusterState ClusterState
16+
}
17+
type mocks struct {
18+
podHealth []mockPodHealthAnalyzerCall
19+
}
20+
k8sPod1 := testutils.NewPodBuilder().WithContainerStatus(true, false, 0).Build()
21+
k8sPod2 := testutils.NewPodBuilder().WithContainerStatus(true, false, 0).
22+
WithContainerStatus(false, false, 0).Build()
23+
podRef1 := types.PodRef{Name: k8sPod1.Name, Namespace: k8sPod1.Namespace}
24+
podRef2 := types.PodRef{Name: k8sPod2.Name, Namespace: k8sPod2.Namespace}
25+
podHealth1 := &types.PodHealth{Pod: podRef1, Containers: 1, ContainersRunning: 1, ContainersReady: 0,
26+
ContainersWithoutRestart: 1}
27+
podHealth2 := &types.PodHealth{Pod: podRef2, Containers: 2, ContainersRunning: 1, ContainersReady: 0,
28+
ContainersWithoutRestart: 2}
29+
tests := []struct {
30+
name string
31+
mocks mocks
32+
args args
33+
expectedAnalysisResult AnalysisResult
34+
}{
35+
{
36+
name: "delegates to sub-analyzers and merges results",
37+
mocks: mocks{
38+
podHealth: []mockPodHealthAnalyzerCall{
39+
{
40+
args: mockPodHealthAnalyzerCallArgs{
41+
pod: k8sPod1,
42+
},
43+
returnValue: podHealth1,
44+
},
45+
{
46+
args: mockPodHealthAnalyzerCallArgs{
47+
pod: k8sPod2,
48+
},
49+
returnValue: podHealth2,
50+
},
51+
},
52+
},
53+
args: args{
54+
clusterState: ClusterState{
55+
Pods: []*corev1.Pod{k8sPod1, k8sPod2},
56+
},
57+
},
58+
expectedAnalysisResult: AnalysisResult{
59+
Pods: []*types.PodHealth{podHealth1, podHealth2},
60+
},
61+
},
62+
}
63+
for _, tt := range tests {
64+
t.Run(tt.name, func(t *testing.T) {
65+
podHealthAnalyzer := createMockPodHealthAnalyzer(t, tt.mocks.podHealth)
66+
analyzer := NewAnalyzer(podHealthAnalyzer)
67+
analysisResult := analyzer.Analyze(tt.args.clusterState)
68+
if diff := cmp.Diff(tt.expectedAnalysisResult, analysisResult); diff != "" {
69+
t.Errorf("Analyze() result mismatch (-want +got):\n%s", diff)
70+
}
71+
})
72+
}
73+
}
74+
75+
type mockPodHealthAnalyzerCallArgs struct {
76+
pod *corev1.Pod
77+
}
78+
79+
type mockPodHealthAnalyzerCall struct {
80+
args mockPodHealthAnalyzerCallArgs
81+
returnValue *types.PodHealth
82+
}
83+
84+
type mockPodHealthAnalyzer struct {
85+
t *testing.T
86+
calls []mockPodHealthAnalyzerCall
87+
}
88+
89+
func (mock mockPodHealthAnalyzer) Analyze(pod *corev1.Pod) *types.PodHealth {
90+
for _, call := range mock.calls {
91+
if reflect.DeepEqual(call.args.pod, pod) {
92+
return call.returnValue
93+
}
94+
}
95+
mock.t.Fatalf("mockPodHealthAnalyzer was called with unexpected arguments:\n\tpod: %s\n", pod)
96+
return nil
97+
}
98+
99+
func createMockPodHealthAnalyzer(t *testing.T, calls []mockPodHealthAnalyzerCall) podhealth.Analyzer {
100+
return mockPodHealthAnalyzer{
101+
t: t,
102+
calls: calls,
103+
}
104+
}
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package podhealth
2+
3+
import (
4+
corev1 "k8s.io/api/core/v1"
5+
"karto/types"
6+
)
7+
8+
type Analyzer interface {
9+
Analyze(pod *corev1.Pod) *types.PodHealth
10+
}
11+
12+
type analyzerImpl struct{}
13+
14+
func NewAnalyzer() Analyzer {
15+
return analyzerImpl{}
16+
}
17+
18+
func (analyzer analyzerImpl) Analyze(pod *corev1.Pod) *types.PodHealth {
19+
var containers, running, ready, withoutRestart int32
20+
for _, containerStatus := range pod.Status.ContainerStatuses {
21+
containers++
22+
if containerStatus.State.Running != nil {
23+
running++
24+
}
25+
if containerStatus.Ready {
26+
ready++
27+
}
28+
if containerStatus.RestartCount == 0 {
29+
withoutRestart++
30+
}
31+
}
32+
return &types.PodHealth{
33+
Pod: analyzer.toPodRef(pod),
34+
Containers: containers,
35+
ContainersRunning: running,
36+
ContainersReady: ready,
37+
ContainersWithoutRestart: withoutRestart,
38+
}
39+
}
40+
41+
func (analyzer analyzerImpl) toPodRef(pod *corev1.Pod) types.PodRef {
42+
return types.PodRef{
43+
Name: pod.Name,
44+
Namespace: pod.Namespace,
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package podhealth
2+
3+
import (
4+
"github.com/google/go-cmp/cmp"
5+
corev1 "k8s.io/api/core/v1"
6+
"karto/testutils"
7+
"karto/types"
8+
"testing"
9+
)
10+
11+
func TestAnalyze(t *testing.T) {
12+
type args struct {
13+
pod *corev1.Pod
14+
}
15+
tests := []struct {
16+
name string
17+
args args
18+
expectedPodHealth *types.PodHealth
19+
}{
20+
{
21+
name: "pod health is the aggregation of its container statuses",
22+
args: args{
23+
pod: testutils.NewPodBuilder().WithName("pod1").
24+
WithContainerStatus(true, true, 0).
25+
WithContainerStatus(true, true, 0).
26+
Build(),
27+
},
28+
expectedPodHealth: &types.PodHealth{
29+
Pod: types.PodRef{Name: "pod1", Namespace: "default"},
30+
Containers: 2,
31+
ContainersRunning: 2,
32+
ContainersReady: 2,
33+
ContainersWithoutRestart: 2,
34+
},
35+
},
36+
{
37+
name: "only containers with a Running state are counted as running",
38+
args: args{
39+
pod: testutils.NewPodBuilder().WithName("pod1").
40+
WithContainerStatus(true, true, 0).
41+
WithContainerStatus(false, true, 0).
42+
Build(),
43+
},
44+
expectedPodHealth: &types.PodHealth{
45+
Pod: types.PodRef{Name: "pod1", Namespace: "default"},
46+
Containers: 2,
47+
ContainersRunning: 1,
48+
ContainersReady: 2,
49+
ContainersWithoutRestart: 2,
50+
},
51+
},
52+
{
53+
name: "only containers marked as ready are counted as ready",
54+
args: args{
55+
pod: testutils.NewPodBuilder().WithName("pod1").
56+
WithContainerStatus(true, false, 0).
57+
WithContainerStatus(true, true, 0).
58+
Build(),
59+
},
60+
expectedPodHealth: &types.PodHealth{
61+
Pod: types.PodRef{Name: "pod1", Namespace: "default"},
62+
Containers: 2,
63+
ContainersRunning: 2,
64+
ContainersReady: 1,
65+
ContainersWithoutRestart: 2,
66+
},
67+
},
68+
{
69+
name: "only containers with zero restarts are counted as without restarts",
70+
args: args{
71+
pod: testutils.NewPodBuilder().WithName("pod1").
72+
WithContainerStatus(true, true, 0).
73+
WithContainerStatus(true, true, 2).
74+
Build(),
75+
},
76+
expectedPodHealth: &types.PodHealth{
77+
Pod: types.PodRef{Name: "pod1", Namespace: "default"},
78+
Containers: 2,
79+
ContainersRunning: 2,
80+
ContainersReady: 2,
81+
ContainersWithoutRestart: 1,
82+
},
83+
},
84+
}
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
analyzer := NewAnalyzer()
88+
podHealth := analyzer.Analyze(tt.args.pod)
89+
if diff := cmp.Diff(tt.expectedPodHealth, podHealth); diff != "" {
90+
t.Errorf("Analyze() result mismatch (-want +got):\n%s", diff)
91+
}
92+
})
93+
}
94+
}

0 commit comments

Comments
 (0)