Skip to content

Commit d897bf3

Browse files
feat: Improve generation of Ingress rules
We used to only return which host(s) each rule would apply to. This PR enhances the formatting of ingress rules, returning them as a JSON object with more information, such as the URL path they are defined on and which serviceName:port the rules are for.
1 parent 1a51653 commit d897bf3

File tree

5 files changed

+236
-18
lines changed

5 files changed

+236
-18
lines changed

components/processors/observek8sattributesprocessor/ingressactions.go

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,88 @@ import (
1010
const (
1111
IngressRulesAttributeKey = "rules"
1212
IngressLoadBalancerAttributeKey = "loadBalancer"
13+
hostKey = "host"
14+
rulesKey = "rules"
15+
httpRulesKey = "httpRules"
16+
pathKey = "path"
17+
backendKey = "backend"
18+
serviceKey = "service"
19+
resourceKey = "resource"
20+
portKey = "port"
21+
nameKey = "name"
1322
)
1423

15-
// Adapted from https://github.com/kubernetes/kubernetes/blob/0d3b859af81e6a5f869a7766c8d45afd1c600b04/pkg/printers/internalversion/printers.go#L1373
16-
func formatIngressRules(rules []netv1.IngressRule) string {
17-
list := []string{}
24+
// formatIngressRules converts a slice of IngressRules to a minimal JSON representation.
25+
// The json structure is:
26+
//
27+
// {
28+
// "host": "example.com", (or "*" if no host is specified)
29+
// "rules": [
30+
// {
31+
// "path": "/app1",
32+
// "backend": {
33+
// "service": { (inside "backend" could be either "service" or "resource")
34+
// "name": "app1-service",
35+
// "port": 8080 (could be either a number or a string)
36+
// }
37+
// }
38+
// },
39+
// {
40+
// "path": "/app2",
41+
// "backend": {
42+
// "resource": "app2-resource", (alternative to service)
43+
// (no port here, just the name of the resource)
44+
// }
45+
// }
46+
// ]
47+
// }
48+
func formatIngressRules(rules []netv1.IngressRule) []attributes {
49+
var ret []attributes
50+
1851
for _, rule := range rules {
19-
list = append(list, rule.Host)
20-
}
21-
if len(list) == 0 {
22-
return "*"
52+
host := rule.Host
53+
if host == "" {
54+
host = "*"
55+
}
56+
57+
var httpRules []attributes
58+
if rule.HTTP != nil {
59+
for _, path := range rule.HTTP.Paths {
60+
61+
ruleInfo := attributes{
62+
pathKey: path.Path,
63+
}
64+
backend := path.Backend
65+
backendAttrs := attributes{}
66+
if backend.Service != nil {
67+
service := backend.Service
68+
serviceAttrs := attributes{
69+
nameKey: service.Name,
70+
}
71+
// Remove one level of indentation and use either the port
72+
// name or the number as "port" directly inside "service",
73+
// since we can use values of any type.
74+
if service.Port.Name != "" {
75+
serviceAttrs[portKey] = service.Port.Name
76+
} else {
77+
serviceAttrs[portKey] = service.Port.Number
78+
}
79+
backendAttrs[serviceKey] = serviceAttrs
80+
} else {
81+
backendAttrs[resourceKey] = backend.Resource.Name
82+
}
83+
ruleInfo[backendKey] = backendAttrs
84+
85+
httpRules = append(httpRules, ruleInfo)
86+
}
87+
}
88+
89+
ret = append(ret, attributes{
90+
hostKey: host,
91+
httpRulesKey: httpRules,
92+
})
2393
}
24-
ret := strings.Join(list, ",")
94+
2595
return ret
2696
}
2797

@@ -35,7 +105,8 @@ func NewIngressRulesAction() IngressRulesAction {
35105

36106
// Generates the Ingress "rules" facet.
37107
func (IngressRulesAction) ComputeAttributes(ingress netv1.Ingress) (attributes, error) {
38-
return attributes{IngressRulesAttributeKey: formatIngressRules(ingress.Spec.Rules)}, nil
108+
rules := formatIngressRules(ingress.Spec.Rules)
109+
return attributes{IngressRulesAttributeKey: rules}, nil
39110
}
40111

41112
// ---------------------------------- Ingress "loadBalancer" ----------------------------------

components/processors/observek8sattributesprocessor/ingressactions_test.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,58 @@ import "testing"
55
func TestIngressActions(t *testing.T) {
66
for _, testCase := range []k8sEventProcessorTest{
77
{
8-
name: "Ingress rules",
8+
name: "Ingress rules with host",
99
inLogs: resourceLogsFromSingleJsonEvent("./testdata/ingressEvent.json"),
1010
expectedResults: []queryWithResult{
11-
{"observe_transform.facets.rules", "prometheus.observe-eng.com"},
12-
},
11+
{
12+
"observe_transform.facets.rules", []any{
13+
map[string]any{
14+
"host": "prometheus.observe-eng.com",
15+
"httpRules": []any{
16+
map[string]any{
17+
"backend": map[string]any{
18+
"service": map[string]any{
19+
"name": "prometheus",
20+
"port": "prometheus",
21+
},
22+
},
23+
"path": "/",
24+
}}}}}},
25+
},
26+
{
27+
name: "Ingress rules without host",
28+
inLogs: resourceLogsFromSingleJsonEvent("./testdata/ingressEvent2.json"),
29+
expectedResults: []queryWithResult{
30+
{
31+
"observe_transform.facets.rules", []any{
32+
map[string]any{
33+
"host": "*",
34+
"httpRules": []any{
35+
map[string]any{
36+
"backend": map[string]any{
37+
"service": map[string]any{
38+
"name": "test",
39+
"port": int64(80),
40+
},
41+
},
42+
"path": "/testpath",
43+
}},
44+
},
45+
// Rule with resource backend
46+
map[string]any{
47+
"host": "test.com",
48+
"httpRules": []any{
49+
map[string]any{
50+
"backend": map[string]any{
51+
"resource": "testResource",
52+
},
53+
"path": "/testpath2",
54+
}},
55+
},
56+
}}},
1357
},
1458
{
15-
name: "Ingress rules",
59+
name: "Load Balancer",
1660
inLogs: resourceLogsFromSingleJsonEvent("./testdata/ingressEvent.json"),
1761
expectedResults: []queryWithResult{
1862
{"observe_transform.facets.loadBalancer", "someUniqueElbIdentifier.elb.us-west-2.amazonaws.com"},

components/processors/observek8sattributesprocessor/processor.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,6 @@ func (kep *K8sEventsProcessor) processLogs(_ context.Context, logs plog.Logs) (p
322322
facetsMap = facets.Map()
323323
} else {
324324
facetsMap = transformMap.PutEmptyMap("facets")
325-
// Make sure we have capacity for at least as many actions as we have defined
326-
// Actions could generate more than one facet, that's taken care of afterwards.
327-
facetsMap.EnsureCapacity(len(kep.podActions))
328325
}
329326

330327
// Compute custom attributes
@@ -335,6 +332,8 @@ func (kep *K8sEventsProcessor) processLogs(_ context.Context, logs plog.Logs) (p
335332
}
336333

337334
// Add them to the facets
335+
// Ensre we have extra capacity for as many attributes as we
336+
// have just compted
338337
facetsMap.EnsureCapacity(facetsMap.Len() + len(values))
339338
for key, val := range values {
340339
if err := mapPut(facetsMap, key, val); err != nil {
@@ -353,14 +352,20 @@ func slicePut(theSlice pcommon.Slice, value any) error {
353352
switch typed := value.(type) {
354353
case string:
355354
elem.SetStr(typed)
355+
case int32:
356+
elem.SetInt(int64(typed))
356357
case int64:
357358
elem.SetInt(typed)
358359
case bool:
359360
elem.SetBool(typed)
360361
case float64:
361362
elem.SetDouble(typed)
362-
// Let's not complicate things and avoid putting maps/slices into slices.
363-
// There's gotta be an easier way to model the processor's output to avoid it
363+
case attributes:
364+
elem.SetEmptyMap()
365+
elem.Map().EnsureCapacity(len(typed))
366+
for k, v := range typed {
367+
mapPut(elem.Map(), k, v)
368+
}
364369
default:
365370
return errors.New("unrecognised type. Cannot be added to a slice")
366371
}
@@ -383,6 +388,8 @@ func mapPut(theMap pcommon.Map, key string, value any) error {
383388
theMap.PutStr(key, typed)
384389
case int:
385390
theMap.PutInt(key, int64(typed))
391+
case int32:
392+
theMap.PutInt(key, int64(typed))
386393
case int64:
387394
theMap.PutInt(key, typed)
388395
case bool:
@@ -395,6 +402,12 @@ func mapPut(theMap pcommon.Map, key string, value any) error {
395402
for _, elem := range typed {
396403
slicePut(slc, elem)
397404
}
405+
case []attributes:
406+
slc := theMap.PutEmptySlice(key)
407+
slc.EnsureCapacity(len(typed))
408+
for _, elem := range typed {
409+
slicePut(slc, elem)
410+
}
398411
case attributes:
399412
// This is potentially arbitrarily recursive. We don't care about
400413
// checking the nesting level since we will never need to define
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"kind": "Ingress",
3+
"apiVersion": "networking.k8s.io/v1",
4+
"metadata": {
5+
"name": "minimal-ingress",
6+
"namespace": "k8smonitoring",
7+
"uid": "8874fc28-72be-4c88-b15e-6d82af7c4f5d",
8+
"resourceVersion": "862319",
9+
"generation": 1,
10+
"creationTimestamp": "2024-10-22T08:29:52Z",
11+
"annotations": {
12+
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"networking.k8s.io/v1\",\"kind\":\"Ingress\",\"metadata\":{\"annotations\":{\"nginx.ingress.kubernetes.io/rewrite-target\":\"/\"},\"name\":\"minimal-ingress\",\"namespace\":\"k8smonitoring\"},\"spec\":{\"ingressClassName\":\"nginx-example\",\"rules\":[{\"http\":{\"paths\":[{\"backend\":{\"service\":{\"name\":\"test\",\"port\":{\"number\":80}}},\"path\":\"/testpath\",\"pathType\":\"Prefix\"}]}}]}}\n",
13+
"nginx.ingress.kubernetes.io/rewrite-target": "/"
14+
},
15+
"managedFields": [
16+
{
17+
"manager": "kubectl-client-side-apply",
18+
"operation": "Update",
19+
"apiVersion": "networking.k8s.io/v1",
20+
"time": "2024-10-22T08:29:52Z",
21+
"fieldsType": "FieldsV1",
22+
"fieldsV1": {
23+
"f:metadata": {
24+
"f:annotations": {
25+
".": {},
26+
"f:kubectl.kubernetes.io/last-applied-configuration": {},
27+
"f:nginx.ingress.kubernetes.io/rewrite-target": {}
28+
}
29+
},
30+
"f:spec": { "f:ingressClassName": {}, "f:rules": {} }
31+
}
32+
}
33+
]
34+
},
35+
"spec": {
36+
"ingressClassName": "nginx-example",
37+
"rules": [
38+
{
39+
"http": {
40+
"paths": [
41+
{
42+
"path": "/testpath",
43+
"pathType": "Prefix",
44+
"backend": {
45+
"service": { "name": "test", "port": { "number": 80 } }
46+
}
47+
}
48+
]
49+
}
50+
},
51+
{
52+
"host": "test.com",
53+
"http": {
54+
"paths": [
55+
{
56+
"path": "/testpath2",
57+
"pathType": "Prefix",
58+
"backend": {
59+
"resource": { "name": "testResource" }
60+
}
61+
}
62+
]
63+
}
64+
}
65+
]
66+
},
67+
"status": { "loadBalancer": {} }
68+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"host": "example.com", // (or "*" if no host is specified)
3+
"rules": [
4+
{
5+
"path": "/app1",
6+
"backend": {
7+
"service": {
8+
"name": "app1-service",
9+
"port": 8080 // (this port could also be the port name of type string)
10+
}
11+
}
12+
},
13+
{
14+
"path": "/app2",
15+
"backend": {
16+
// (The backend could be either service or resource)
17+
"resource": "app2-resource",
18+
"port": "somePortName" // (alternative to port number)
19+
}
20+
}
21+
]
22+
}

0 commit comments

Comments
 (0)