Repository: knative/eventing (Go)

Issue:
Event Lineage: Create Transform Function for Attributes Filter
<!-- 
Are you using Knative? If you do, we would love to know!
https://github.com/knative/community/issues/new?template=ADOPTERS.yaml&title=%5BADOPTERS%5D%3A+%24%7BCOMPANY+NAME+HERE%7D
-->

**Problem**
Transform functions are how we track changes to eventtypes as events flow through the system. To support the attributes filter, we should implement a transform function to represent how a given eventtype becomes "narrower" as it passes the filter. Specifically, this transform function should (for each attribute set in the attributes filter):
1. Check if the attribute is already set. If it is and does not match the expected value from the filter, return a `nil` eventtype to model no events passing this filter
2. If the attribute is not already set, set it to the value expected by the filter
3. If the attribute is compatible with the value required by the filter, set it equal to the value required by the filter.

**[Persona:](https://github.com/knative/eventing/blob/main/docs/personas.md)**
Which persona is this feature for?

**Exit Criteria**
A measurable (binary) test that would indicate that the problem has been resolved.

**Time Estimate (optional):**
How many developer-days do you think this may take to resolve? 2

**Additional context (optional)**
Add any other context about the feature request here.



Discussion:
/assign



Patch:
diff --git a/pkg/graph/constructor.go b/pkg/graph/constructor.go
index 96c7c877bd0..c1019a25bfe 100644
--- a/pkg/graph/constructor.go
+++ b/pkg/graph/constructor.go
@@ -118,19 +118,6 @@ func (g *Graph) AddSource(source duckv1.Source) {
 	v.AddEdge(to, dest, CloudEventOverridesTransform{Overrides: source.Spec.CloudEventOverrides}, true)
 }
 
-func (g *Graph) getOrCreateVertex(dest *duckv1.Destination) *Vertex {
-	v, ok := g.vertices[makeComparableDestination(dest)]
-	if !ok {
-		v = &Vertex{
-			self:   dest,
-			parent: g,
-		}
-		g.vertices[makeComparableDestination(dest)] = v
-	}
-
-	return v
-}
-
 func (g *Graph) AddTrigger(trigger eventingv1.Trigger) error {
 	brokerRef := &duckv1.KReference{
 		Name:       trigger.Spec.Broker,
@@ -155,7 +142,7 @@ func (g *Graph) AddTrigger(trigger eventingv1.Trigger) error {
 	to := g.getOrCreateVertex(&trigger.Spec.Subscriber)
 
 	//TODO: the transform function should be set according to the trigger filter - there are multiple open issues to address this later
-	broker.AddEdge(to, triggerDest, NoTransform{}, false)
+	broker.AddEdge(to, triggerDest, getTransformForTrigger(trigger), false)
 
 	if trigger.Spec.Delivery == nil || trigger.Spec.Delivery.DeadLetterSink == nil {
 		return nil
@@ -209,3 +196,24 @@ func (g *Graph) AddSubscription(subscription messagingv1.Subscription) error {
 	return nil
 
 }
+
+func getTransformForTrigger(trigger eventingv1.Trigger) Transform {
+	if len(trigger.Spec.Filters) == 0 && trigger.Spec.Filter != nil {
+		return &AttributesFilterTransform{Filter: trigger.Spec.Filter}
+	}
+
+	return NoTransform{}
+}
+
+func (g *Graph) getOrCreateVertex(dest *duckv1.Destination) *Vertex {
+	v, ok := g.vertices[makeComparableDestination(dest)]
+	if !ok {
+		v = &Vertex{
+			self:   dest,
+			parent: g,
+		}
+		g.vertices[makeComparableDestination(dest)] = v
+	}
+
+	return v
+}
diff --git a/pkg/graph/transforms.go b/pkg/graph/transforms.go
index 26660403eb9..276ba0096a4 100644
--- a/pkg/graph/transforms.go
+++ b/pkg/graph/transforms.go
@@ -17,6 +17,11 @@ limitations under the License.
 package graph
 
 import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	eventingv1 "knative.dev/eventing/pkg/apis/eventing/v1"
 	eventingv1beta3 "knative.dev/eventing/pkg/apis/eventing/v1beta3"
 	duckv1 "knative.dev/pkg/apis/duck/v1"
 )
@@ -33,6 +38,82 @@ func (nt NoTransform) Name() string {
 	return "no-transform"
 }
 
+type AttributesFilterTransform struct {
+	Filter *eventingv1.TriggerFilter
+}
+
+var _ Transform = &AttributesFilterTransform{}
+
+func (aft *AttributesFilterTransform) Apply(et *eventingv1beta3.EventType, tfc TransformFunctionContext) (*eventingv1beta3.EventType, TransformFunctionContext) {
+	etAttributes := make(map[string]*eventingv1beta3.EventAttributeDefinition)
+	for i := range et.Spec.Attributes {
+		etAttributes[et.Spec.Attributes[i].Name] = &et.Spec.Attributes[i]
+	}
+
+	for k, v := range aft.Filter.Attributes {
+		if attribute, ok := etAttributes[k]; ok {
+			if attribute.Value != v {
+				regexp, err := buildRegexForAttribute(attribute.Value)
+				if err != nil {
+					return nil, tfc
+				}
+
+				if regexp.MatchString(v) {
+					attribute.Value = v
+				} else {
+					return nil, tfc
+				}
+			}
+		} else {
+			etAttributes[k] = &eventingv1beta3.EventAttributeDefinition{
+				Name:     k,
+				Value:    v,
+				Required: true,
+			}
+		}
+	}
+
+	updatedAttributes := make([]eventingv1beta3.EventAttributeDefinition, 0, len(et.Spec.Attributes))
+	for _, v := range etAttributes {
+		updatedAttributes = append(updatedAttributes, *v)
+	}
+
+	et.Spec.Attributes = updatedAttributes
+
+	return et, tfc
+}
+
+func (aft *AttributesFilterTransform) Name() string {
+	return "attributes-filter"
+}
+
+func buildRegexForAttribute(attribute string) (*regexp.Regexp, error) {
+	chunks := []string{"^"}
+
+	var chunk strings.Builder
+	for i := 0; i < len(attribute); {
+		if attribute[i] == '{' {
+			chunks = append(chunks, chunk.String(), "[a-zA-Z]+")
+			chunk.Reset()
+
+			offset := strings.Index(attribute[i:], "}")
+			if offset == -1 {
+				return nil, fmt.Errorf("no closing bracket for variable")
+			}
+
+			i += offset + 1
+			continue
+		}
+
+		chunk.WriteByte(attribute[i])
+		i++
+	}
+
+	chunks = append(chunks, chunk.String(), "$")
+
+	return regexp.Compile(strings.Join(chunks, ""))
+}
+
 type EventTypeTransform struct {
 	EventType *eventingv1beta3.EventType
 }


Test patch:
diff --git a/pkg/graph/transforms_test.go b/pkg/graph/transforms_test.go
new file mode 100644
index 00000000000..6580a9c8452
--- /dev/null
+++ b/pkg/graph/transforms_test.go
@@ -0,0 +1,192 @@
+/*
+Copyright 2024 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package graph
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	eventingv1 "knative.dev/eventing/pkg/apis/eventing/v1"
+	eventingv1beta3 "knative.dev/eventing/pkg/apis/eventing/v1beta3"
+)
+
+func TestAttributeFilterTransform(t *testing.T) {
+	tests := []struct {
+		name             string
+		input            *eventingv1beta3.EventType
+		expected         *eventingv1beta3.EventType
+		filterAttributes eventingv1.TriggerFilterAttributes
+	}{
+		{
+			name: "one attribute, none set before",
+			input: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: make([]eventingv1beta3.EventAttributeDefinition, 0),
+				},
+			},
+			expected: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.event.type",
+							Required: true,
+						},
+					},
+				},
+			},
+			filterAttributes: eventingv1.TriggerFilterAttributes{
+				"type": "example.event.type",
+			},
+		},
+		{
+			name: "two attributes, one set before",
+			input: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.event.type",
+							Required: true,
+						},
+					},
+				},
+			},
+			expected: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.event.type",
+							Required: true,
+						},
+						{
+							Name:     "source",
+							Value:    "/sample/source",
+							Required: true,
+						},
+					},
+				},
+			},
+			filterAttributes: eventingv1.TriggerFilterAttributes{
+				"type":   "example.event.type",
+				"source": "/sample/source",
+			},
+		},
+		{
+			name: "one attribute, not compatible with filter",
+			input: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.event.type",
+							Required: true,
+						},
+					},
+				},
+			},
+			expected: nil,
+			filterAttributes: eventingv1.TriggerFilterAttributes{
+				"type": "sample.event.type",
+			},
+		},
+		{
+			name: "one attribute, with variable",
+			input: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.{variable}.type",
+							Required: true,
+						},
+					},
+				},
+			},
+			expected: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.event.type",
+							Required: true,
+						},
+					},
+				},
+			},
+			filterAttributes: eventingv1.TriggerFilterAttributes{
+				"type": "example.event.type",
+			},
+		},
+		{
+			name: "one attribute, with variable at end",
+			input: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.event.{variable}",
+							Required: true,
+						},
+					},
+				},
+			},
+			expected: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.event.type",
+							Required: true,
+						},
+					},
+				},
+			},
+			filterAttributes: eventingv1.TriggerFilterAttributes{
+				"type": "example.event.type",
+			},
+		},
+		{
+			name: "one attribute, with invalid variable (no closing bracket)",
+			input: &eventingv1beta3.EventType{
+				Spec: eventingv1beta3.EventTypeSpec{
+					Attributes: []eventingv1beta3.EventAttributeDefinition{
+						{
+							Name:     "type",
+							Value:    "example.{variable.type",
+							Required: true,
+						},
+					},
+				},
+			},
+			expected: nil,
+			filterAttributes: eventingv1.TriggerFilterAttributes{
+				"type": "example.event.type",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			transform := AttributesFilterTransform{Filter: &eventingv1.TriggerFilter{Attributes: test.filterAttributes}}
+			out, _ := transform.Apply(test.input, TransformFunctionContext{})
+			assert.Equal(t, test.expected, out)
+		})
+	}
+}