Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/kind-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
- ./test/rekt/...
- ./test/e2e
- ./test/conformance
- ./test/experimental

# Map between K8s and KinD versions.
# This is attempting to make it a bit clearer what's being tested.
Expand Down
1 change: 1 addition & 0 deletions config/400-config-experimental-features.yaml
24 changes: 24 additions & 0 deletions config/core/configmaps/experimental-features.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2021 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
#
# https://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.

apiVersion: v1
kind: ConfigMap
metadata:
name: config-experimental-features
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same here

namespace: knative-eventing
labels:
eventing.knative.dev/release: devel
knative.dev/config-propagation: original
knative.dev/config-category: eventing
data:
85 changes: 85 additions & 0 deletions pkg/apis/experimental/api_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
Copyright 2021 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 experimental
Comment thread
slinkydeveloper marked this conversation as resolved.

import (
"context"
"fmt"
"reflect"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
)

// ValidateAPIFields checks that the experimental features fields are disabled if the experimental flag is disabled.
// experimentalFields can contain a string with dots, to identify sub-structs, like "Destination.Ref.APIVersion"
func ValidateAPIFields(ctx context.Context, featureName string, object interface{}, experimentalFields ...string) (errs *apis.FieldError) {
obj := reflect.ValueOf(object)
obj = reflect.Indirect(obj)
if obj.Kind() != reflect.Struct {
return nil
}

// If feature not enabled, let's check the field is not used
if !FromContext(ctx).IsEnabled(featureName) {
for _, fieldName := range experimentalFields {
fieldVal := walk(obj, strings.Split(fieldName, ".")...)

if !fieldVal.IsZero() {
errs = errs.Also(&apis.FieldError{
Message: fmt.Sprintf("Disallowed field because the experimental feature '%s' is disabled", featureName),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

remove experimental

Paths: []string{fmt.Sprintf("%s.%s", obj.Type().Name(), fieldName)},
})
}
}
}

return errs
}

// ValidateAnnotations checks that the experimental features annotations are disabled if the experimental flag is disabled
func ValidateAnnotations(ctx context.Context, featureName string, object metav1.Object, experimentalAnnotations ...string) (errs *apis.FieldError) {
// If feature not enabled, let's check the annotation is not used
if !FromContext(ctx).IsEnabled(featureName) {
for _, annotation := range experimentalAnnotations {
if _, ok := object.GetAnnotations()[annotation]; ok {
errs = errs.Also(&apis.FieldError{
Message: fmt.Sprintf("Disallowed annotation because the experimental feature '%s' is disabled", featureName),
Paths: []string{annotation},
})
}
}
}

return errs
}

func walk(value reflect.Value, paths ...string) reflect.Value {
switch value.Kind() {
case reflect.Struct:
newVal := value.FieldByName(paths[0])
if len(paths) == 1 {
return newVal
}
return walk(value.FieldByName(paths[0]), paths[1:]...)
case reflect.Ptr:
return walk(reflect.Indirect(value), paths...)
default:
return reflect.Zero(value.Type())
}
}
205 changes: 205 additions & 0 deletions pkg/apis/experimental/api_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
Copyright 2021 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 experimental

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"

eventingv1 "knative.dev/eventing/pkg/apis/eventing/v1"
)

const flagName = "my-flag"

func TestValidateAPIFields(t *testing.T) {
tests := []struct {
name string
flags Flags
featureName string
object interface{}
experimentalFields []string
wantErrs *apis.FieldError
}{
{
name: "invalid input",
featureName: flagName,
flags: map[string]bool{
flagName: true,
},
object: []string{},
experimentalFields: []string{"Filter"},
},
{
name: "enabled flag",
featureName: flagName,
flags: map[string]bool{
flagName: true,
},
object: eventingv1.TriggerSpec{
Broker: "blabla",
Subscriber: duckv1.Destination{
URI: apis.HTTP("example.com"),
},
Filter: &eventingv1.TriggerFilter{},
},
experimentalFields: []string{"Filter"},
},
{
name: "disabled pointer flag",
featureName: flagName,
flags: map[string]bool{
flagName: false,
},
object: eventingv1.TriggerSpec{
Broker: "blabla",
Subscriber: duckv1.Destination{
URI: apis.HTTP("example.com"),
},
Filter: &eventingv1.TriggerFilter{},
},
experimentalFields: []string{"Filter"},
wantErrs: &apis.FieldError{
Message: fmt.Sprintf("Disallowed field because the experimental feature '%s' is disabled", flagName),
Paths: []string{"TriggerSpec.Filter"},
},
},
{
name: "disabled nested string flag",
featureName: flagName,
flags: map[string]bool{
flagName: false,
},
object: eventingv1.TriggerSpec{
Broker: "blabla",
Subscriber: duckv1.Destination{
Ref: &duckv1.KReference{
Namespace: "abc",
},
},
},
experimentalFields: []string{"Subscriber.Ref.Namespace"},
wantErrs: &apis.FieldError{
Message: fmt.Sprintf("Disallowed field because the experimental feature '%s' is disabled", flagName),
Paths: []string{"Subscriber.Ref.Namespace"},
},
},
{
name: "enabled nested string flag",
featureName: flagName,
flags: map[string]bool{
flagName: true,
},
object: eventingv1.TriggerSpec{
Broker: "blabla",
Subscriber: duckv1.Destination{
Ref: &duckv1.KReference{
Namespace: "abc",
},
},
},
experimentalFields: []string{"Subscriber.Ref.Namespace"},
},
{
name: "disabled map flag",
featureName: flagName,
flags: map[string]bool{
flagName: false,
},
object: &eventingv1.TriggerFilter{
Attributes: map[string]string{},
},
experimentalFields: []string{"Attributes"},
wantErrs: &apis.FieldError{
Message: fmt.Sprintf("Disallowed field because the experimental feature '%s' is disabled", flagName),
Paths: []string{"TriggerFilter.Attributes"},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := ToContext(context.Background(), tt.flags)

res := ValidateAPIFields(ctx, tt.featureName, tt.object, tt.experimentalFields...)
if tt.wantErrs == nil {
require.Nil(t, res)
} else {
require.Error(t, res, tt.wantErrs.Error())
}
})
}
}

func TestValidateAnnotations(t *testing.T) {
tests := []struct {
name string
flags Flags
featureName string
object metav1.Object
experimentalFields []string
wantErrs *apis.FieldError
}{{
name: "enabled flag",
featureName: flagName,
flags: map[string]bool{
flagName: true,
},
object: &eventingv1.Broker{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"dev.knative/myfancyannotation": "blabla",
},
},
},
experimentalFields: []string{"dev.knative/myfancyannotation"},
},
{
name: "disabled flag",
featureName: flagName,
flags: map[string]bool{
flagName: false,
},
object: &eventingv1.Broker{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"dev.knative/myfancyannotation": "blabla",
},
},
},
experimentalFields: []string{"dev.knative/myfancyannotation"},
wantErrs: &apis.FieldError{
Message: fmt.Sprintf("Disallowed field because the experimental feature '%s' is disabled", flagName),
Paths: []string{"dev.knative/myfancyannotation"},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := ToContext(context.Background(), tt.flags)

res := ValidateAnnotations(ctx, tt.featureName, tt.object, tt.experimentalFields...)
if tt.wantErrs == nil {
require.Nil(t, res)
} else {
require.Error(t, res, tt.wantErrs.Error())
}
})
}
}
60 changes: 60 additions & 0 deletions pkg/apis/experimental/features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2021 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 experimental

import (
"fmt"
"strings"

corev1 "k8s.io/api/core/v1"
)

// Flags is a map containing all the enabled/disabled flags for the experimental features.
// Missing entry in the map means feature is equal to feature not enabled.
type Flags map[string]bool

// IsEnabled returns true if the feature is enabled
func (e Flags) IsEnabled(featureName string) bool {
return e != nil && e[featureName]
}

// NewFlagsConfigFromMap creates a Flags from the supplied Map
func NewFlagsConfigFromMap(data map[string]string) (Flags, error) {
flags := Flags{}

for k, v := range data {
if strings.HasPrefix(k, "_") {
// Ignore all the keys starting with _
continue
}
sanitizedKey := strings.TrimSpace(k)
if strings.EqualFold(v, "true") {
flags[sanitizedKey] = true
} else if strings.EqualFold(v, "false") {
flags[sanitizedKey] = false
} else {
return Flags{}, fmt.Errorf("cannot parse the boolean flag '%s' = '%s'. Allowed values: [true, false]", k, v)
}
}

return flags, nil
}

// NewFlagsConfigFromConfigMap creates a Flags from the supplied configMap
func NewFlagsConfigFromConfigMap(config *corev1.ConfigMap) (Flags, error) {
return NewFlagsConfigFromMap(config.Data)
}
Loading