Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jv integration tests for runtime config #1899

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b1cdc47
X-Smart-Branch-Parent: master
JoukoVirtanen Nov 16, 2024
6364aff
X-Smart-Squash: Squashed 14 commits:
JoukoVirtanen Nov 1, 2024
dade64c
More error handling for reading the configuration. Using a volume mount
JoukoVirtanen Nov 2, 2024
95c001b
Fixed lint error
JoukoVirtanen Nov 2, 2024
ec61352
The runtime config file is only modified on the host machine
JoukoVirtanen Nov 2, 2024
0f0be47
There is now only one source code file for runtime config introspection
JoukoVirtanen Nov 3, 2024
a9f5c3c
Minor fixes for k8s
JoukoVirtanen Nov 3, 2024
e410b11
Added comments and sleeps when config does not change so we get a scr…
JoukoVirtanen Nov 3, 2024
9cdf634
Increased the afterglow period
JoukoVirtanen Nov 6, 2024
7dfd521
Update collector/lib/ConfigLoader.cpp
JoukoVirtanen Nov 6, 2024
7f1fa8a
Renamed unit test. Integration tests use a dedicated directory
JoukoVirtanen Nov 7, 2024
a30b703
Created a new assert package
JoukoVirtanen Nov 7, 2024
29f768f
Apply suggestions from code review
JoukoVirtanen Nov 7, 2024
a144acd
Split test into smaller tests
JoukoVirtanen Nov 11, 2024
4862733
Apply suggestions from code review
JoukoVirtanen Nov 12, 2024
0ae580b
Removed unneeded sort
JoukoVirtanen Nov 13, 2024
087c326
Using slices.ContainsFunc in HasConnection
JoukoVirtanen Nov 14, 2024
b70f822
Made timings in AssertRepeated configurable
JoukoVirtanen Nov 14, 2024
c724053
Always sort the connections when comparing
JoukoVirtanen Nov 14, 2024
b01f751
Not using assertMismatch
JoukoVirtanen Nov 14, 2024
678fc86
Updated comments
JoukoVirtanen Nov 14, 2024
f092de4
Apply suggestions from code review
JoukoVirtanen Nov 15, 2024
11fd614
Sorting connections inside Connections instead of SortedConnections
JoukoVirtanen Nov 16, 2024
30a7ad8
Created ElementsMatchFunc in assert pkg
JoukoVirtanen Nov 16, 2024
766747d
Using spew for logging
JoukoVirtanen Nov 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions collector/lib/ConfigLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ bool ConfigLoader::LoadConfiguration(CollectorConfig& config) {
bool ConfigLoader::LoadConfiguration(CollectorConfig& config, const YAML::Node& node) {
const auto& config_file = CONFIG_FILE.value();

if (node.IsNull() || !node.IsDefined()) {
if (node.IsNull() || !node.IsDefined() || !node.IsMap()) {
CLOG(ERROR) << "Unable to read config from " << config_file;
return false;
}

YAML::Node networking = node["networking"];
if (!networking) {
if (!networking || networking.IsNull()) {
CLOG(DEBUG) << "No networking in " << config_file;
return true;
}
Expand Down
20 changes: 14 additions & 6 deletions collector/test/ConfigLoaderTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,21 @@ TEST(CollectorConfigTest, TestYamlConfigToConfigInvalid) {
}
}

TEST(CollectorConfigTest, TestYamlConfigToConfigEmpty) {
YAML::Node yamlNode = YAML::Load("");
CollectorConfig config;
ASSERT_FALSE(ConfigLoader::LoadConfiguration(config, yamlNode));
TEST(CollectorConfigTest, TestYamlConfigToConfigEmptyOrMalformed) {
std::vector<std::string> tests = {
R"(
asdf
)",
R"()"};

for (const auto& yamlStr : tests) {
YAML::Node yamlNode = YAML::Load(yamlStr);
CollectorConfig config;
ASSERT_FALSE(ConfigLoader::LoadConfiguration(config, yamlNode));

auto runtime_config = config.GetRuntimeConfig();
auto runtime_config = config.GetRuntimeConfig();

EXPECT_FALSE(runtime_config.has_value());
EXPECT_FALSE(runtime_config.has_value());
}
}
} // namespace collector
4 changes: 4 additions & 0 deletions integration-tests/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,3 +529,7 @@ func TestUdpNetworkFlow(t *testing.T) {
}
suite.Run(t, new(suites.UdpNetworkFlow))
}

func TestRuntimeConfigFile(t *testing.T) {
suite.Run(t, new(suites.RuntimeConfigFileTestSuite))
}
93 changes: 93 additions & 0 deletions integration-tests/pkg/assert/assert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package assert

import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
"time"

"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"

"github.com/stackrox/collector/integration-tests/pkg/collector"
"github.com/stackrox/collector/integration-tests/pkg/types"
)

func AssertExternalIps(t *testing.T, enable bool, collectorIP string) {
tickTime := 1 * time.Second
timeout := 3 * time.Minute
AssertRepeated(t, tickTime, timeout, func() bool {
body, err := collector.IntrospectionQuery(collectorIP, "/state/config")
assert.NoError(t, err)
var response types.RuntimeConfig
err = json.Unmarshal(body, &response)
assert.NoError(t, err)

return response.Networking.ExternalIps.Enable == enable
})
}

func AssertNoRuntimeConfig(t *testing.T, collectorIP string) {
tickTime := 1 * time.Second
timeout := 3 * time.Minute
AssertRepeated(t, tickTime, timeout, func() bool {
body, err := collector.IntrospectionQuery(collectorIP, "/state/config")
assert.NoError(t, err)
return strings.TrimSpace(string(body)) == "{}"
})
}

func AssertRepeated(t *testing.T, tickTime time.Duration, timeout time.Duration, condition func() bool) {
tick := time.NewTicker(tickTime)
timer := time.After(timeout)

for {
select {
case <-tick.C:
if condition() {
// Condition has been met
return
}

case <-timer:
// TODO: This message should be passed in rather than hard coded here
t.Log("Timeout reached: Runtime configuration was not updated")
t.FailNow()
}
}
}

func ElementsMatchFunc[N any](expected []N, actual []N, equal func(a, b N) bool) bool {
if len(expected) != len(actual) {
return false
}

for i := range expected {
if !equal(expected[i], actual[i]) {
return false
}
}
return true
}

func ListsToAssertMsg[N any](expected []N, actual []N) string {
var expectedBuf, actualBuf bytes.Buffer
spew.Fdump(&expectedBuf, expected)
spew.Fdump(&actualBuf, actual)
return fmt.Sprintf(
"Expected elements:\n%s\n\nActual elements:\n%s\n",
expectedBuf.String(),
actualBuf.String(),
)
}

func AssertElementsMatchFunc[N any](t *testing.T, expected []N, actual []N, equal func(a, b N) bool) bool {
match := ElementsMatchFunc(expected, actual, equal)
if !match {
assertMsg := ListsToAssertMsg(expected, actual)
assert.True(t, match, assertMsg)
}
return match
}
1 change: 1 addition & 0 deletions integration-tests/pkg/collector/collector_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func NewDockerCollectorManager(e executor.Executor, name string) *DockerCollecto
"/host/etc:ro": "/etc",
"/host/usr/lib:ro": "/usr/lib",
"/host/sys/kernel/debug:ro": "/sys/kernel/debug",
"/etc/stackrox:ro": "/tmp/collector-test",
}

return &DockerCollectorManager{
Expand Down
4 changes: 2 additions & 2 deletions integration-tests/pkg/collector/introspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"github.com/stackrox/collector/integration-tests/pkg/log"
)

func (k *K8sCollectorManager) IntrospectionQuery(endpoint string) ([]byte, error) {
uri := fmt.Sprintf("http://%s:8080%s", k.IP(), endpoint)
func IntrospectionQuery(collectorIP string, endpoint string) ([]byte, error) {
uri := fmt.Sprintf("http://%s:8080%s", collectorIP, endpoint)
resp, err := http.Get(uri)
if err != nil {
return nil, err
Expand Down
59 changes: 56 additions & 3 deletions integration-tests/pkg/mock_sensor/expect_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package mock_sensor

import (
"testing"

"time"

"github.com/stretchr/testify/assert"
"github.com/thoas/go-funk"

collectorAssert "github.com/stackrox/collector/integration-tests/pkg/assert"
"github.com/stackrox/collector/integration-tests/pkg/types"
)

Expand All @@ -33,7 +33,7 @@ loop:
case <-timer:
// we know they don't match at this point, but by using
// ElementsMatch we get much better logging about the differences
return assert.ElementsMatch(t, expected, s.Connections(containerID), "timed out waiting for networks")
return assert.ElementsMatch(t, expected, s.Connections(containerID), "timed out waiting for network connections")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this file were made here https://github.com/stackrox/collector/pull/1902/files

case network := <-s.LiveConnections():
if network.GetContainerId() != containerID {
continue loop
Expand Down Expand Up @@ -72,14 +72,67 @@ loop:
if conn.GetContainerId() != containerID {
continue loop
}

if len(s.Connections(containerID)) == n {
return s.Connections(containerID)
}
}
}
}

// checkIfConnectionsMatchExpected compares a list of expected and observed connection match exactly.
func (s *MockSensor) checkIfConnectionsMatchExpected(t *testing.T, connections []types.NetworkInfo, expected []types.NetworkInfo) bool {
equal := func(c1 types.NetworkInfo, c2 types.NetworkInfo) bool {
return c1.Equal(c2)
}
// If the number of observed connections is greater than the number of expected connections they will
// never be equal. Exit early.
if len(connections) > len(expected) {
assert.FailNow(t, collectorAssert.ListsToAssertMsg(expected, connections))
}

// If the number of observed connections equals the number of expected connections, but the elements are
// different, they will never be the same. Exit early
if len(connections) == len(expected) && !collectorAssert.ElementsMatchFunc(connections, expected, equal) {
assert.FailNow(t, collectorAssert.ListsToAssertMsg(expected, connections))
}
return collectorAssert.ElementsMatchFunc(connections, expected, equal)
}

// ExpectSameElementsConnections compares a list of expected connections to the observed connections. This comparison is done at the beginning, when a new
// connection arrives, and after a timeout period. The number of connections must match and the expected and observed connections must match, but the order
// does not matter.
func (s *MockSensor) ExpectSameElementsConnections(t *testing.T, containerID string, timeout time.Duration, expected ...types.NetworkInfo) bool {
types.SortConnections(expected)

connections := s.Connections(containerID)
if s.checkIfConnectionsMatchExpected(t, connections, expected) {
return true
}

timer := time.After(timeout)

for {
select {
case <-timer:
connections := s.Connections(containerID)
success := s.checkIfConnectionsMatchExpected(t, connections, expected)
if !success {
return assert.ElementsMatch(t, expected, connections, "networking connections do not match")
}
return success
case conn := <-s.LiveConnections():
if conn.GetContainerId() != containerID {
continue
}
connections := s.Connections(containerID)
success := s.checkIfConnectionsMatchExpected(t, connections, expected)
if success {
return true
}
}
}
}

// ExpectEndpoints waits up to the timeout for the gRPC server to receive
// the list of expected Endpoints. It will first check to see if the endpoints
// have been received already, and then monitor the live feed of endpoints
Expand Down
29 changes: 14 additions & 15 deletions integration-tests/pkg/mock_sensor/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"net"
"os"
"slices"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -32,7 +33,6 @@ const (
// us to use any comparable type as the key)
type ProcessMap map[types.ProcessInfo]interface{}
type LineageMap map[types.ProcessLineage]interface{}
type ConnMap map[types.NetworkInfo]interface{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this file were made here https://github.com/stackrox/collector/pull/1902/files

type EndpointMap map[types.EndpointInfo]interface{}

type MockSensor struct {
Expand All @@ -47,7 +47,7 @@ type MockSensor struct {
processLineages map[string]LineageMap
processMutex sync.Mutex

connections map[string]ConnMap
connections map[string][]types.NetworkInfo
endpoints map[string]EndpointMap
networkMutex sync.Mutex

Expand All @@ -65,7 +65,7 @@ func NewMockSensor(test string) *MockSensor {
testName: test,
processes: make(map[string]ProcessMap),
processLineages: make(map[string]LineageMap),
connections: make(map[string]ConnMap),
connections: make(map[string][]types.NetworkInfo),
endpoints: make(map[string]EndpointMap),
}
}
Expand Down Expand Up @@ -155,11 +155,10 @@ func (m *MockSensor) Connections(containerID string) []types.NetworkInfo {
defer m.networkMutex.Unlock()

if connections, ok := m.connections[containerID]; ok {
keys := make([]types.NetworkInfo, 0, len(connections))
for k := range connections {
keys = append(keys, k)
}
return keys
conns := make([]types.NetworkInfo, len(connections))
copy(conns, connections)
types.SortConnections(conns)
return conns
}
return make([]types.NetworkInfo, 0)
}
Expand All @@ -171,8 +170,9 @@ func (m *MockSensor) HasConnection(containerID string, conn types.NetworkInfo) b
defer m.networkMutex.Unlock()

if conns, ok := m.connections[containerID]; ok {
_, exists := conns[conn]
return exists
return slices.ContainsFunc(conns, func(c types.NetworkInfo) bool {
return c.Equal(conn)
})
}

return false
Expand Down Expand Up @@ -271,7 +271,7 @@ func (m *MockSensor) Stop() {

m.processes = make(map[string]ProcessMap)
m.processLineages = make(map[string]LineageMap)
m.connections = make(map[string]ConnMap)
m.connections = make(map[string][]types.NetworkInfo)
m.endpoints = make(map[string]EndpointMap)

m.processChannel.Stop()
Expand Down Expand Up @@ -432,11 +432,10 @@ func (m *MockSensor) pushConnection(containerID string, connection *sensorAPI.Ne
CloseTimestamp: connection.GetCloseTimestamp().String(),
}

if connections, ok := m.connections[containerID]; ok {
connections[conn] = true
if _, ok := m.connections[containerID]; ok {
m.connections[containerID] = append(m.connections[containerID], conn)
} else {
connections := ConnMap{conn: true}
m.connections[containerID] = connections
m.connections[containerID] = []types.NetworkInfo{conn}
}
}

Expand Down
40 changes: 40 additions & 0 deletions integration-tests/pkg/types/network.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package types

import (
"sort"
)

const (
NilTimestamp = "<nil>"
)
Expand All @@ -16,3 +20,39 @@ func (n *NetworkInfo) IsActive() bool {
// no close timestamp means the connection is open, and active
return n.CloseTimestamp == NilTimestamp
}

func (n *NetworkInfo) Equal(other NetworkInfo) bool {
return n.LocalAddress == other.LocalAddress &&
n.RemoteAddress == other.RemoteAddress &&
n.Role == other.Role &&
n.SocketFamily == other.SocketFamily &&
n.IsActive() == other.IsActive()
}

func (n *NetworkInfo) Less(other NetworkInfo) bool {
if n.LocalAddress != other.LocalAddress {
return n.LocalAddress < other.LocalAddress
}

if n.RemoteAddress != other.RemoteAddress {
return n.RemoteAddress < other.RemoteAddress
}

if n.Role != other.Role {
return n.Role < other.Role
}

if n.SocketFamily != other.SocketFamily {
return n.SocketFamily < other.SocketFamily
}

if n.IsActive() != other.IsActive() {
return n.IsActive()
}

return false
}

func SortConnections(connections []NetworkInfo) {
sort.Slice(connections, func(i, j int) bool { return connections[i].Less(connections[j]) })
}
Loading
Loading