Skip to content

Commit 4316ac0

Browse files
committed
Add dynamic ranges to generated communication matrix
This PR is adding the following changes: 1. As requested from the partenrs, the dynamic port ranges of the cluster (Linux dynamic\private ranges, kubelet node port dynamic range, host level services dynamic range), are added to the generated communication matrix in all of the supported formats. The dynamic ranges are extracted in the following ways: - Host level services range: are set by a static defined range (9000-9999). - Node port dynamic range: we are looking for a custom defined range (network.Spec.ServiceNodePortRange), if there is no range set, we use a default static range (30000-32767) - Linux dynamic\private range: we retrive the dynamic range by reading the host sysctl (/proc/sys/net/ipv4/ip_local_port_range). 2. We also have added the option of using a custom entries file that include ranges, so partners will be able to add their own ranges if needed. 3. The e2e tests have been modified the following: - EPS vs SS: in the comparsion between the matrices, we also check if host level open ports (from ss matrix) which don't have an EPS are in the range specified in the generated commatrix. - Doc vs EPS: in the comparison between the matrices, we also chcek if the ports in the genereted commatrix which are not documented in the doc matrix, do appear in the doc ranges. 4. The custom entries samples had been modified to also include ranges. 5. commatrix.go file's unit tests had been modified to allow mocking of a debugpod in the tests. For that sense, we have added to the commatrixCreator struct a utils field similar to what's done in the ConnectionCheck struct.
1 parent 71b9c6a commit 4316ac0

File tree

10 files changed

+570
-173
lines changed

10 files changed

+570
-173
lines changed

cmd/generate/generate.go

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ import (
66
"path/filepath"
77
"slices"
88
"strings"
9-
"time"
10-
11-
"context"
129

1310
"github.com/openshift-kni/commatrix/pkg/client"
1411
commatrixcreator "github.com/openshift-kni/commatrix/pkg/commatrix-creator"
@@ -20,13 +17,9 @@ import (
2017
configv1 "github.com/openshift/api/config/v1"
2118
log "github.com/sirupsen/logrus"
2219
"github.com/spf13/cobra"
23-
corev1 "k8s.io/api/core/v1"
24-
apierrors "k8s.io/apimachinery/pkg/api/errors"
25-
"k8s.io/apimachinery/pkg/util/wait"
2620
"k8s.io/cli-runtime/pkg/genericclioptions"
2721
"k8s.io/cli-runtime/pkg/genericiooptions"
2822
"k8s.io/kubectl/pkg/util/templates"
29-
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
3023

3124
"github.com/openshift-kni/commatrix/pkg/types"
3225
)
@@ -261,8 +254,10 @@ func generateMatrix(o *GenerateOptions, deployment types.Deployment, platformTyp
261254
return nil, fmt.Errorf("failed creating the endpointslices exporter %s", err)
262255
}
263256

257+
utilsHelpers := utils.New(o.cs)
258+
264259
log.Debug("Creating communication matrix")
265-
commMatrix, err := commatrixcreator.New(epExporter, o.customEntriesPath, o.customEntriesFormat, platformType, deployment, ipv6Enabled)
260+
commMatrix, err := commatrixcreator.New(epExporter, o.customEntriesPath, o.customEntriesFormat, platformType, deployment, ipv6Enabled, utilsHelpers)
266261
if err != nil {
267262
return nil, err
268263
}
@@ -301,21 +296,6 @@ func generateSS(o *GenerateOptions, deployment types.Deployment) (*types.ComMatr
301296
defer func() {
302297
if delErr := o.utilsHelpers.DeleteNamespace(consts.DefaultDebugNamespace); delErr != nil {
303298
log.Warnf("failed to delete namespace %s: %v", consts.DefaultDebugNamespace, delErr)
304-
return
305-
}
306-
if pollErr := wait.PollUntilContextTimeout(context.TODO(), time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) {
307-
ns := &corev1.Namespace{}
308-
err := o.cs.Get(context.TODO(), ctrlclient.ObjectKey{Name: consts.DefaultDebugNamespace}, ns)
309-
if apierrors.IsNotFound(err) {
310-
return true, nil
311-
}
312-
if err != nil {
313-
log.Warningf("retrying due to error: %v", err)
314-
return false, nil // keep retrying
315-
}
316-
return false, nil
317-
}); pollErr != nil {
318-
log.Errorf("error while waiting for namespace %s deletion: %v", consts.DefaultDebugNamespace, pollErr)
319299
}
320300
}()
321301

pkg/commatrix-creator/commatrix.go

Lines changed: 173 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
package commatrixcreator
22

33
import (
4+
"context"
45
"fmt"
56
"io"
67
"os"
78
"path/filepath"
89
"slices"
10+
"strconv"
11+
"strings"
912

1013
log "github.com/sirupsen/logrus"
14+
corev1 "k8s.io/api/core/v1"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1116

17+
"github.com/openshift-kni/commatrix/pkg/consts"
1218
"github.com/openshift-kni/commatrix/pkg/endpointslices"
1319
"github.com/openshift-kni/commatrix/pkg/mcp"
1420
"github.com/openshift-kni/commatrix/pkg/types"
21+
"github.com/openshift-kni/commatrix/pkg/utils"
1522
configv1 "github.com/openshift/api/config/v1"
23+
clientOptions "sigs.k8s.io/controller-runtime/pkg/client"
1624
)
1725

1826
type CommunicationMatrixCreator struct {
@@ -22,16 +30,18 @@ type CommunicationMatrixCreator struct {
2230
platformType configv1.PlatformType
2331
deployment types.Deployment
2432
ipv6Enabled bool
33+
utilsHelpers utils.UtilsInterface
2534
}
2635

27-
func New(exporter *endpointslices.EndpointSlicesExporter, customEntriesPath string, customEntriesFormat string, platformType configv1.PlatformType, deployment types.Deployment, ipv6Enabled bool) (*CommunicationMatrixCreator, error) {
36+
func New(exporter *endpointslices.EndpointSlicesExporter, customEntriesPath string, customEntriesFormat string, platformType configv1.PlatformType, deployment types.Deployment, ipv6Enabled bool, utilsHelpers utils.UtilsInterface) (*CommunicationMatrixCreator, error) {
2837
return &CommunicationMatrixCreator{
2938
exporter: exporter,
3039
customEntriesPath: customEntriesPath,
3140
customEntriesFormat: customEntriesFormat,
3241
platformType: platformType,
3342
deployment: deployment,
3443
ipv6Enabled: ipv6Enabled,
44+
utilsHelpers: utilsHelpers,
3545
}, nil
3646
}
3747

@@ -74,23 +84,33 @@ func (cm *CommunicationMatrixCreator) CreateEndpointMatrix() (*types.ComMatrix,
7484
staticEntries = expandStaticEntriesByPool(staticEntries, PoolRolesForStaticEntriesExpansion)
7585
epSliceComDetails = append(epSliceComDetails, staticEntries...)
7686

87+
var customMatrix *types.ComMatrix
7788
if cm.customEntriesPath != "" {
7889
log.Debug("Loading custom entries from file")
79-
customComDetails, err := cm.GetComDetailsListFromFile()
90+
customMatrix, err = cm.GetComMatrixFromFile()
8091
if err != nil {
8192
log.Errorf("Failed adding custom entries: %s", err)
8293
return nil, fmt.Errorf("failed adding custom entries: %s", err)
8394
}
84-
epSliceComDetails = append(epSliceComDetails, customComDetails...)
95+
epSliceComDetails = append(epSliceComDetails, customMatrix.Matrix...)
8596
}
8697

87-
commMatrix := &types.ComMatrix{Matrix: epSliceComDetails}
98+
dynamicRanges, err := cm.getDynamicRanges()
99+
if err != nil {
100+
log.Errorf("Failed to get dynamic ranges: %v", err)
101+
return nil, fmt.Errorf("failed to get dynamic ranges: %w", err)
102+
}
103+
if customMatrix != nil && len(customMatrix.DynamicRanges) > 0 {
104+
dynamicRanges = append(dynamicRanges, customMatrix.DynamicRanges...)
105+
}
106+
107+
commMatrix := &types.ComMatrix{Matrix: epSliceComDetails, DynamicRanges: dynamicRanges}
88108
log.Debug("Sorting ComMatrix and removing duplicates")
89109
commMatrix.SortAndRemoveDuplicates()
90110
return commMatrix, nil
91111
}
92112

93-
func (cm *CommunicationMatrixCreator) GetComDetailsListFromFile() ([]types.ComDetails, error) {
113+
func (cm *CommunicationMatrixCreator) GetComMatrixFromFile() (*types.ComMatrix, error) {
94114
log.Debugf("Opening file %s", cm.customEntriesPath)
95115
f, err := os.Open(filepath.Clean(cm.customEntriesPath))
96116
if err != nil {
@@ -107,7 +127,7 @@ func (cm *CommunicationMatrixCreator) GetComDetailsListFromFile() ([]types.ComDe
107127
}
108128

109129
log.Debugf("Unmarshalling file content with format %s", cm.customEntriesFormat)
110-
res, err := types.ParseToComDetailsList(raw, cm.customEntriesFormat)
130+
res, err := types.ParseToComMatrix(raw, cm.customEntriesFormat)
111131
if err != nil {
112132
log.Errorf("Failed to unmarshal %s file: %v", cm.customEntriesFormat, err)
113133
return nil, fmt.Errorf("failed to unmarshal custom entries file: %v", err)
@@ -174,3 +194,150 @@ func expandStaticEntriesByPool(staticEntries []types.ComDetails, poolToRoles map
174194
}
175195
return out
176196
}
197+
198+
func (cm *CommunicationMatrixCreator) getDynamicRanges() ([]types.DynamicRange, error) {
199+
log.Debug("Getting dynamic ranges")
200+
dynamicRanges := types.HostLevelServicesDynamicRange
201+
202+
nodePortDynamicRange, err := cm.getNodePortDynamicRange()
203+
if err != nil {
204+
log.Errorf("Failed to get node port dynamic range: %v", err)
205+
return nil, fmt.Errorf("failed to get node port dynamic range: %w", err)
206+
}
207+
dynamicRanges = append(dynamicRanges, nodePortDynamicRange...)
208+
209+
linuxDynamicPrivateRange, err := cm.getLinuxDynamicPrivateRange()
210+
if err != nil {
211+
log.Errorf("Failed to get Linux dynamic private range: %v", err)
212+
return nil, fmt.Errorf("failed to get Linux dynamic private range: %w", err)
213+
}
214+
dynamicRanges = append(dynamicRanges, linuxDynamicPrivateRange...)
215+
216+
return dynamicRanges, nil
217+
}
218+
219+
// GetNodePortDynamicRange returns the cluster's Service NodePort range as dynamic ranges.
220+
// If the cluster does not define a custom range, it falls back to the Kubernetes default (30000-32767).
221+
func (cm *CommunicationMatrixCreator) getNodePortDynamicRange() ([]types.DynamicRange, error) {
222+
log.Debug("Getting node port dynamic range")
223+
network := &configv1.Network{}
224+
if err := cm.exporter.Get(context.TODO(), clientOptions.ObjectKey{Name: "cluster"}, network); err != nil {
225+
log.Errorf("Failed to get Network config: %v", err)
226+
return nil, fmt.Errorf("failed to get Network config: %w", err)
227+
}
228+
229+
dr := types.KubeletNodePortDefaultDynamicRange
230+
rangeStr := strings.TrimSpace(network.Spec.ServiceNodePortRange)
231+
if rangeStr == "" {
232+
log.Debug("ServiceNodePortRange not set; using default")
233+
return dr, nil
234+
}
235+
236+
minPort, maxPort, err := parsePortRange(rangeStr)
237+
if err != nil {
238+
log.Errorf("Invalid ServiceNodePortRange format %q: %v", rangeStr, err)
239+
return nil, fmt.Errorf("invalid ServiceNodePortRange format %q: %w", rangeStr, err)
240+
}
241+
242+
for i := range dr {
243+
dr[i].MinPort = minPort
244+
dr[i].MaxPort = maxPort
245+
}
246+
return dr, nil
247+
}
248+
249+
// getLinuxDynamicPrivateRange retrieves the Linux dynamic/private port range from a cluster node
250+
// by reading the host sysctl:
251+
// - /proc/sys/net/ipv4/ip_local_port_range
252+
func (cm *CommunicationMatrixCreator) getLinuxDynamicPrivateRange() ([]types.DynamicRange, error) {
253+
log.Debug("Getting Linux dynamic/private port range from cluster")
254+
255+
// Pick an arbitrary node to query (ranges are expected to be consistent across nodes).
256+
nodes, err := cm.exporter.CoreV1Interface.Nodes().List(context.TODO(), metav1.ListOptions{})
257+
if err != nil {
258+
log.Errorf("Failed to list nodes: %v", err)
259+
return nil, fmt.Errorf("failed to list nodes: %w", err)
260+
}
261+
if len(nodes.Items) == 0 {
262+
return nil, fmt.Errorf("no nodes found in the cluster")
263+
}
264+
nodeName := nodes.Items[0].Name
265+
266+
// Ensure namespace exists.
267+
if err := cm.utilsHelpers.CreateNamespace(consts.DefaultDebugNamespace); err != nil {
268+
log.Errorf("Failed to create debug namespace: %v", err)
269+
return nil, fmt.Errorf("failed to create debug namespace: %w", err)
270+
}
271+
defer func() {
272+
if delErr := cm.utilsHelpers.DeleteNamespace(consts.DefaultDebugNamespace); delErr != nil {
273+
log.Warnf("failed to delete namespace %s: %v", consts.DefaultDebugNamespace, delErr)
274+
}
275+
}()
276+
277+
// Create a privileged pod on the selected node.
278+
pod, err := cm.utilsHelpers.CreatePodOnNode(nodeName, consts.DefaultDebugNamespace, consts.DefaultDebugPodImage, []string{})
279+
if err != nil {
280+
log.Errorf("Failed to create debug pod: %v", err)
281+
return nil, fmt.Errorf("failed to create debug pod: %w", err)
282+
}
283+
defer func() {
284+
if delErr := cm.utilsHelpers.DeletePod(pod); delErr != nil {
285+
log.Warnf("failed to delete debug pod %s: %v", pod.Name, delErr)
286+
}
287+
}()
288+
289+
// Wait for the pod to be running.
290+
if err := cm.utilsHelpers.WaitForPodStatus(consts.DefaultDebugNamespace, pod, corev1.PodRunning); err != nil {
291+
log.Errorf("Debug pod did not reach Running state: %v", err)
292+
return nil, fmt.Errorf("debug pod did not reach Running state: %w", err)
293+
}
294+
295+
// Read IPv4 range (applies to both IPv4 and IPv6 ephemeral ports).
296+
out, err := cm.utilsHelpers.RunCommandOnPod(pod, []string{"/bin/sh", "-c", "cat /host/proc/sys/net/ipv4/ip_local_port_range"})
297+
if err != nil {
298+
log.Errorf("Failed to read IPv4 ip_local_port_range: %v", err)
299+
return nil, fmt.Errorf("failed to read IPv4 ip_local_port_range: %w", err)
300+
}
301+
minPort, maxPort, err := parsePortRange(string(out))
302+
if err != nil {
303+
log.Errorf("Failed to parse IPv4 ip_local_port_range output: %v", err)
304+
return nil, fmt.Errorf("failed to parse IPv4 ip_local_port_range: %w", err)
305+
}
306+
307+
return []types.DynamicRange{
308+
{
309+
Direction: "Ingress",
310+
Protocol: "TCP",
311+
MinPort: minPort,
312+
MaxPort: maxPort,
313+
Description: "Linux dynamic/private ports",
314+
Optional: true,
315+
},
316+
{
317+
Direction: "Ingress",
318+
Protocol: "UDP",
319+
MinPort: minPort,
320+
MaxPort: maxPort,
321+
Description: "Linux dynamic/private ports",
322+
Optional: true,
323+
},
324+
}, nil
325+
}
326+
327+
// parsePortRange parses "MIN MAX" or "MIN-MAX" formatted ranges.
328+
func parsePortRange(s string) (int, int, error) {
329+
normalized := strings.TrimSpace(strings.ReplaceAll(s, "-", " "))
330+
fields := strings.Fields(normalized)
331+
if len(fields) != 2 {
332+
return 0, 0, fmt.Errorf("unexpected format %q", s)
333+
}
334+
minPort, err := strconv.Atoi(fields[0])
335+
if err != nil {
336+
return 0, 0, err
337+
}
338+
maxPort, err := strconv.Atoi(fields[1])
339+
if err != nil {
340+
return 0, 0, err
341+
}
342+
return minPort, maxPort, nil
343+
}

0 commit comments

Comments
 (0)