diff --git a/config/core/resources/eventtype.yaml b/config/core/resources/eventtype.yaml index 6acc5e162f3..da2deed78fb 100644 --- a/config/core/resources/eventtype.yaml +++ b/config/core/resources/eventtype.yaml @@ -76,6 +76,23 @@ spec: value: type: string description: "Value of the attribute. May be a template string using curly brackets {} to represent variable sections of the string." + variables: + description: "Each variable used within attribute values must be defined in the variables field, except for unnamed variables (i.e. {}), which do not need to be defined." + type: array + items: + type: object + required: + - name + properties: + name: + type: string + description: "Name of the variable used within EventType attribute values enclosed in curly brackets." + pattern: + type: string + description: "A CESQL LIKE pattern that the attribute value would adhere to." + example: + type: string + description: "Example of an attribute value that adheres to the CESQL pattern." status: description: 'Status represents the current state of the EventType. This data may be out of date.' diff --git a/docs/eventing-api.md b/docs/eventing-api.md index e80b1c1065b..7ca4b353904 100644 --- a/docs/eventing-api.md +++ b/docs/eventing-api.md @@ -4191,6 +4191,20 @@ string
Attributes is an array of CloudEvent attributes and extension attributes.
+variables
Variables is an array that provides definitions for variables used within attribute values.
+Attributes is an array of CloudEvent attributes and extension attributes.
+variables
Variables is an array that provides definitions for variables used within attribute values.
++(Appears on:EventTypeSpec) +
++
+Field | +Description | +
---|---|
+name + +string + + |
+
+ Name is the name of the variable used within EventType attribute values enclosed in curly brackets. + |
+
+pattern + +string + + |
+
+ Pattern is a CESQL LIKE pattern that the attribute value would adhere to. + |
+
+example + +string + + |
+
+ Example is an example of an attribute value that adheres to the CESQL pattern. + |
+
diff --git a/pkg/apis/eventing/v1beta3/eventtype_types.go b/pkg/apis/eventing/v1beta3/eventtype_types.go index 2ecc74586d6..c817a6bf2b1 100644 --- a/pkg/apis/eventing/v1beta3/eventtype_types.go +++ b/pkg/apis/eventing/v1beta3/eventtype_types.go @@ -71,6 +71,9 @@ type EventTypeSpec struct { Description string `json:"description,omitempty"` // Attributes is an array of CloudEvent attributes and extension attributes. Attributes []EventAttributeDefinition `json:"attributes"` + // Variables is an array that provides definitions for variables used within attribute values. + // +optional + Variables []EventVariableDefinition `json:"variables,omitempty"` } type EventAttributeDefinition struct { @@ -86,6 +89,15 @@ type EventAttributeDefinition struct { Value string `json:"value,omitempty"` } +type EventVariableDefinition struct { + // Name is the name of the variable used within EventType attribute values enclosed in curly brackets. + Name string `json:"name"` + // Pattern is a CESQL LIKE pattern that the attribute value would adhere to. + Pattern string `json:"pattern,omitempty"` + // Example is an example of an attribute value that adheres to the CESQL pattern. + Example string `json:"example,omitempty"` +} + // EventTypeStatus represents the current state of a EventType. type EventTypeStatus struct { // inherits duck/v1 Status, which currently provides: diff --git a/pkg/apis/eventing/v1beta3/eventtype_validation.go b/pkg/apis/eventing/v1beta3/eventtype_validation.go index 75590bcae5d..e5ce4fc86f8 100644 --- a/pkg/apis/eventing/v1beta3/eventtype_validation.go +++ b/pkg/apis/eventing/v1beta3/eventtype_validation.go @@ -18,6 +18,7 @@ package v1beta3 import ( "context" + "strings" "knative.dev/pkg/apis" "knative.dev/pkg/kmp" @@ -32,6 +33,7 @@ func (ets *EventTypeSpec) Validate(ctx context.Context) *apis.FieldError { // TODO: validate attribute with name=source is a valid URI // TODO: validate attribute with name=schema is a valid URI errs = errs.Also(ets.ValidateAttributes().ViaField("attributes")) + errs = errs.Also(ets.ValidateVariables().ViaField("variables")) return errs } @@ -83,3 +85,61 @@ func (ets *EventTypeSpec) ValidateAttributes() *apis.FieldError { return nil } + +func (ets *EventTypeSpec) ValidateVariables() *apis.FieldError { + var errs *apis.FieldError + + usedVariables := ets.extractAttributeVariables() + if len(usedVariables) == 0 { + return nil + } + + definedVariables := make(map[string]EventVariableDefinition, len(ets.Variables)) + for _, variable := range ets.Variables { + definedVariables[variable.Name] = variable + } + + var missingVariables []string + for _, varName := range usedVariables { + if _, ok := definedVariables[varName]; !ok { + // keep track of any used variables that aren't defined + missingVariables = append(missingVariables, varName) + } + } + + if len(missingVariables) > 0 { + errs = errs.Also(apis.ErrMissingField(missingVariables...)) + } + + return errs +} + +// extractEmbeddedAttributeVariables extracts variables embedded within attribute values +// enclosed in curly brackets (e.g. "path.{A}.{B}" -> ["A", "B"]). +func (ets *EventTypeSpec) extractAttributeVariables() []string { + var variables []string + + for _, attr := range ets.Attributes { + for idx := 0; idx < len(attr.Value); idx++ { + if attr.Value[idx] == '\\' { + idx++ // skip over escaped character + continue + } + if attr.Value[idx] != '{' { + continue // ignore characters not enclosed in curly brackets + } + + idx++ + var varName strings.Builder + for idx < len(attr.Value) && attr.Value[idx] != '}' { + varName.WriteByte(attr.Value[idx]) + idx++ + } + + if idx < len(attr.Value) && attr.Value[idx] == '}' && varName.Len() > 0 { + variables = append(variables, varName.String()) + } + } + } + return variables +} diff --git a/pkg/apis/eventing/v1beta3/eventtype_validation_test.go b/pkg/apis/eventing/v1beta3/eventtype_validation_test.go index 682b11a5c92..ac26c3b8bb1 100644 --- a/pkg/apis/eventing/v1beta3/eventtype_validation_test.go +++ b/pkg/apis/eventing/v1beta3/eventtype_validation_test.go @@ -150,6 +150,109 @@ func TestEventTypeSpecValidation(t *testing.T) { }, }, }, + }, { + name: "invalid eventtype due to missing variable definitions", + ets: &EventTypeSpec{ + Reference: &duckv1.KReference{ + APIVersion: "eventing.knative.dev/v1", + Kind: "Broker", + Name: "test-broker", + }, + Attributes: []EventAttributeDefinition{ + { + Name: "type", + Value: "event-type", + Required: true, + }, + { + Name: "source", + Value: testSource.String(), + Required: true, + }, + { + Name: "specversion", + Value: "v1", + Required: true, + }, + { + Name: "id", + Required: true, + }, + { + Name: "attributeWithVariable", + Value: "test.{testVariable}.{definedVariable}", + Required: true, + }, + { + Name: "anotherAttributeWithVariable", + Value: "test.something.{requestStatus}.more", + Required: true, + }, + }, + Variables: []EventVariableDefinition{ + { + Name: "definedVariable", + Pattern: "%", + Example: "", + }, + }, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("variables.requestStatus", "variables.testVariable") + return fe + }(), + }, { + name: "valid eventtype with variable definitions", + ets: &EventTypeSpec{ + Reference: &duckv1.KReference{ + APIVersion: "eventing.knative.dev/v1", + Kind: "Broker", + Name: "test-broker", + }, + Attributes: []EventAttributeDefinition{ + { + Name: "type", + Value: "event-type", + Required: true, + }, + { + Name: "source", + Value: testSource.String(), + Required: true, + }, + { + Name: "specversion", + Value: "v1", + Required: true, + }, + { + Name: "id", + Required: true, + }, + { + Name: "attributeWithVariable", + Value: "test.{testVariable}", + Required: true, + }, + { + Name: "anotherAttributeWithVariable", + Value: "test.something.{requestStatus}.more", + Required: true, + }, + }, + Variables: []EventVariableDefinition{ + { + Name: "testVariable", + Pattern: "%", + Example: "Any.value_passes", + }, + { + Name: "requestStatus", + Pattern: "req_est.%", + Example: "request.failed", + }, + }, + }, }, } @@ -491,3 +594,56 @@ func TestEventTypeImmutableFields(t *testing.T) { }) } } + +func TestEventTypeSpecExtractAttributeVariables(t *testing.T) { + tests := []struct { + name string + ets *EventTypeSpec + want []string + }{ + { + name: "extract included variables", + ets: &EventTypeSpec{ + Attributes: []EventAttributeDefinition{ + { + Name: "type", + Value: "a.{first}.{second}.{third}.{}", + Required: true, + }, + { + Name: "source", + Value: "{fourth}{fifth}.ab.{sixth}", + Required: true, + }, + }, + }, + want: []string{"first", "second", "third", "fourth", "fifth", "sixth"}, + }, + { + name: "ignores escaped curly brackets", + ets: &EventTypeSpec{ + Attributes: []EventAttributeDefinition{ + { + Name: "type", + Value: "a.{first}.\\{second}.{third}", + Required: true, + }, + { + Name: "source", + Value: "\\{fourth}{fifth}.ab.{sixth}", + Required: true, + }, + }, + }, + want: []string{"first", "third", "fifth", "sixth"}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.ets.extractAttributeVariables() + if diff := cmp.Diff(got, test.want); diff != "" { + t.Error("ExtractAttributeVariables (-want, +got) =", diff) + } + }) + } +} diff --git a/pkg/apis/eventing/v1beta3/zz_generated.deepcopy.go b/pkg/apis/eventing/v1beta3/zz_generated.deepcopy.go index e25635dc580..d85bc736f17 100644 --- a/pkg/apis/eventing/v1beta3/zz_generated.deepcopy.go +++ b/pkg/apis/eventing/v1beta3/zz_generated.deepcopy.go @@ -116,6 +116,11 @@ func (in *EventTypeSpec) DeepCopyInto(out *EventTypeSpec) { *out = make([]EventAttributeDefinition, len(*in)) copy(*out, *in) } + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make([]EventVariableDefinition, len(*in)) + copy(*out, *in) + } return } @@ -145,3 +150,19 @@ func (in *EventTypeStatus) DeepCopy() *EventTypeStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EventVariableDefinition) DeepCopyInto(out *EventVariableDefinition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventVariableDefinition. +func (in *EventVariableDefinition) DeepCopy() *EventVariableDefinition { + if in == nil { + return nil + } + out := new(EventVariableDefinition) + in.DeepCopyInto(out) + return out +}