mirror of https://github.com/knative/pkg.git
Initial commit for the webhook to set the annotations about mutator. (#275)
* Initial commit for the webhook to set the annotations about mutator. The user that created or updated the resource will be set in the annotations. * update comments * remove debug logging * logging :/ * logging :/, returns * logging :/ III * error wrap * simplify test * rename the test * add pkg/errors to the deps for better errors * do not require CRD to implement Annotatable * review issues * fix interface as required by review
This commit is contained in:
parent
c089ddfc5d
commit
1982208dd9
|
@ -335,6 +335,14 @@
|
|||
revision = "5f041e8faa004a95c88a202771f4cc3e991971e6"
|
||||
version = "v2.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:14715f705ff5dfe0ffd6571d7d201dd8e921030f8070321a79380d8ca4ec1a24"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
|
||||
version = "v0.8.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7c7cfeecd2b7147bcfec48a4bf622b4879e26aec145a9e373ce51d0c23b16f6b"
|
||||
name = "github.com/prometheus/client_golang"
|
||||
|
@ -1008,6 +1016,7 @@
|
|||
"github.com/knative/test-infra/tools/dep-collector",
|
||||
"github.com/markbates/inflect",
|
||||
"github.com/mattbaird/jsonpatch",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/rogpeppe/go-internal/semver",
|
||||
"go.opencensus.io/exporter/prometheus",
|
||||
"go.opencensus.io/plugin/ochttp",
|
||||
|
@ -1023,6 +1032,7 @@
|
|||
"k8s.io/api/admission/v1beta1",
|
||||
"k8s.io/api/admissionregistration/v1beta1",
|
||||
"k8s.io/api/apps/v1",
|
||||
"k8s.io/api/authentication/v1",
|
||||
"k8s.io/api/batch/v1",
|
||||
"k8s.io/api/core/v1",
|
||||
"k8s.io/api/extensions/v1beta1",
|
||||
|
|
|
@ -8,9 +8,10 @@ required = [
|
|||
"k8s.io/code-generator/cmd/client-gen",
|
||||
"k8s.io/code-generator/cmd/lister-gen",
|
||||
"k8s.io/code-generator/cmd/informer-gen",
|
||||
"github.com/knative/test-infra/tools/dep-collector",
|
||||
"github.com/knative/test-infra/scripts",
|
||||
"github.com/evanphx/json-patch",
|
||||
"github.com/knative/test-infra/scripts",
|
||||
"github.com/knative/test-infra/tools/dep-collector",
|
||||
"github.com/pkg/errors",
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package apis
|
||||
|
||||
import (
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
|
@ -47,3 +48,8 @@ type Listable interface {
|
|||
|
||||
GetListType() runtime.Object
|
||||
}
|
||||
|
||||
// Annotatable indicates that a particular type applies various annotations.
|
||||
type Annotatable interface {
|
||||
AnnotateUserInfo(previous Annotatable, ui *authenticationv1.UserInfo)
|
||||
}
|
||||
|
|
|
@ -62,3 +62,7 @@ func (cs *InnerDefaultSpec) SetDefaults() {
|
|||
func (*InnerDefaultResource) Validate() *apis.FieldError {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AnnotateUserInfo satisfies the Annotatable interface.
|
||||
// For this type it is nop.
|
||||
func (*InnerDefaultResource) AnnotateUserInfo(p apis.Annotatable, userName string) {}
|
||||
|
|
|
@ -19,10 +19,12 @@ package testing
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/knative/pkg/apis"
|
||||
"github.com/knative/pkg/kmp"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/knative/pkg/apis"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
@ -35,13 +37,22 @@ type Resource struct {
|
|||
Spec ResourceSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// CreatorAnnotation is the annotation that denotes the user that created the resource.
|
||||
CreatorAnnotation = "testing.knative.dev/creator"
|
||||
// UpdaterAnnotation is the annotation that denotes the user that last updated the resource.
|
||||
UpdaterAnnotation = "testing.knative.dev/updater"
|
||||
)
|
||||
|
||||
// Check that Resource may be validated and defaulted.
|
||||
var _ apis.Validatable = (*Resource)(nil)
|
||||
var _ apis.Defaultable = (*Resource)(nil)
|
||||
var _ apis.Immutable = (*Resource)(nil)
|
||||
var _ apis.Annotatable = (*Resource)(nil)
|
||||
var _ apis.Listable = (*Resource)(nil)
|
||||
|
||||
// Check that we implement the Generation duck type.
|
||||
// ResourceSpec represents test resource spec.
|
||||
// TODO: Check that we implement the Generation duck type.
|
||||
type ResourceSpec struct {
|
||||
Generation int64 `json:"generation,omitempty"`
|
||||
|
||||
|
@ -51,10 +62,12 @@ type ResourceSpec struct {
|
|||
FieldThatsImmutableWithDefault string `json:"fieldThatsImmutableWithDefault,omitempty"`
|
||||
}
|
||||
|
||||
// SetDefaults sets the defaults on the object.
|
||||
func (c *Resource) SetDefaults() {
|
||||
c.Spec.SetDefaults()
|
||||
}
|
||||
|
||||
// SetDefaults sets the defaults on the spec.
|
||||
func (cs *ResourceSpec) SetDefaults() {
|
||||
if cs.FieldWithDefault == "" {
|
||||
cs.FieldWithDefault = "I'm a default."
|
||||
|
@ -64,6 +77,34 @@ func (cs *ResourceSpec) SetDefaults() {
|
|||
}
|
||||
}
|
||||
|
||||
// AnnotateUserInfo satisfies the Annotatable interface.
|
||||
func (c *Resource) AnnotateUserInfo(prev apis.Annotatable, ui *authenticationv1.UserInfo) {
|
||||
a := c.ObjectMeta.GetAnnotations()
|
||||
if a == nil {
|
||||
a = map[string]string{}
|
||||
}
|
||||
userName := ui.Username
|
||||
|
||||
// If previous is nil (i.e. this is `Create` operation),
|
||||
// then we set both fields.
|
||||
// Otherwise copy creator from the previous state.
|
||||
if prev == nil {
|
||||
a[CreatorAnnotation] = userName
|
||||
} else {
|
||||
up := prev.(*Resource)
|
||||
// No spec update ==> bail out.
|
||||
if ok, _ := kmp.SafeEqual(up.Spec, c.Spec); ok {
|
||||
return
|
||||
}
|
||||
if up.ObjectMeta.GetAnnotations() != nil {
|
||||
a[CreatorAnnotation] = up.ObjectMeta.GetAnnotations()[CreatorAnnotation]
|
||||
}
|
||||
}
|
||||
// Regardless of `old` set the updater.
|
||||
a[UpdaterAnnotation] = userName
|
||||
c.ObjectMeta.SetAnnotations(a)
|
||||
}
|
||||
|
||||
func (c *Resource) Validate() *apis.FieldError {
|
||||
return c.Spec.Validate().ViaField("spec")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,282 @@
|
|||
// Package errors provides simple error handling primitives.
|
||||
//
|
||||
// The traditional error handling idiom in Go is roughly akin to
|
||||
//
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// which when applied recursively up the call stack results in error reports
|
||||
// without context or debugging information. The errors package allows
|
||||
// programmers to add context to the failure path in their code in a way
|
||||
// that does not destroy the original value of the error.
|
||||
//
|
||||
// Adding context to an error
|
||||
//
|
||||
// The errors.Wrap function returns a new error that adds context to the
|
||||
// original error by recording a stack trace at the point Wrap is called,
|
||||
// together with the supplied message. For example
|
||||
//
|
||||
// _, err := ioutil.ReadAll(r)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "read failed")
|
||||
// }
|
||||
//
|
||||
// If additional control is required, the errors.WithStack and
|
||||
// errors.WithMessage functions destructure errors.Wrap into its component
|
||||
// operations: annotating an error with a stack trace and with a message,
|
||||
// respectively.
|
||||
//
|
||||
// Retrieving the cause of an error
|
||||
//
|
||||
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||
// preceding error. Depending on the nature of the error it may be necessary
|
||||
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||
// for inspection. Any error value which implements this interface
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||
// the topmost error that does not implement causer, which is assumed to be
|
||||
// the original cause. For example:
|
||||
//
|
||||
// switch err := errors.Cause(err).(type) {
|
||||
// case *MyError:
|
||||
// // handle specifically
|
||||
// default:
|
||||
// // unknown error
|
||||
// }
|
||||
//
|
||||
// Although the causer interface is not exported by this package, it is
|
||||
// considered a part of its stable public interface.
|
||||
//
|
||||
// Formatted printing of errors
|
||||
//
|
||||
// All error values returned from this package implement fmt.Formatter and can
|
||||
// be formatted by the fmt package. The following verbs are supported:
|
||||
//
|
||||
// %s print the error. If the error has a Cause it will be
|
||||
// printed recursively.
|
||||
// %v see %s
|
||||
// %+v extended format. Each Frame of the error's StackTrace will
|
||||
// be printed in detail.
|
||||
//
|
||||
// Retrieving the stack trace of an error or wrapper
|
||||
//
|
||||
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||
// invoked. This information can be retrieved with the following interface:
|
||||
//
|
||||
// type stackTracer interface {
|
||||
// StackTrace() errors.StackTrace
|
||||
// }
|
||||
//
|
||||
// The returned errors.StackTrace type is defined as
|
||||
//
|
||||
// type StackTrace []Frame
|
||||
//
|
||||
// The Frame type represents a call site in the stack trace. Frame supports
|
||||
// the fmt.Formatter interface that can be used for printing information about
|
||||
// the stack trace of this error. For example:
|
||||
//
|
||||
// if err, ok := err.(stackTracer); ok {
|
||||
// for _, f := range err.StackTrace() {
|
||||
// fmt.Printf("%+s:%d", f)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Although the stackTracer interface is not exported by this package, it is
|
||||
// considered a part of its stable public interface.
|
||||
//
|
||||
// See the documentation for Frame.Format for more details.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New returns an error with the supplied message.
|
||||
// New also records the stack trace at the point it was called.
|
||||
func New(message string) error {
|
||||
return &fundamental{
|
||||
msg: message,
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf formats according to a format specifier and returns the string
|
||||
// as a value that satisfies error.
|
||||
// Errorf also records the stack trace at the point it was called.
|
||||
func Errorf(format string, args ...interface{}) error {
|
||||
return &fundamental{
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// fundamental is an error that has a message and a stack, but no caller.
|
||||
type fundamental struct {
|
||||
msg string
|
||||
*stack
|
||||
}
|
||||
|
||||
func (f *fundamental) Error() string { return f.msg }
|
||||
|
||||
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
io.WriteString(s, f.msg)
|
||||
f.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, f.msg)
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", f.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||
// If err is nil, WithStack returns nil.
|
||||
func WithStack(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
type withStack struct {
|
||||
error
|
||||
*stack
|
||||
}
|
||||
|
||||
func (w *withStack) Cause() error { return w.error }
|
||||
|
||||
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v", w.Cause())
|
||||
w.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, w.Error())
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap returns an error annotating err with a stack trace
|
||||
// at the point Wrap is called, and the supplied message.
|
||||
// If err is nil, Wrap returns nil.
|
||||
func Wrap(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf returns an error annotating err with a stack trace
|
||||
// at the point Wrapf is called, and the format specifier.
|
||||
// If err is nil, Wrapf returns nil.
|
||||
func Wrapf(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessage annotates err with a new message.
|
||||
// If err is nil, WithMessage returns nil.
|
||||
func WithMessage(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessagef annotates err with the format specifier.
|
||||
// If err is nil, WithMessagef returns nil.
|
||||
func WithMessagef(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
|
||||
type withMessage struct {
|
||||
cause error
|
||||
msg string
|
||||
}
|
||||
|
||||
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||
func (w *withMessage) Cause() error { return w.cause }
|
||||
|
||||
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||
io.WriteString(s, w.msg)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's', 'q':
|
||||
io.WriteString(s, w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Cause returns the underlying cause of the error, if possible.
|
||||
// An error value has a cause if it implements the following
|
||||
// interface:
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// If the error does not implement Cause, the original error will
|
||||
// be returned. If the error is nil, nil will be returned without further
|
||||
// investigation.
|
||||
func Cause(err error) error {
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
for err != nil {
|
||||
cause, ok := err.(causer)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
err = cause.Cause()
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Frame represents a program counter inside a stack frame.
|
||||
type Frame uintptr
|
||||
|
||||
// pc returns the program counter for this frame;
|
||||
// multiple frames may have the same PC value.
|
||||
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||
|
||||
// file returns the full path to the file that contains the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) file() string {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
file, _ := fn.FileLine(f.pc())
|
||||
return file
|
||||
}
|
||||
|
||||
// line returns the line number of source code of the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) line() int {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return 0
|
||||
}
|
||||
_, line := fn.FileLine(f.pc())
|
||||
return line
|
||||
}
|
||||
|
||||
// Format formats the frame according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+s function name and path of source file relative to the compile time
|
||||
// GOPATH separated by \n\t (<funcname>\n\t<path>)
|
||||
// %+v equivalent to %+s:%d
|
||||
func (f Frame) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
pc := f.pc()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
io.WriteString(s, "unknown")
|
||||
} else {
|
||||
file, _ := fn.FileLine(pc)
|
||||
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
|
||||
}
|
||||
default:
|
||||
io.WriteString(s, path.Base(f.file()))
|
||||
}
|
||||
case 'd':
|
||||
fmt.Fprintf(s, "%d", f.line())
|
||||
case 'n':
|
||||
name := runtime.FuncForPC(f.pc()).Name()
|
||||
io.WriteString(s, funcname(name))
|
||||
case 'v':
|
||||
f.Format(s, 's')
|
||||
io.WriteString(s, ":")
|
||||
f.Format(s, 'd')
|
||||
}
|
||||
}
|
||||
|
||||
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||
type StackTrace []Frame
|
||||
|
||||
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s lists source files for each Frame in the stack
|
||||
// %v lists the source file and line number for each Frame in the stack
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
for _, f := range st {
|
||||
fmt.Fprintf(s, "\n%+v", f)
|
||||
}
|
||||
case s.Flag('#'):
|
||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
default:
|
||||
fmt.Fprintf(s, "%v", []Frame(st))
|
||||
}
|
||||
case 's':
|
||||
fmt.Fprintf(s, "%s", []Frame(st))
|
||||
}
|
||||
}
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
type stack []uintptr
|
||||
|
||||
func (s *stack) Format(st fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case st.Flag('+'):
|
||||
for _, pc := range *s {
|
||||
f := Frame(pc)
|
||||
fmt.Fprintf(st, "\n%+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stack) StackTrace() StackTrace {
|
||||
f := make([]Frame, len(*s))
|
||||
for i := 0; i < len(f); i++ {
|
||||
f[i] = Frame((*s)[i])
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func callers() *stack {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(3, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return &st
|
||||
}
|
||||
|
||||
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||
func funcname(name string) string {
|
||||
i := strings.LastIndex(name, "/")
|
||||
name = name[i+1:]
|
||||
i = strings.Index(name, ".")
|
||||
return name[i+1:]
|
||||
}
|
|
@ -37,11 +37,13 @@ import (
|
|||
"github.com/knative/pkg/kmp"
|
||||
"github.com/knative/pkg/logging"
|
||||
"github.com/knative/pkg/logging/logkey"
|
||||
perrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/markbates/inflect"
|
||||
"github.com/mattbaird/jsonpatch"
|
||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
@ -224,6 +226,29 @@ func validate(old GenericCRD, new GenericCRD) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func setAnnotations(patches duck.JSONPatch, new, old GenericCRD, ui *authenticationv1.UserInfo) (duck.JSONPatch, error) {
|
||||
// Nowhere to set the annotations.
|
||||
if new == nil {
|
||||
return patches, nil
|
||||
}
|
||||
na, ok := new.(apis.Annotatable)
|
||||
if !ok {
|
||||
return patches, nil
|
||||
}
|
||||
var oa apis.Annotatable
|
||||
if old != nil {
|
||||
oa = old.(apis.Annotatable)
|
||||
}
|
||||
b, a := new.DeepCopyObject().(apis.Annotatable), na
|
||||
|
||||
a.AnnotateUserInfo(oa, ui)
|
||||
patch, err := duck.CreatePatch(b, a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(patches, patch...), nil
|
||||
}
|
||||
|
||||
// setDefaults simply leverages apis.Defaultable to set defaults.
|
||||
func setDefaults(patches duck.JSONPatch, crd GenericCRD) (duck.JSONPatch, error) {
|
||||
before, after := crd.DeepCopyObject(), crd
|
||||
|
@ -469,7 +494,7 @@ func (ac *AdmissionController) admit(ctx context.Context, request *admissionv1be
|
|||
return &admissionv1beta1.AdmissionResponse{Allowed: true}
|
||||
}
|
||||
|
||||
patchBytes, err := ac.mutate(ctx, request.Kind, request.OldObject.Raw, request.Object.Raw)
|
||||
patchBytes, err := ac.mutate(ctx, request)
|
||||
if err != nil {
|
||||
return makeErrorStatus("mutation failed: %v", err)
|
||||
}
|
||||
|
@ -485,7 +510,10 @@ func (ac *AdmissionController) admit(ctx context.Context, request *admissionv1be
|
|||
}
|
||||
}
|
||||
|
||||
func (ac *AdmissionController) mutate(ctx context.Context, kind metav1.GroupVersionKind, oldBytes []byte, newBytes []byte) ([]byte, error) {
|
||||
func (ac *AdmissionController) mutate(ctx context.Context, req *admissionv1beta1.AdmissionRequest) ([]byte, error) {
|
||||
kind := req.Kind
|
||||
newBytes := req.Object.Raw
|
||||
oldBytes := req.OldObject.Raw
|
||||
// Why, oh why are these different types...
|
||||
gvk := schema.GroupVersionKind{
|
||||
Group: kind.Group,
|
||||
|
@ -517,7 +545,7 @@ func (ac *AdmissionController) mutate(ctx context.Context, kind metav1.GroupVers
|
|||
return nil, fmt.Errorf("cannot decode incoming old object: %v", err)
|
||||
}
|
||||
}
|
||||
var patches []jsonpatch.JsonPatchOperation
|
||||
var patches duck.JSONPatch
|
||||
|
||||
// Add these before defaulting fields, otherwise defaulting may cause an illegal patch because
|
||||
// it expects the round tripped through Golang fields to be present already.
|
||||
|
@ -528,8 +556,8 @@ func (ac *AdmissionController) mutate(ctx context.Context, kind metav1.GroupVers
|
|||
patches = append(patches, rtp...)
|
||||
|
||||
if patches, err = updateGeneration(ctx, patches, oldObj, newObj); err != nil {
|
||||
logger.Errorw("failed to update generation", zap.Error(err))
|
||||
return nil, fmt.Errorf("Failed to update generation: %s", err)
|
||||
logger.Errorw("Failed to update generation", zap.Error(err))
|
||||
return nil, perrors.Wrap(err, "failed to update generation")
|
||||
}
|
||||
|
||||
if patches, err = setDefaults(patches, newObj); err != nil {
|
||||
|
@ -539,17 +567,21 @@ func (ac *AdmissionController) mutate(ctx context.Context, kind metav1.GroupVers
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if patches, err = setAnnotations(patches, newObj, oldObj, &req.UserInfo); err != nil {
|
||||
logger.Errorw("Failed the resource annotator", zap.Error(err))
|
||||
return nil, perrors.Wrap(err, "error setting annotations")
|
||||
}
|
||||
|
||||
// None of the validators will accept a nil value for newObj.
|
||||
if newObj == nil {
|
||||
return nil, errMissingNewObject
|
||||
}
|
||||
if err := validate(oldObj, newObj); err != nil {
|
||||
logger.Errorw("failed the resource specific validation", zap.Error(err))
|
||||
logger.Errorw("Failed the resource specific validation", zap.Error(err))
|
||||
// Return the error message as-is to give the validation callback
|
||||
// discretion over (our portion of) the message that the user sees.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(patches)
|
||||
}
|
||||
|
||||
|
@ -629,12 +661,12 @@ func hasChanged(ctx context.Context, old, new GenericCRD) (bool, error) {
|
|||
|
||||
oldSpecJSON, err := getSpecJSON(old)
|
||||
if err != nil {
|
||||
logger.Errorw("failed to get Spec JSON for old", zap.Error(err))
|
||||
logger.Errorw("Failed to get Spec JSON for old", zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
newSpecJSON, err := getSpecJSON(new)
|
||||
if err != nil {
|
||||
logger.Errorw("failed to get Spec JSON for new", zap.Error(err))
|
||||
logger.Errorw("Failed to get Spec JSON for new", zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
@ -647,10 +679,10 @@ func hasChanged(ctx context.Context, old, new GenericCRD) (bool, error) {
|
|||
}
|
||||
specPatchesJSON, err := json.Marshal(specPatches)
|
||||
if err != nil {
|
||||
logger.Errorw("failed to marshal spec patches", zap.Error(err))
|
||||
logger.Errorw("Failed to marshal spec patches", zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
logger.Infof("Specs differ:\n%+v\n", string(specPatchesJSON))
|
||||
logger.Infof("Specs differ:\n%s\n", string(specPatchesJSON))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import (
|
|||
"go.uber.org/zap"
|
||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
||||
|
@ -63,6 +64,8 @@ func newDefaultOptions() ControllerOptions {
|
|||
const (
|
||||
testNamespace = "test-namespace"
|
||||
testResourceName = "test-resource"
|
||||
user1 = "brutto@knative.dev"
|
||||
user2 = "arrabbiato@knative.dev"
|
||||
)
|
||||
|
||||
func newNonRunningTestAdmissionController(t *testing.T, options ControllerOptions) (
|
||||
|
@ -142,6 +145,7 @@ func TestValidCreateResourceSucceeds(t *testing.T) {
|
|||
expectAllowed(t, resp)
|
||||
expectPatches(t, resp.Patch, []jsonpatch.JsonPatchOperation{
|
||||
incrementGenerationPatch(r.Spec.Generation),
|
||||
setUserAnnotation(user1, user1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -162,6 +166,7 @@ func TestValidCreateResourceSucceedsWithDefaultPatch(t *testing.T) {
|
|||
Path: "/spec/fieldWithDefault",
|
||||
Value: "I'm a default.",
|
||||
},
|
||||
setUserAnnotation(user1, user1),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -240,8 +245,10 @@ func TestInvalidCreateResourceFails(t *testing.T) {
|
|||
func TestNopUpdateResourceSucceeds(t *testing.T) {
|
||||
r := createResource(1234, "a name")
|
||||
r.SetDefaults() // Fill in defaults to check that there are no patches.
|
||||
nr := r.DeepCopyObject().(*Resource)
|
||||
r.AnnotateUserInfo(nil, &authenticationv1.UserInfo{Username: user1})
|
||||
_, ac := newNonRunningTestAdmissionController(t, newDefaultOptions())
|
||||
resp := ac.admit(TestContextWithLogger(t), createUpdateResource(r, r))
|
||||
resp := ac.admit(TestContextWithLogger(t), createUpdateResource(r, nr))
|
||||
expectAllowed(t, resp)
|
||||
expectPatches(t, resp.Patch, []jsonpatch.JsonPatchOperation{})
|
||||
}
|
||||
|
@ -301,9 +308,50 @@ func TestUpdateGeneration(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidUpdateResourcePreserveAnnotations(t *testing.T) {
|
||||
old := createResource(1234, "a name")
|
||||
old.SetDefaults() // Fill in defaults to check that there are no patches.
|
||||
old.AnnotateUserInfo(nil, &authenticationv1.UserInfo{Username: user1})
|
||||
new := createResource(1234, "a name")
|
||||
new.SetDefaults()
|
||||
// User set annotations on the resource.
|
||||
new.ObjectMeta.SetAnnotations(map[string]string{
|
||||
"key": "to-my-heart",
|
||||
})
|
||||
|
||||
_, ac := newNonRunningTestAdmissionController(t, newDefaultOptions())
|
||||
resp := ac.admit(TestContextWithLogger(t), createUpdateResource(old, new))
|
||||
expectAllowed(t, resp)
|
||||
// No spec change => no user info change.
|
||||
expectPatches(t, resp.Patch, duck.JSONPatch{})
|
||||
}
|
||||
|
||||
func TestValidBigChangeResourceSucceeds(t *testing.T) {
|
||||
old := createResource(1234, "a name")
|
||||
old.SetDefaults() // Fill in defaults to check that there are no patches.
|
||||
old.AnnotateUserInfo(nil, &authenticationv1.UserInfo{Username: user1})
|
||||
new := createResource(1234, "a name")
|
||||
new.Spec.FieldWithDefault = "melon collie and the infinite sadness"
|
||||
|
||||
_, ac := newNonRunningTestAdmissionController(t, newDefaultOptions())
|
||||
resp := ac.admit(TestContextWithLogger(t), createUpdateResource(old, new))
|
||||
expectAllowed(t, resp)
|
||||
expectPatches(t, resp.Patch, []jsonpatch.JsonPatchOperation{{
|
||||
Operation: "replace",
|
||||
Path: "/spec/generation",
|
||||
Value: float64(1235),
|
||||
}, {
|
||||
Operation: "add",
|
||||
Path: "/spec/fieldThatsImmutableWithDefault",
|
||||
Value: "this is another default value",
|
||||
}, setUserAnnotation(user1, user2),
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidUpdateResourceSucceeds(t *testing.T) {
|
||||
old := createResource(1234, "a name")
|
||||
old.SetDefaults() // Fill in defaults to check that there are no patches.
|
||||
old.AnnotateUserInfo(nil, &authenticationv1.UserInfo{Username: user1})
|
||||
new := createResource(1234, "a name")
|
||||
// We clear the field that has a default.
|
||||
|
||||
|
@ -352,6 +400,7 @@ func TestInvalidUpdateResourceFailsImmutability(t *testing.T) {
|
|||
|
||||
func TestDefaultingImmutableFields(t *testing.T) {
|
||||
old := createResource(1234, "a name")
|
||||
old.AnnotateUserInfo(nil, &authenticationv1.UserInfo{Username: user1})
|
||||
new := createResource(1234, "a name")
|
||||
|
||||
// If we don't specify the new, but immutable field, we default it,
|
||||
|
@ -360,15 +409,20 @@ func TestDefaultingImmutableFields(t *testing.T) {
|
|||
_, ac := newNonRunningTestAdmissionController(t, newDefaultOptions())
|
||||
resp := ac.admit(TestContextWithLogger(t), createUpdateResource(old, new))
|
||||
expectAllowed(t, resp)
|
||||
expectPatches(t, resp.Patch, []jsonpatch.JsonPatchOperation{{
|
||||
Operation: "add",
|
||||
Path: "/spec/fieldThatsImmutableWithDefault",
|
||||
Value: "this is another default value",
|
||||
}, {
|
||||
Operation: "add",
|
||||
Path: "/spec/fieldWithDefault",
|
||||
Value: "I'm a default.",
|
||||
}})
|
||||
expectPatches(t, resp.Patch,
|
||||
[]jsonpatch.JsonPatchOperation{{
|
||||
Operation: "add",
|
||||
Path: "/spec/fieldThatsImmutableWithDefault",
|
||||
Value: "this is another default value",
|
||||
}, {
|
||||
Operation: "add",
|
||||
Path: "/spec/fieldWithDefault",
|
||||
Value: "I'm a default.",
|
||||
},
|
||||
// From WebHook perspective an update is happening here,
|
||||
// so the annotations must be set.
|
||||
setUserAnnotation(user1, user2),
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidWebhook(t *testing.T) {
|
||||
|
@ -589,7 +643,9 @@ func createBaseUpdateResource() *admissionv1beta1.AdmissionRequest {
|
|||
Version: "v1alpha1",
|
||||
Kind: "Resource",
|
||||
},
|
||||
}
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
Username: user2,
|
||||
}}
|
||||
}
|
||||
|
||||
func createUpdateResource(old, new *Resource) *admissionv1beta1.AdmissionRequest {
|
||||
|
@ -615,6 +671,9 @@ func createCreateResource(r *Resource) *admissionv1beta1.AdmissionRequest {
|
|||
Version: "v1alpha1",
|
||||
Kind: "Resource",
|
||||
},
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
Username: user1,
|
||||
},
|
||||
}
|
||||
marshaled, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
|
@ -678,8 +737,13 @@ func expectPatches(t *testing.T, a []byte, e []jsonpatch.JsonPatchOperation) {
|
|||
return lhs.Path < rhs.Path
|
||||
})
|
||||
|
||||
// Even though diff is useful, seeing the whole objects
|
||||
// one under another helps a lot.
|
||||
t.Logf("Got Patches: %#v", got)
|
||||
t.Logf("Want Patches: %#v", e)
|
||||
if diff := cmp.Diff(e, got, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("expectPatches (-want, +got) = %v", diff)
|
||||
t.Logf("diff Patches: %v", diff)
|
||||
t.Errorf("expectPatches (-want, +got) = %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -691,6 +755,29 @@ func incrementGenerationPatch(old int64) jsonpatch.JsonPatchOperation {
|
|||
}
|
||||
}
|
||||
|
||||
func updateAnnotationsWithUser(userC, userU string) []jsonpatch.JsonPatchOperation {
|
||||
// Just keys is being updated, so format is iffy.
|
||||
return []jsonpatch.JsonPatchOperation{{
|
||||
Operation: "add",
|
||||
Path: "/metadata/annotations/testing.knative.dev~1creator",
|
||||
Value: "brutto@knative.dev",
|
||||
}, {
|
||||
Operation: "add",
|
||||
Path: "/metadata/annotations/testing.knative.dev~1updater",
|
||||
Value: "arrabbiato@knative.dev",
|
||||
}}
|
||||
}
|
||||
func setUserAnnotation(userC, userU string) jsonpatch.JsonPatchOperation {
|
||||
return jsonpatch.JsonPatchOperation{
|
||||
Operation: "add",
|
||||
Path: "/metadata/annotations",
|
||||
Value: map[string]interface{}{
|
||||
CreatorAnnotation: userC,
|
||||
UpdaterAnnotation: userU,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewAdmissionController(client kubernetes.Interface, options ControllerOptions,
|
||||
logger *zap.SugaredLogger) (*AdmissionController, error) {
|
||||
return &AdmissionController{
|
||||
|
|
Loading…
Reference in New Issue