This adds context.Context to the interfaces used by the pkg webhook. (#23)

See also: https://github.com/knative/pkg/pull/332
This commit is contained in:
Matt Moore 2019-03-21 10:52:47 -07:00 committed by Knative Prow Robot
parent b9d80eaec5
commit 3fc06fd3c9
12 changed files with 493 additions and 29 deletions

5
Gopkg.lock generated
View File

@ -136,15 +136,14 @@
revision = "f2b4162afba35581b6d4a50d3b8f34e33c144682"
[[projects]]
branch = "master"
digest = "1:0f5c0a93ea5a62ea721282a07746832546580707042f705710f6e9f9778a0598"
digest = "1:fa6e19b10f3088d6f290e32ba2f9735d2810dd8e6544028d4d0c842c162b83ef"
name = "github.com/knative/pkg"
packages = [
"apis",
"kmeta",
]
pruneopts = "NUT"
revision = "2a7e950c4e2d635d6e32ba3d050dd32ba7be5be3"
revision = "60fdcbcabd2faeb34328d8b2725dc76c59189453"
[[projects]]
branch = "master"

View File

@ -28,6 +28,11 @@ required = [
name = "k8s.io/client-go"
version = "kubernetes-1.12.6"
[[override]]
name = "github.com/knative/pkg"
# HEAD as of 2019-03-21
revision = "60fdcbcabd2faeb34328d8b2725dc76c59189453"
[prune]
go-tests = true
unused-packages = true

View File

@ -16,6 +16,8 @@ limitations under the License.
package v1alpha1
func (r *Image) SetDefaults() {
import "context"
func (r *Image) SetDefaults(ctx context.Context) {
// TODO(mattmoor): This
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
@ -36,7 +37,7 @@ func TestImageDefaulting(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := test.in
got.SetDefaults()
got.SetDefaults(context.Background())
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("SetDefaults (-want, +got) = %v", diff)
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
@ -25,11 +26,11 @@ import (
"github.com/knative/pkg/apis"
)
func (rt *Image) Validate() *apis.FieldError {
return rt.Spec.Validate().ViaField("spec")
func (rt *Image) Validate(ctx context.Context) *apis.FieldError {
return rt.Spec.Validate(ctx).ViaField("spec")
}
func (rs *ImageSpec) Validate() *apis.FieldError {
func (rs *ImageSpec) Validate(ctx context.Context) *apis.FieldError {
if rs.Image == "" {
return apis.ErrMissingField("image")
}

View File

@ -17,9 +17,9 @@ limitations under the License.
package v1alpha1
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
"github.com/knative/pkg/apis"
@ -55,9 +55,12 @@ func TestImageValidation(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := test.r.Validate()
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("Validate (-want, +got) = %v", diff)
got := test.r.Validate(context.Background())
if (test.want == nil) != (got == nil) {
t.Fatalf("Validate() = %v, wanted %v", got, test.want)
}
if want, got := test.want.Error(), got.Error(); want != got {
t.Fatalf("Validate() = %v, wanted %v", got, want)
}
})
}

View File

@ -18,6 +18,7 @@ package apis
import (
"fmt"
"sort"
"strings"
)
@ -28,12 +29,15 @@ const CurrentField = ""
// FieldError is used to propagate the context of errors pertaining to
// specific fields in a manner suitable for use in a recursive walk, so
// that errors contain the appropriate field context.
// +k8s:deepcopy-gen=false
// FieldError methods are non-mutating.
// +k8s:deepcopy-gen=true
type FieldError struct {
Message string
Paths []string
// Details contains an optional longer payload.
// +optional
Details string
errors []FieldError
}
// FieldError implements error
@ -50,27 +54,234 @@ func (fe *FieldError) ViaField(prefix ...string) *FieldError {
if fe == nil {
return nil
}
var newPaths []string
// Copy over message and details, paths will be updated and errors come
// along using .Also().
newErr := &FieldError{
Message: fe.Message,
Details: fe.Details,
}
// Prepend the Prefix to existing errors.
newPaths := make([]string, 0, len(fe.Paths))
for _, oldPath := range fe.Paths {
if oldPath == CurrentField {
newPaths = append(newPaths, strings.Join(prefix, "."))
} else {
newPaths = append(newPaths,
strings.Join(append(prefix, oldPath), "."))
newPaths = append(newPaths, flatten(append(prefix, oldPath)))
}
newErr.Paths = newPaths
for _, e := range fe.errors {
newErr = newErr.Also(e.ViaField(prefix...))
}
return newErr
}
// ViaIndex is used to attach an index to the next ViaField provided.
// For example, if a type recursively validates a parameter that has a collection:
// for i, c := range spec.Collection {
// if err := doValidation(c); err != nil {
// return err.ViaIndex(i).ViaField("collection")
// }
// }
func (fe *FieldError) ViaIndex(index int) *FieldError {
return fe.ViaField(asIndex(index))
}
// ViaFieldIndex is the short way to chain: err.ViaIndex(bar).ViaField(foo)
func (fe *FieldError) ViaFieldIndex(field string, index int) *FieldError {
return fe.ViaIndex(index).ViaField(field)
}
// ViaKey is used to attach a key to the next ViaField provided.
// For example, if a type recursively validates a parameter that has a collection:
// for k, v := range spec.Bag. {
// if err := doValidation(v); err != nil {
// return err.ViaKey(k).ViaField("bag")
// }
// }
func (fe *FieldError) ViaKey(key string) *FieldError {
return fe.ViaField(asKey(key))
}
// ViaFieldKey is the short way to chain: err.ViaKey(bar).ViaField(foo)
func (fe *FieldError) ViaFieldKey(field string, key string) *FieldError {
return fe.ViaKey(key).ViaField(field)
}
// Also collects errors, returns a new collection of existing errors and new errors.
func (fe *FieldError) Also(errs ...*FieldError) *FieldError {
var newErr *FieldError
// collect the current objects errors, if it has any
if !fe.isEmpty() {
newErr = fe.DeepCopy()
} else {
newErr = &FieldError{}
}
// and then collect the passed in errors
for _, e := range errs {
if !e.isEmpty() {
newErr.errors = append(newErr.errors, *e)
}
}
fe.Paths = newPaths
return fe
if newErr.isEmpty() {
return nil
}
return newErr
}
func (fe *FieldError) isEmpty() bool {
if fe == nil {
return true
}
return fe.Message == "" && fe.Details == "" && len(fe.errors) == 0 && len(fe.Paths) == 0
}
// normalized returns a flattened copy of all the errors.
func (fe *FieldError) normalized() []*FieldError {
// In case we call normalized on a nil object, return just an empty
// list. This can happen when .Error() is called on a nil object.
if fe == nil {
return []*FieldError(nil)
}
// Allocate errors with at least as many objects as we'll get on the first pass.
errors := make([]*FieldError, 0, len(fe.errors)+1)
// If this FieldError is a leaf, add it.
if fe.Message != "" {
errors = append(errors, &FieldError{
Message: fe.Message,
Paths: fe.Paths,
Details: fe.Details,
})
}
// And then collect all other errors recursively.
for _, e := range fe.errors {
errors = append(errors, e.normalized()...)
}
return errors
}
// Error implements error
func (fe *FieldError) Error() string {
if fe.Details == "" {
return fmt.Sprintf("%v: %v", fe.Message, strings.Join(fe.Paths, ", "))
// Get the list of errors as a flat merged list.
normedErrors := merge(fe.normalized())
errs := make([]string, 0, len(normedErrors))
for _, e := range normedErrors {
if e.Details == "" {
errs = append(errs, fmt.Sprintf("%v: %v", e.Message, strings.Join(e.Paths, ", ")))
} else {
errs = append(errs, fmt.Sprintf("%v: %v\n%v", e.Message, strings.Join(e.Paths, ", "), e.Details))
}
}
return fmt.Sprintf("%v: %v\n%v", fe.Message, strings.Join(fe.Paths, ", "), fe.Details)
return strings.Join(errs, "\n")
}
// Helpers ---
func asIndex(index int) string {
return fmt.Sprintf("[%d]", index)
}
func isIndex(part string) bool {
return strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]")
}
func asKey(key string) string {
return fmt.Sprintf("[%s]", key)
}
// flatten takes in a array of path components and looks for chances to flatten
// objects that have index prefixes, examples:
// err([0]).ViaField(bar).ViaField(foo) -> foo.bar.[0] converts to foo.bar[0]
// err(bar).ViaIndex(0).ViaField(foo) -> foo.[0].bar converts to foo[0].bar
// err(bar).ViaField(foo).ViaIndex(0) -> [0].foo.bar converts to [0].foo.bar
// err(bar).ViaIndex(0).ViaIndex(1).ViaField(foo) -> foo.[1].[0].bar converts to foo[1][0].bar
func flatten(path []string) string {
var newPath []string
for _, part := range path {
for _, p := range strings.Split(part, ".") {
if p == CurrentField {
continue
} else if len(newPath) > 0 && isIndex(p) {
newPath[len(newPath)-1] += p
} else {
newPath = append(newPath, p)
}
}
}
return strings.Join(newPath, ".")
}
// mergePaths takes in two string slices and returns the combination of them
// without any duplicate entries.
func mergePaths(a, b []string) []string {
newPaths := make([]string, 0, len(a)+len(b))
newPaths = append(newPaths, a...)
for _, bi := range b {
if !containsString(newPaths, bi) {
newPaths = append(newPaths, bi)
}
}
return newPaths
}
// containsString takes in a string slice and looks for the provided string
// within the slice.
func containsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
// merge takes in a flat list of FieldErrors and returns back a merged list of
// FieldErrors. FieldErrors have their Paths combined (and de-duped) if their
// Message and Details are the same. Merge will not inspect FieldError.errors.
// Merge will also sort the .Path slice, and the errors slice before returning.
func merge(errs []*FieldError) []*FieldError {
// make a map big enough for all the errors.
m := make(map[string]*FieldError, len(errs))
// Convert errs to a map where the key is <message>-<details> and the value
// is the error. If an error already exists in the map with the same key,
// then the paths will be merged.
for _, e := range errs {
k := key(e)
if v, ok := m[k]; ok {
// Found a match, merge the keys.
v.Paths = mergePaths(v.Paths, e.Paths)
} else {
// Does not exist in the map, save the error.
m[k] = e
}
}
// Take the map made previously and flatten it back out again.
newErrs := make([]*FieldError, 0, len(m))
for _, v := range m {
// While we have access to the merged paths, sort them too.
sort.Slice(v.Paths, func(i, j int) bool { return v.Paths[i] < v.Paths[j] })
newErrs = append(newErrs, v)
}
// Sort the flattened map.
sort.Slice(newErrs, func(i, j int) bool {
if newErrs[i].Message == newErrs[j].Message {
return newErrs[i].Details < newErrs[j].Details
}
return newErrs[i].Message < newErrs[j].Message
})
// return back the merged list of sorted errors.
return newErrs
}
// key returns the key using the fields .Message and .Details.
func key(err *FieldError) string {
return fmt.Sprintf("%s-%s", err.Message, err.Details)
}
// Public helpers ---
// ErrMissingField is a variadic helper method for constructing a FieldError for
// a set of missing fields.
func ErrMissingField(fieldPaths ...string) *FieldError {
@ -89,6 +300,12 @@ func ErrDisallowedFields(fieldPaths ...string) *FieldError {
}
}
// ErrInvalidArrayValue consturcts a FieldError for a repetetive `field`
// at `index` that has received an invalid string value.
func ErrInvalidArrayValue(value, field string, index int) *FieldError {
return ErrInvalidValue(value, CurrentField).ViaFieldIndex(field, index)
}
// ErrInvalidValue constructs a FieldError for a field that has received an
// invalid string value.
func ErrInvalidValue(value, fieldPath string) *FieldError {
@ -116,8 +333,8 @@ func ErrMultipleOneOf(fieldPaths ...string) *FieldError {
}
}
// ErrInvalidKeyName is a variadic helper method for constructing a
// FieldError that specifies a key name that is invalid.
// ErrInvalidKeyName is a variadic helper method for constructing a FieldError
// that specifies a key name that is invalid.
func ErrInvalidKeyName(value, fieldPath string, details ...string) *FieldError {
return &FieldError{
Message: fmt.Sprintf("invalid key name %q", value),
@ -125,3 +342,12 @@ func ErrInvalidKeyName(value, fieldPath string, details ...string) *FieldError {
Details: strings.Join(details, ", "),
}
}
// ErrOutOfBoundsValue constructs a FieldError for a field that has received an
// out of bound value.
func ErrOutOfBoundsValue(value, lower, upper, fieldPath string) *FieldError {
return &FieldError{
Message: fmt.Sprintf("expected %s <= %s <= %s", lower, value, upper),
Paths: []string{fieldPath},
}
}

View File

@ -16,16 +16,23 @@ limitations under the License.
package apis
import (
"context"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// Defaultable defines an interface for setting the defaults for the
// uninitialized fields of this instance.
type Defaultable interface {
SetDefaults()
SetDefaults(context.Context)
}
// Validatable indicates that a particular type may have its fields validated.
type Validatable interface {
// Validate checks the validity of this types fields.
Validate() *FieldError
Validate(context.Context) *FieldError
}
// Immutable indicates that a particular type has fields that should
@ -33,5 +40,18 @@ type Validatable interface {
type Immutable interface {
// CheckImmutableFields checks that the current instance's immutable
// fields haven't changed from the provided original.
CheckImmutableFields(original Immutable) *FieldError
CheckImmutableFields(ctx context.Context, original Immutable) *FieldError
}
// Listable indicates that a particular type can be returned via the returned
// list type by the API server.
type Listable interface {
runtime.Object
GetListType() runtime.Object
}
// Annotatable indicates that a particular type applies various annotations.
type Annotatable interface {
AnnotateUserInfo(ctx context.Context, previous Annotatable, ui *authenticationv1.UserInfo)
}

47
vendor/github.com/knative/pkg/apis/kind2resource.go generated vendored Normal file
View File

@ -0,0 +1,47 @@
/*
Copyright 2018 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 apis
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// KindToResource converts a GroupVersionKind to a GroupVersionResource
// through the world's simplest (worst) pluralizer.
func KindToResource(gvk schema.GroupVersionKind) schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: gvk.Group,
Version: gvk.Version,
Resource: pluralizeKind(gvk.Kind),
}
}
// Takes a kind and pluralizes it. This is super terrible, but I am
// not aware of a generic way to do this.
// I am not alone in thinking this and I haven't found a better solution:
// This seems relevant:
// https://github.com/kubernetes/kubernetes/issues/18622
func pluralizeKind(kind string) string {
ret := strings.ToLower(kind)
if strings.HasSuffix(ret, "s") {
return fmt.Sprintf("%ses", ret)
}
return fmt.Sprintf("%ss", ret)
}

View File

@ -20,6 +20,34 @@ limitations under the License.
package apis
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FieldError) DeepCopyInto(out *FieldError) {
*out = *in
if in.Paths != nil {
in, out := &in.Paths, &out.Paths
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.errors != nil {
in, out := &in.errors, &out.errors
*out = make([]FieldError, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FieldError.
func (in *FieldError) DeepCopy() *FieldError {
if in == nil {
return nil
}
out := new(FieldError)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VolatileTime) DeepCopyInto(out *VolatileTime) {
*out = *in

94
vendor/github.com/knative/pkg/kmeta/accessor.go generated vendored Normal file
View File

@ -0,0 +1,94 @@
/*
Copyright 2018 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 kmeta
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/cache"
)
// Accessor is a collection of interfaces from metav1.TypeMeta,
// runtime.Object and metav1.Object that Kubernetes API types
// registered with runtime.Scheme must support.
type Accessor interface {
// Interfaces for metav1.TypeMeta
GroupVersionKind() schema.GroupVersionKind
SetGroupVersionKind(gvk schema.GroupVersionKind)
// Interfaces for runtime.Object
GetObjectKind() schema.ObjectKind
DeepCopyObject() runtime.Object
// Interfaces for metav1.Object
GetNamespace() string
SetNamespace(namespace string)
GetName() string
SetName(name string)
GetGenerateName() string
SetGenerateName(name string)
GetUID() types.UID
SetUID(uid types.UID)
GetResourceVersion() string
SetResourceVersion(version string)
GetGeneration() int64
SetGeneration(generation int64)
GetSelfLink() string
SetSelfLink(selfLink string)
GetCreationTimestamp() metav1.Time
SetCreationTimestamp(timestamp metav1.Time)
GetDeletionTimestamp() *metav1.Time
SetDeletionTimestamp(timestamp *metav1.Time)
GetDeletionGracePeriodSeconds() *int64
SetDeletionGracePeriodSeconds(*int64)
GetLabels() map[string]string
SetLabels(labels map[string]string)
GetAnnotations() map[string]string
SetAnnotations(annotations map[string]string)
GetInitializers() *metav1.Initializers
SetInitializers(initializers *metav1.Initializers)
GetFinalizers() []string
SetFinalizers(finalizers []string)
GetOwnerReferences() []metav1.OwnerReference
SetOwnerReferences([]metav1.OwnerReference)
GetClusterName() string
SetClusterName(clusterName string)
}
// DeletionHandlingAccessor tries to convert given interface into Accessor first;
// and to handle deletion, it try to fetch info from DeletedFinalStateUnknown on failure.
// The name is a reference to cache.DeletionHandlingMetaNamespaceKeyFunc
func DeletionHandlingAccessor(obj interface{}) (Accessor, error) {
accessor, ok := obj.(Accessor)
if !ok {
// To handle obj deletion, try to fetch info from DeletedFinalStateUnknown.
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
return nil, fmt.Errorf("Couldn't get Accessor from tombstone %#v", obj)
}
accessor, ok = tombstone.Obj.(Accessor)
if !ok {
return nil, fmt.Errorf("The object that Tombstone contained is not of kmeta.Accessor %#v", obj)
}
}
return accessor, nil
}

View File

@ -26,6 +26,13 @@ import (
// The methods in this file are used for managing subresources in cases where
// a controller instantiates different resources for each version of itself.
// There are two sets of methods available here:
// * `*VersionLabel*`: these methods act on `metadata.resourceVersion` and
// create new labels for EVERY change to the resource (incl. `/status`).
// * `*GenerationLabel*`: these methods act on `metadata.generation` and
// create new labels for changes to the resource's "spec" (typically, but
// some K8s resources change `metadata.generation` for annotations as well
// e.g. Deployment).
//
// For example, if an A might instantiate N B's at version 1 and M B's at
// version 2 then it can use MakeVersionLabels to decorate each subresource
@ -66,6 +73,37 @@ func MakeOldVersionLabelSelector(om metav1.ObjectMetaAccessor) labels.Selector {
)
}
// MakeGenerationLabels constructs a set of labels to apply to subresources
// instantiated at this version of the parent resource, so that we can
// efficiently select them.
func MakeGenerationLabels(om metav1.ObjectMetaAccessor) labels.Set {
return map[string]string{
"controller": string(om.GetObjectMeta().GetUID()),
"generation": genStr(om),
}
}
// MakeGenerationLabelSelector constructs a selector for subresources
// instantiated at this version of the parent resource. This keys
// off of the labels populated by MakeGenerationLabels.
func MakeGenerationLabelSelector(om metav1.ObjectMetaAccessor) labels.Selector {
return labels.SelectorFromSet(MakeGenerationLabels(om))
}
// MakeOldGenerationLabelSelector constructs a selector for subresources
// instantiated at an older version of the parent resource. This keys
// off of the labels populated by MakeGenerationLabels.
func MakeOldGenerationLabelSelector(om metav1.ObjectMetaAccessor) labels.Selector {
return labels.NewSelector().Add(
mustNewRequirement("controller", selection.Equals, []string{string(om.GetObjectMeta().GetUID())}),
mustNewRequirement("generation", selection.NotEquals, []string{genStr(om)}),
)
}
func genStr(om metav1.ObjectMetaAccessor) string {
return fmt.Sprintf("%05d", om.GetObjectMeta().GetGeneration())
}
// mustNewRequirement panics if there are any errors constructing our selectors.
func mustNewRequirement(key string, op selection.Operator, vals []string) labels.Requirement {
r, err := labels.NewRequirement(key, op, vals)