Skip to content

Commit 6aa4c8f

Browse files
committed
Add top check
1 parent a0c144b commit 6aa4c8f

File tree

4 files changed

+297
-0
lines changed

4 files changed

+297
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2525
in the container. CNJR-8736
2626
- Container processes check that records the process list from inside the
2727
container to the inspect report. CNJR-4619
28+
- Resource usage check that captures the `top` command output from inside
29+
the container to the inspect report. CNJR-4618
2830

2931
## [0.4.2] - 2025-03-25
3032

pkg/checks/container_top.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Package checks defines all of the possible Conjur Inspect checks that can
2+
// be run.
3+
package checks
4+
5+
import (
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
"github.com/cyberark/conjur-inspect/pkg/check"
11+
"github.com/cyberark/conjur-inspect/pkg/container"
12+
"github.com/cyberark/conjur-inspect/pkg/log"
13+
)
14+
15+
// ContainerTop collects resource usage information from inside a container using top
16+
type ContainerTop struct {
17+
Provider container.ContainerProvider
18+
}
19+
20+
// Describe provides a textual description of what this check gathers info on
21+
func (ct *ContainerTop) Describe() string {
22+
return fmt.Sprintf("Container top (%s)", ct.Provider.Name())
23+
}
24+
25+
// Run performs the container top resource usage collection
26+
func (ct *ContainerTop) Run(runContext *check.RunContext) []check.Result {
27+
// If there is no container ID, return
28+
if strings.TrimSpace(runContext.ContainerID) == "" {
29+
return []check.Result{}
30+
}
31+
32+
// Check if the container runtime is available
33+
runtimeKey := strings.ToLower(ct.Provider.Name())
34+
if !IsRuntimeAvailable(runContext, runtimeKey) {
35+
if runContext.VerboseErrors {
36+
return check.ErrorResult(
37+
ct,
38+
fmt.Errorf("container runtime not available"),
39+
)
40+
}
41+
return []check.Result{}
42+
}
43+
44+
containerInstance := ct.Provider.Container(runContext.ContainerID)
45+
46+
// Execute top command to get resource usage snapshot
47+
// -b flag: batch mode (non-interactive)
48+
// -n 2: run for 2 iterations
49+
stdout, stderr, err := containerInstance.Exec(
50+
"top", "-b", "-c", "-H", "-w", "512", "-n", "1",
51+
)
52+
if err != nil {
53+
return check.ErrorResult(
54+
ct,
55+
fmt.Errorf("failed to retrieve top output from container: %w", err),
56+
)
57+
}
58+
59+
// Read stdout from the command execution
60+
topBytes, err := io.ReadAll(stdout)
61+
if err != nil {
62+
return check.ErrorResult(
63+
ct,
64+
fmt.Errorf("failed to read top output: %w", err),
65+
)
66+
}
67+
68+
// Read any stderr for logging
69+
stderrBytes, _ := io.ReadAll(stderr)
70+
if len(stderrBytes) > 0 {
71+
log.Warn("stderr while reading container top: %s", string(stderrBytes))
72+
}
73+
74+
topOutput := string(topBytes)
75+
76+
// Save top output to output store
77+
outputFileName := fmt.Sprintf(
78+
"%s-container-top.log",
79+
strings.ToLower(ct.Provider.Name()),
80+
)
81+
_, err = runContext.OutputStore.Save(
82+
outputFileName,
83+
strings.NewReader(topOutput),
84+
)
85+
if err != nil {
86+
log.Warn("failed to save container top output: %s", err)
87+
}
88+
89+
// Return empty results - this check only produces raw output
90+
return []check.Result{}
91+
}

pkg/checks/container_top_test.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package checks
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
"testing"
8+
9+
"github.com/cyberark/conjur-inspect/pkg/check"
10+
"github.com/cyberark/conjur-inspect/pkg/test"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestContainerTopDescribe(t *testing.T) {
16+
provider := &test.ContainerProvider{}
17+
ct := &ContainerTop{Provider: provider}
18+
assert.Equal(t, "Container top (Test Container Provider)", ct.Describe())
19+
}
20+
21+
func TestContainerTopRunSuccessful(t *testing.T) {
22+
// Create a mock container provider with top output
23+
topOutput := strings.Join([]string{
24+
"top - 10:00:00 up 1 day, 2:34, 0 users, load average: 0.52, 0.58, 0.59",
25+
"Tasks: 4 total, 1 running, 3 sleeping, 0 stopped, 0 zombie",
26+
"%Cpu(s): 2.3 us, 1.0 sy, 0.0 ni, 96.5 id, 0.2 wa, 0.0 hi, 0.0 si, 0.0 st",
27+
"MiB Mem : 15953.8 total, 1234.5 free, 8765.4 used, 5953.9 buff/cache",
28+
"MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 6543.2 avail Mem",
29+
"",
30+
" PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND",
31+
" 1 root 20 0 715584 23456 15360 S 0.0 0.1 0:01.23 conjur",
32+
" 10 conjur 20 0 1234567 98765 43210 S 1.5 0.6 1:23.45 nginx",
33+
" 11 postgres 20 0 2345678 123456 54321 S 2.3 0.8 2:34.56 postgres",
34+
}, "\n")
35+
36+
provider := &test.ContainerProvider{
37+
ExecResponses: map[string]test.ExecResponse{
38+
"top -b -c -H -w 512 -n 1": {
39+
Stdout: strings.NewReader(topOutput),
40+
Stderr: strings.NewReader(""),
41+
Error: nil,
42+
},
43+
},
44+
}
45+
46+
ct := &ContainerTop{Provider: provider}
47+
runContext := test.NewRunContext("container123")
48+
results := ct.Run(&runContext)
49+
50+
// Should return empty results on success
51+
assert.Empty(t, results)
52+
53+
// Verify the file was saved to the output store
54+
items, err := runContext.OutputStore.Items()
55+
require.NoError(t, err)
56+
require.Len(t, items, 1)
57+
info, err := items[0].Info()
58+
require.NoError(t, err)
59+
// Provider name is "Test Container Provider" and gets ToLower() -> "test container provider"
60+
assert.Equal(t, "test container provider-container-top.log", info.Name())
61+
62+
// Verify the content was saved correctly
63+
outputStoreItemReader, cleanup, err := items[0].Open()
64+
defer cleanup()
65+
require.NoError(t, err)
66+
67+
savedContent, err := io.ReadAll(outputStoreItemReader)
68+
require.NoError(t, err)
69+
assert.Equal(t, topOutput, string(savedContent))
70+
}
71+
72+
func TestContainerTopEmptyContainerID(t *testing.T) {
73+
provider := &test.ContainerProvider{}
74+
ct := &ContainerTop{Provider: provider}
75+
runContext := test.NewRunContext("")
76+
results := ct.Run(&runContext)
77+
78+
// Should return empty results when container ID is empty
79+
assert.Empty(t, results)
80+
81+
// Verify nothing was saved
82+
items, err := runContext.OutputStore.Items()
83+
require.NoError(t, err)
84+
assert.Len(t, items, 0)
85+
}
86+
87+
func TestContainerTopWhitespaceContainerID(t *testing.T) {
88+
provider := &test.ContainerProvider{}
89+
ct := &ContainerTop{Provider: provider}
90+
runContext := test.NewRunContext(" \t\n ")
91+
results := ct.Run(&runContext)
92+
93+
// Should return empty results when container ID is only whitespace
94+
assert.Empty(t, results)
95+
96+
// Verify nothing was saved
97+
items, err := runContext.OutputStore.Items()
98+
require.NoError(t, err)
99+
assert.Len(t, items, 0)
100+
}
101+
102+
func TestContainerTopRuntimeUnavailable(t *testing.T) {
103+
provider := &test.ContainerProvider{}
104+
ct := &ContainerTop{Provider: provider}
105+
runContext := test.NewRunContext("container123")
106+
107+
// Simulate runtime unavailability
108+
runContext.ContainerRuntimeAvailability = map[string]check.RuntimeAvailability{
109+
"test container provider": {
110+
Available: false,
111+
Error: fmt.Errorf("docker not found"),
112+
},
113+
}
114+
115+
// Without VerboseErrors, should return empty results
116+
runContext.VerboseErrors = false
117+
results := ct.Run(&runContext)
118+
assert.Empty(t, results)
119+
120+
// Verify nothing was saved
121+
items, err := runContext.OutputStore.Items()
122+
require.NoError(t, err)
123+
assert.Len(t, items, 0)
124+
}
125+
126+
func TestContainerTopRuntimeUnavailableVerboseErrors(t *testing.T) {
127+
provider := &test.ContainerProvider{}
128+
ct := &ContainerTop{Provider: provider}
129+
runContext := test.NewRunContext("container123")
130+
131+
// Simulate runtime unavailability
132+
runContext.ContainerRuntimeAvailability = map[string]check.RuntimeAvailability{
133+
"test container provider": {
134+
Available: false,
135+
Error: fmt.Errorf("docker not found"),
136+
},
137+
}
138+
139+
// With VerboseErrors, should return error result
140+
runContext.VerboseErrors = true
141+
results := ct.Run(&runContext)
142+
143+
require.Len(t, results, 1)
144+
assert.Equal(t, "Container top (Test Container Provider)", results[0].Title)
145+
assert.Equal(t, check.StatusError, results[0].Status)
146+
assert.Contains(t, results[0].Message, "container runtime not available")
147+
}
148+
149+
func TestContainerTopExecError(t *testing.T) {
150+
provider := &test.ContainerProvider{
151+
ExecResponses: map[string]test.ExecResponse{
152+
"top -b -c -H -w 512 -n 1": {
153+
Stdout: nil,
154+
Stderr: strings.NewReader("error: command not found"),
155+
Error: fmt.Errorf("exec failed"),
156+
},
157+
},
158+
}
159+
160+
ct := &ContainerTop{Provider: provider}
161+
runContext := test.NewRunContext("container123")
162+
results := ct.Run(&runContext)
163+
164+
// Should return error result
165+
require.Len(t, results, 1)
166+
assert.Equal(t, "Container top (Test Container Provider)", results[0].Title)
167+
assert.Equal(t, check.StatusError, results[0].Status)
168+
assert.Contains(t, results[0].Message, "failed to retrieve top output from container")
169+
}
170+
171+
func TestContainerTopStderr(t *testing.T) {
172+
topOutput := "top - 10:00:00 up 1 day, 2:34, 0 users, load average: 0.52, 0.58, 0.59\nTasks: 4 total, 1 running, 3 sleeping, 0 stopped, 0 zombie"
173+
stderrOutput := "warning: some non-fatal warning"
174+
175+
provider := &test.ContainerProvider{
176+
ExecResponses: map[string]test.ExecResponse{
177+
"top -b -c -H -w 512 -n 1": {
178+
Stdout: strings.NewReader(topOutput),
179+
Stderr: strings.NewReader(stderrOutput),
180+
Error: nil,
181+
},
182+
},
183+
}
184+
185+
ct := &ContainerTop{Provider: provider}
186+
runContext := test.NewRunContext("container123")
187+
results := ct.Run(&runContext)
188+
189+
// Should still succeed with empty results
190+
assert.Empty(t, results)
191+
192+
// Verify the output was saved despite stderr
193+
items, err := runContext.OutputStore.Items()
194+
require.NoError(t, err)
195+
require.Len(t, items, 1)
196+
}

pkg/cmd/default_report.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ func defaultReportSections() []report.Section {
127127
Provider: &container.PodmanProvider{},
128128
},
129129

130+
// Container top
131+
&checks.ContainerTop{
132+
Provider: &container.DockerProvider{},
133+
},
134+
&checks.ContainerTop{
135+
Provider: &container.PodmanProvider{},
136+
},
137+
130138
// Container config
131139
&checks.ConjurConfig{
132140
Provider: &container.DockerProvider{},

0 commit comments

Comments
 (0)