Auto-update dependencies (#83)

Produced via:
  `dep ensure -update knative.dev/test-infra knative.dev/pkg`
/assign mattmoor
This commit is contained in:
mattmoor-sockpuppet 2019-08-28 07:31:42 -07:00 committed by Knative Prow Robot
parent 4362288203
commit 7fb04b4264
13 changed files with 435 additions and 322 deletions

6
Gopkg.lock generated
View File

@ -927,7 +927,7 @@
[[projects]]
branch = "master"
digest = "1:655572a72adfa087e24c0d8ca498a1f517ecddee1acf9768e9e89c8c3178a9e5"
digest = "1:bb5bc945cb10dbe8d011d5b6477576bd72ef6420a1329c5f12e1f5de1db32877"
name = "knative.dev/pkg"
packages = [
"apis",
@ -946,7 +946,7 @@
"metrics/metricskey",
]
pruneopts = "T"
revision = "73863941e2242e1c0e47bb70021c915707995b9f"
revision = "9118872a32f620bbf4798b4e6103094f6b7bce6c"
[[projects]]
branch = "master"
@ -957,7 +957,7 @@
"tools/dep-collector",
]
pruneopts = "UT"
revision = "b639ab478b1e4882090abb51a043ba2dc9aaafcb"
revision = "5ce07d0fc6603e4cf576f91cdd85dcd74c4d629c"
[solve-meta]
analyzer-name = "dep"

15
vendor/knative.dev/pkg/Gopkg.lock generated vendored
View File

@ -240,21 +240,21 @@
revision = "c3068f13fcc3961fd05f96f13c8250e350db4209"
[[projects]]
digest = "1:3efb665a5beaa0266ff287cdb58dff8a966631000e9f8ad8d832a288b90ca247"
digest = "1:d7736b4372fa7b6d447118aca5a9a6d90a61d4e0d467316ac631e4ba58458ae4"
name = "github.com/google/mako"
packages = [
"clients/proto/analyzers/threshold_analyzer_go_proto",
"clients/proto/analyzers/utest_analyzer_go_proto",
"clients/proto/analyzers/window_deviation_go_proto",
"helpers/go/quickstore",
"helpers/proto/quickstore/quickstore_go_proto",
"go/quickstore",
"internal/go/common",
"internal/quickstore_microservice/proto/quickstore_go_proto",
"proto/quickstore/quickstore_go_proto",
"spec/proto/mako_go_proto",
]
pruneopts = "NUT"
revision = "d56a6e811df75f8e4d6e7c256d25acf19ef16d03"
version = "v0.0.0-rc.4"
revision = "122f8dcef9e3906310e7dba05849cedb5be43b24"
version = "0.1.0"
[[projects]]
digest = "1:ab3ec1fe3e39bac4b3ab63390767766622be35b7cab03f47f787f9ec60522a53"
@ -1280,8 +1280,8 @@
"github.com/google/go-cmp/cmp/cmpopts",
"github.com/google/go-github/github",
"github.com/google/mako/clients/proto/analyzers/threshold_analyzer_go_proto",
"github.com/google/mako/helpers/go/quickstore",
"github.com/google/mako/helpers/proto/quickstore/quickstore_go_proto",
"github.com/google/mako/go/quickstore",
"github.com/google/mako/proto/quickstore/quickstore_go_proto",
"github.com/google/mako/spec/proto/mako_go_proto",
"github.com/google/uuid",
"github.com/gorilla/websocket",
@ -1356,7 +1356,6 @@
"k8s.io/client-go/kubernetes",
"k8s.io/client-go/kubernetes/fake",
"k8s.io/client-go/kubernetes/scheme",
"k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1",
"k8s.io/client-go/kubernetes/typed/core/v1",
"k8s.io/client-go/plugin/pkg/client/auth/gcp",
"k8s.io/client-go/rest",

View File

@ -58,6 +58,10 @@ required = [
name = "contrib.go.opencensus.io/exporter/stackdriver"
version = "v0.12.2"
[[constraint]]
name = "github.com/google/mako"
version = "v0.1.0"
[[constraint]]
name = "knative.dev/test-infra"
branch = "master"

View File

@ -125,6 +125,25 @@ type DestinationRuleSpec struct {
// One or more named sets that represent individual versions of a
// service. Traffic policies can be overridden at subset level.
Subsets []Subset `json:"subsets,omitempty"`
// A list of namespaces to which this destination rule is exported.
// The resolution of a destination rule to apply to a service occurs in the
// context of a hierarchy of namespaces. Exporting a destination rule allows
// it to be included in the resolution hierarchy for services in
// other namespaces. This feature provides a mechanism for service owners
// and mesh administrators to control the visibility of destination rules
// across namespace boundaries.
//
// If no namespaces are specified then the destination rule is exported to all
// namespaces by default.
//
// The value "." is reserved and defines an export to the same namespace that
// the destination rule is declared in. Similarly, the value "*" is reserved and
// defines an export to all namespaces.
//
// NOTE: in the current release, the `exportTo` value is restricted to
// "." or "*" (i.e., the current namespace or all namespaces).
ExportTo []string `json:"exportTo,omitempty"`
}
// Traffic policies to apply for a specific destination, across all

View File

@ -141,6 +141,25 @@ type VirtualServiceSpec struct {
TCP []TCPRoute `json:"tcp,omitempty"`
TLS []TLSRoute `json:"tls,omitempty"`
// A list of namespaces to which this destination rule is exported.
// The resolution of a destination rule to apply to a service occurs in the
// context of a hierarchy of namespaces. Exporting a destination rule allows
// it to be included in the resolution hierarchy for services in
// other namespaces. This feature provides a mechanism for service owners
// and mesh administrators to control the visibility of destination rules
// across namespace boundaries.
//
// If no namespaces are specified then the destination rule is exported to all
// namespaces by default.
//
// The value "." is reserved and defines an export to the same namespace that
// the destination rule is declared in. Similarly, the value "*" is reserved and
// defines an export to all namespaces.
//
// NOTE: in the current release, the `exportTo` value is restricted to
// "." or "*" (i.e., the current namespace or all namespaces).
ExportTo []string `json:"exportTo,omitempty"`
}
// Describes match conditions and actions for routing HTTP/1.1, HTTP2, and

View File

@ -200,6 +200,11 @@ func (in *DestinationRuleSpec) DeepCopyInto(out *DestinationRuleSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ExportTo != nil {
in, out := &in.ExportTo, &out.ExportTo
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
@ -1145,6 +1150,11 @@ func (in *VirtualServiceSpec) DeepCopyInto(out *VirtualServiceSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ExportTo != nil {
in, out := &in.ExportTo, &out.ExportTo
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}

View File

@ -70,11 +70,11 @@ func (r *URIResolver) URIFromDestination(dest apisv1alpha1.Destination, parent i
if err != nil {
return "", err
}
return extendPath(url, dest.Path).String(), nil
return extendPath(url.DeepCopy(), dest.Path).String(), nil
}
if dest.URI != nil {
return extendPath(dest.URI, dest.Path).String(), nil
return extendPath(dest.URI.DeepCopy(), dest.Path).String(), nil
}
return "", fmt.Errorf("destination missing ObjectReference and URI, expected exactly one")

View File

@ -51,6 +51,7 @@ func init() {
contents, err := ioutil.ReadFile(configFile)
if err != nil {
log.Printf("Failed to load the config file: %v", err)
return
}
config := &config{}
if err = yaml.Unmarshal(contents, &config); err != nil {

View File

@ -18,6 +18,7 @@ package mako
import (
"context"
"fmt"
"log"
"runtime"
"strings"
@ -25,8 +26,9 @@ import (
"cloud.google.com/go/compute/metadata"
"knative.dev/pkg/injection/clients/kubeclient"
"github.com/google/mako/helpers/go/quickstore"
qpb "github.com/google/mako/helpers/proto/quickstore/quickstore_go_proto"
"github.com/google/mako/go/quickstore"
qpb "github.com/google/mako/proto/quickstore/quickstore_go_proto"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"knative.dev/pkg/changeset"
"knative.dev/pkg/controller"
@ -69,17 +71,35 @@ func Setup(ctx context.Context, extraTags ...string) (context.Context, *quicksto
}
// Get the Kubernetes version from the API server.
version, err := kubeclient.Get(ctx).Discovery().ServerVersion()
kc := kubeclient.Get(ctx)
version, err := kc.Discovery().ServerVersion()
if err != nil {
return nil, nil, nil, err
}
// Get GCP project ID as a tag.
// Determine the number of Kubernetes nodes through the kubernetes client.
nodes, err := kc.CoreV1().Nodes().List(metav1.ListOptions{})
if err != nil {
return nil, nil, nil, err
}
tags = append(tags, "nodes="+fmt.Sprintf("%d", len(nodes.Items)))
// Decorate GCP metadata as tags (when we're running on GCP).
if projectID, err := metadata.ProjectID(); err != nil {
log.Printf("GCP project ID is not available: %v", err)
} else {
tags = append(tags, "project-id="+EscapeTag(projectID))
}
if zone, err := metadata.Zone(); err != nil {
log.Printf("GCP zone is not available: %v", err)
} else {
tags = append(tags, "zone="+EscapeTag(zone))
}
if machineType, err := metadata.Get("instance/machine-type"); err != nil {
log.Printf("GCP machine type is not available: %v", err)
} else if parts := strings.Split(machineType, "/"); len(parts) != 4 {
tags = append(tags, "instanceType="+EscapeTag(parts[3]))
}
qs, qclose, err := quickstore.NewAtAddress(ctx, &qpb.QuickstoreInput{
BenchmarkKey: MustGetBenchmark(),

View File

@ -162,7 +162,7 @@ func (a *APICoverageRecorder) GetResourceCoverage(w http.ResponseWriter, r *http
var ignoredFields coveragecalculator.IgnoredFields
ignoredFieldsFilePath := os.Getenv("KO_DATA_PATH") + "/ignoredfields.yaml"
if err := ignoredFields.ReadFromFile(ignoredFieldsFilePath); err != nil {
fmt.Fprintf(w, "Error reading file: %s", ignoredFieldsFilePath)
a.Logger.Errorf("Error reading file %s: %v", ignoredFieldsFilePath, err)
}
tree := a.ResourceForest.TopLevelTrees[resource]
@ -184,7 +184,7 @@ func (a *APICoverageRecorder) GetTotalCoverage(w http.ResponseWriter, r *http.Re
ignoredFieldsFilePath := os.Getenv("KO_DATA_PATH") + "/ignoredfields.yaml"
if err = ignoredFields.ReadFromFile(ignoredFieldsFilePath); err != nil {
fmt.Fprintf(w, "error reading file: %s error: %v", ignoredFieldsFilePath, err)
a.Logger.Errorf("Error reading file %s: %v", ignoredFieldsFilePath, err)
}
totalCoverage := coveragecalculator.CoverageValues{}
@ -200,6 +200,7 @@ func (a *APICoverageRecorder) GetTotalCoverage(w http.ResponseWriter, r *http.Re
var body []byte
if body, err = json.Marshal(totalCoverage); err != nil {
fmt.Fprintf(w, "error marshalling total coverage response: %v", err)
return
}
if _, err = w.Write(body); err != nil {

View File

@ -237,7 +237,7 @@ func BuildWebhookConfiguration(componentCommonName string, webhookName string, n
FailurePolicy: admissionregistrationv1beta1.Fail,
ClientAuth: tls.NoClientCert,
RegistrationDelay: time.Second * 2,
Port: 443,
Port: 8443, // Using port greater than 1024 as webhook runs as non-root user.
Namespace: namespace,
DeploymentName: componentCommonName,
ServiceName: componentCommonName,

View File

@ -0,0 +1,325 @@
/*
Copyright 2019 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 webhook
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/markbates/inflect"
"github.com/mattbaird/jsonpatch"
"go.uber.org/zap"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
"knative.dev/pkg/apis"
"knative.dev/pkg/apis/duck"
"knative.dev/pkg/kmp"
"knative.dev/pkg/logging"
)
// ResourceCallback defines a signature for resource specific (Route, Configuration, etc.)
// handlers that can validate and mutate an object. If non-nil error is returned, object mutation
// is denied. Mutations should be appended to the patches operations.
type ResourceCallback func(patches *[]jsonpatch.JsonPatchOperation, old GenericCRD, new GenericCRD) error
// ResourceDefaulter defines a signature for resource specific (Route, Configuration, etc.)
// handlers that can set defaults on an object. If non-nil error is returned, object mutation
// is denied. Mutations should be appended to the patches operations.
type ResourceDefaulter func(patches *[]jsonpatch.JsonPatchOperation, crd GenericCRD) error
// GenericCRD is the interface definition that allows us to perform the generic
// CRD actions like deciding whether to increment generation and so forth.
type GenericCRD interface {
apis.Defaultable
apis.Validatable
runtime.Object
}
type ResourceAdmissionController struct {
Handlers map[schema.GroupVersionKind]GenericCRD
Options ControllerOptions
DisallowUnknownFields bool
}
func (ac *ResourceAdmissionController) Admit(ctx context.Context, request *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
logger := logging.FromContext(ctx)
switch request.Operation {
case admissionv1beta1.Create, admissionv1beta1.Update:
default:
logger.Infof("Unhandled webhook operation, letting it through %v", request.Operation)
return &admissionv1beta1.AdmissionResponse{Allowed: true}
}
patchBytes, err := ac.mutate(ctx, request)
if err != nil {
return makeErrorStatus("mutation failed: %v", err)
}
logger.Infof("Kind: %q PatchBytes: %v", request.Kind, string(patchBytes))
return &admissionv1beta1.AdmissionResponse{
Patch: patchBytes,
Allowed: true,
PatchType: func() *admissionv1beta1.PatchType {
pt := admissionv1beta1.PatchTypeJSONPatch
return &pt
}(),
}
}
func (ac *ResourceAdmissionController) Register(ctx context.Context, kubeClient kubernetes.Interface, caCert []byte) error {
client := kubeClient.AdmissionregistrationV1beta1().MutatingWebhookConfigurations()
logger := logging.FromContext(ctx)
failurePolicy := admissionregistrationv1beta1.Fail
var rules []admissionregistrationv1beta1.RuleWithOperations
for gvk := range ac.Handlers {
plural := strings.ToLower(inflect.Pluralize(gvk.Kind))
rules = append(rules, admissionregistrationv1beta1.RuleWithOperations{
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Create,
admissionregistrationv1beta1.Update,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{gvk.Group},
APIVersions: []string{gvk.Version},
Resources: []string{plural + "/*"},
},
})
}
// Sort the rules by Group, Version, Kind so that things are deterministically ordered.
sort.Slice(rules, func(i, j int) bool {
lhs, rhs := rules[i], rules[j]
if lhs.APIGroups[0] != rhs.APIGroups[0] {
return lhs.APIGroups[0] < rhs.APIGroups[0]
}
if lhs.APIVersions[0] != rhs.APIVersions[0] {
return lhs.APIVersions[0] < rhs.APIVersions[0]
}
return lhs.Resources[0] < rhs.Resources[0]
})
webhook := &admissionregistrationv1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: ac.Options.WebhookName,
},
Webhooks: []admissionregistrationv1beta1.Webhook{{
Name: ac.Options.WebhookName,
Rules: rules,
ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
Service: &admissionregistrationv1beta1.ServiceReference{
Namespace: ac.Options.Namespace,
Name: ac.Options.ServiceName,
},
CABundle: caCert,
},
FailurePolicy: &failurePolicy,
}},
}
// Set the owner to our deployment.
deployment, err := kubeClient.Apps().Deployments(ac.Options.Namespace).Get(ac.Options.DeploymentName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to fetch our deployment: %v", err)
}
deploymentRef := metav1.NewControllerRef(deployment, deploymentKind)
webhook.OwnerReferences = append(webhook.OwnerReferences, *deploymentRef)
// Try to create the webhook and if it already exists validate webhook rules.
_, err = client.Create(webhook)
if err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("failed to create a webhook: %v", err)
}
logger.Info("Webhook already exists")
configuredWebhook, err := client.Get(ac.Options.WebhookName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("error retrieving webhook: %v", err)
}
if ok, err := kmp.SafeEqual(configuredWebhook.Webhooks, webhook.Webhooks); err != nil {
return fmt.Errorf("error diffing webhooks: %v", err)
} else if !ok {
logger.Info("Updating webhook")
// Set the ResourceVersion as required by update.
webhook.ObjectMeta.ResourceVersion = configuredWebhook.ObjectMeta.ResourceVersion
if _, err := client.Update(webhook); err != nil {
return fmt.Errorf("failed to update webhook: %s", err)
}
} else {
logger.Info("Webhook is already valid")
}
} else {
logger.Info("Created a webhook")
}
return nil
}
func (ac *ResourceAdmissionController) 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,
Version: kind.Version,
Kind: kind.Kind,
}
logger := logging.FromContext(ctx)
handler, ok := ac.Handlers[gvk]
if !ok {
logger.Errorf("Unhandled kind: %v", gvk)
return nil, fmt.Errorf("unhandled kind: %v", gvk)
}
// nil values denote absence of `old` (create) or `new` (delete) objects.
var oldObj, newObj GenericCRD
if len(newBytes) != 0 {
newObj = handler.DeepCopyObject().(GenericCRD)
newDecoder := json.NewDecoder(bytes.NewBuffer(newBytes))
if ac.DisallowUnknownFields {
newDecoder.DisallowUnknownFields()
}
if err := newDecoder.Decode(&newObj); err != nil {
return nil, fmt.Errorf("cannot decode incoming new object: %v", err)
}
}
if len(oldBytes) != 0 {
oldObj = handler.DeepCopyObject().(GenericCRD)
oldDecoder := json.NewDecoder(bytes.NewBuffer(oldBytes))
if ac.DisallowUnknownFields {
oldDecoder.DisallowUnknownFields()
}
if err := oldDecoder.Decode(&oldObj); err != nil {
return nil, fmt.Errorf("cannot decode incoming old object: %v", err)
}
}
var patches duck.JSONPatch
var err error
// Skip this step if the type we're dealing with is a duck type, since it is inherently
// incomplete and this will patch away all of the unspecified fields.
if _, ok := newObj.(duck.Populatable); !ok {
// 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.
rtp, err := roundTripPatch(newBytes, newObj)
if err != nil {
return nil, fmt.Errorf("cannot create patch for round tripped newBytes: %v", err)
}
patches = append(patches, rtp...)
}
// Set up the context for defaulting and validation
if oldObj != nil {
// Copy the old object and set defaults so that we don't reject our own
// defaulting done earlier in the webhook.
oldObj = oldObj.DeepCopyObject().(GenericCRD)
oldObj.SetDefaults(ctx)
s, ok := oldObj.(apis.HasSpec)
if ok {
SetUserInfoAnnotations(s, ctx, req.Resource.Group)
}
if req.SubResource == "" {
ctx = apis.WithinUpdate(ctx, oldObj)
} else {
ctx = apis.WithinSubResourceUpdate(ctx, oldObj, req.SubResource)
}
} else {
ctx = apis.WithinCreate(ctx)
}
ctx = apis.WithUserInfo(ctx, &req.UserInfo)
// Default the new object.
if patches, err = setDefaults(ctx, patches, newObj); err != nil {
logger.Errorw("Failed the resource specific defaulter", zap.Error(err))
// Return the error message as-is to give the defaulter callback
// discretion over (our portion of) the message that the user sees.
return nil, err
}
if patches, err = ac.setUserInfoAnnotations(ctx, patches, newObj, req.Resource.Group); err != nil {
logger.Errorw("Failed the resource user info annotator", zap.Error(err))
return nil, err
}
// None of the validators will accept a nil value for newObj.
if newObj == nil {
return nil, errMissingNewObject
}
if err := validate(ctx, newObj); err != nil {
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)
}
func (ac *ResourceAdmissionController) setUserInfoAnnotations(ctx context.Context, patches duck.JSONPatch, new GenericCRD, groupName string) (duck.JSONPatch, error) {
if new == nil {
return patches, nil
}
nh, ok := new.(apis.HasSpec)
if !ok {
return patches, nil
}
b, a := new.DeepCopyObject().(apis.HasSpec), nh
SetUserInfoAnnotations(nh, ctx, groupName)
patch, err := duck.CreatePatch(b, a)
if err != nil {
return nil, err
}
return append(patches, patch...), nil
}
// roundTripPatch generates the JSONPatch that corresponds to round tripping the given bytes through
// the Golang type (JSON -> Golang type -> JSON). Because it is not always true that
// bytes == json.Marshal(json.Unmarshal(bytes)).
//
// For example, if bytes did not contain a 'spec' field and the Golang type specifies its 'spec'
// field without omitempty, then by round tripping through the Golang type, we would have added
// `'spec': {}`.
func roundTripPatch(bytes []byte, unmarshalled interface{}) (duck.JSONPatch, error) {
if unmarshalled == nil {
return duck.JSONPatch{}, nil
}
marshaledBytes, err := json.Marshal(unmarshalled)
if err != nil {
return nil, fmt.Errorf("cannot marshal interface: %v", err)
}
return jsonpatch.CreatePatch(bytes, marshaledBytes)
}

View File

@ -17,7 +17,6 @@ limitations under the License.
package webhook
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
@ -25,30 +24,22 @@ import (
"errors"
"fmt"
"net/http"
"sort"
"strings"
"time"
"go.uber.org/zap"
"knative.dev/pkg/apis"
"knative.dev/pkg/apis/duck"
"knative.dev/pkg/kmp"
"knative.dev/pkg/logging"
"knative.dev/pkg/logging/logkey"
"github.com/markbates/inflect"
"github.com/mattbaird/jsonpatch"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
clientadmissionregistrationv1beta1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1"
)
const (
@ -105,34 +96,15 @@ type ControllerOptions struct {
StatsReporter StatsReporter
}
// ResourceCallback defines a signature for resource specific (Route, Configuration, etc.)
// handlers that can validate and mutate an object. If non-nil error is returned, object creation
// is denied. Mutations should be appended to the patches operations.
type ResourceCallback func(patches *[]jsonpatch.JsonPatchOperation, old GenericCRD, new GenericCRD) error
// ResourceDefaulter defines a signature for resource specific (Route, Configuration, etc.)
// handlers that can set defaults on an object. If non-nil error is returned, object creation
// is denied. Mutations should be appended to the patches operations.
type ResourceDefaulter func(patches *[]jsonpatch.JsonPatchOperation, crd GenericCRD) error
// AdmissionController implements the external admission webhook for validation of
// pilot configuration.
type AdmissionController struct {
Client kubernetes.Interface
Options ControllerOptions
Handlers map[schema.GroupVersionKind]GenericCRD
Logger *zap.SugaredLogger
Client kubernetes.Interface
Options ControllerOptions
Logger *zap.SugaredLogger
resourceAdmissionController ResourceAdmissionController
WithContext func(context.Context) context.Context
DisallowUnknownFields bool
}
// GenericCRD is the interface definition that allows us to perform the generic
// CRD actions like deciding whether to increment generation and so forth.
type GenericCRD interface {
apis.Defaultable
apis.Validatable
runtime.Object
WithContext func(context.Context) context.Context
}
// NewAdmissionController constructs an AdmissionController
@ -153,12 +125,15 @@ func NewAdmissionController(
}
return &AdmissionController{
Client: client,
Options: opts,
Handlers: handlers,
Logger: logger,
WithContext: ctx,
DisallowUnknownFields: disallowUnknownFields,
Client: client,
Options: opts,
resourceAdmissionController: ResourceAdmissionController{
Handlers: handlers,
Options: opts,
DisallowUnknownFields: disallowUnknownFields,
},
Logger: logger,
WithContext: ctx,
}, nil
}
@ -238,7 +213,7 @@ func getOrGenerateKeyCertsFromSecret(ctx context.Context, client kubernetes.Inte
// validate performs validation on the provided "new" CRD.
// For legacy purposes, this also does apis.Immutable validation,
// which is deprecated and will be removed in a future release.
func validate(ctx context.Context, new GenericCRD) error {
func validate(ctx context.Context, new apis.Validatable) error {
if apis.IsInUpdate(ctx) {
old := apis.GetBaseline(ctx)
if immutableNew, ok := new.(apis.Immutable); ok {
@ -317,8 +292,7 @@ func (ac *AdmissionController) Run(stop <-chan struct{}) error {
select {
case <-time.After(ac.Options.RegistrationDelay):
cl := ac.Client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations()
if err := ac.register(ctx, cl, caCert); err != nil {
if err := ac.resourceAdmissionController.Register(ctx, ac.Client, caCert); err != nil {
logger.Errorw("failed to register webhook", zap.Error(err))
return err
}
@ -343,97 +317,6 @@ func (ac *AdmissionController) Run(stop <-chan struct{}) error {
}
}
// Register registers the external admission webhook for pilot
// configuration types.
func (ac *AdmissionController) register(
ctx context.Context, client clientadmissionregistrationv1beta1.MutatingWebhookConfigurationInterface, caCert []byte) error { // nolint: lll
logger := logging.FromContext(ctx)
failurePolicy := admissionregistrationv1beta1.Fail
var rules []admissionregistrationv1beta1.RuleWithOperations
for gvk := range ac.Handlers {
plural := strings.ToLower(inflect.Pluralize(gvk.Kind))
rules = append(rules, admissionregistrationv1beta1.RuleWithOperations{
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Create,
admissionregistrationv1beta1.Update,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{gvk.Group},
APIVersions: []string{gvk.Version},
Resources: []string{plural + "/*"},
},
})
}
// Sort the rules by Group, Version, Kind so that things are deterministically ordered.
sort.Slice(rules, func(i, j int) bool {
lhs, rhs := rules[i], rules[j]
if lhs.APIGroups[0] != rhs.APIGroups[0] {
return lhs.APIGroups[0] < rhs.APIGroups[0]
}
if lhs.APIVersions[0] != rhs.APIVersions[0] {
return lhs.APIVersions[0] < rhs.APIVersions[0]
}
return lhs.Resources[0] < rhs.Resources[0]
})
webhook := &admissionregistrationv1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: ac.Options.WebhookName,
},
Webhooks: []admissionregistrationv1beta1.Webhook{{
Name: ac.Options.WebhookName,
Rules: rules,
ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
Service: &admissionregistrationv1beta1.ServiceReference{
Namespace: ac.Options.Namespace,
Name: ac.Options.ServiceName,
},
CABundle: caCert,
},
FailurePolicy: &failurePolicy,
}},
}
// Set the owner to our deployment.
deployment, err := ac.Client.Apps().Deployments(ac.Options.Namespace).Get(ac.Options.DeploymentName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to fetch our deployment: %v", err)
}
deploymentRef := metav1.NewControllerRef(deployment, deploymentKind)
webhook.OwnerReferences = append(webhook.OwnerReferences, *deploymentRef)
// Try to create the webhook and if it already exists validate webhook rules.
_, err = client.Create(webhook)
if err != nil {
if !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("failed to create a webhook: %v", err)
}
logger.Info("Webhook already exists")
configuredWebhook, err := client.Get(ac.Options.WebhookName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("error retrieving webhook: %v", err)
}
if ok, err := kmp.SafeEqual(configuredWebhook.Webhooks, webhook.Webhooks); err != nil {
return fmt.Errorf("error diffing webhooks: %v", err)
} else if !ok {
logger.Info("Updating webhook")
// Set the ResourceVersion as required by update.
webhook.ObjectMeta.ResourceVersion = configuredWebhook.ObjectMeta.ResourceVersion
if _, err := client.Update(webhook); err != nil {
return fmt.Errorf("failed to update webhook: %s", err)
}
} else {
logger.Info("Webhook is already valid")
}
} else {
logger.Info("Created a webhook")
}
return nil
}
// ServeHTTP implements the external admission webhook for mutating
// serving resources.
func (ac *AdmissionController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -468,7 +351,7 @@ func (ac *AdmissionController) ServeHTTP(w http.ResponseWriter, r *http.Request)
ctx = ac.WithContext(ctx)
}
reviewResponse := ac.admit(ctx, review.Request)
reviewResponse := ac.resourceAdmissionController.Admit(ctx, review.Request)
var response admissionv1beta1.AdmissionReview
if reviewResponse != nil {
response.Response = reviewResponse
@ -497,174 +380,6 @@ func makeErrorStatus(reason string, args ...interface{}) *admissionv1beta1.Admis
}
}
func (ac *AdmissionController) admit(ctx context.Context, request *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
logger := logging.FromContext(ctx)
switch request.Operation {
case admissionv1beta1.Create, admissionv1beta1.Update:
default:
logger.Infof("Unhandled webhook operation, letting it through %v", request.Operation)
return &admissionv1beta1.AdmissionResponse{Allowed: true}
}
patchBytes, err := ac.mutate(ctx, request)
if err != nil {
return makeErrorStatus("mutation failed: %v", err)
}
logger.Infof("Kind: %q PatchBytes: %v", request.Kind, string(patchBytes))
return &admissionv1beta1.AdmissionResponse{
Patch: patchBytes,
Allowed: true,
PatchType: func() *admissionv1beta1.PatchType {
pt := admissionv1beta1.PatchTypeJSONPatch
return &pt
}(),
}
}
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,
Version: kind.Version,
Kind: kind.Kind,
}
logger := logging.FromContext(ctx)
handler, ok := ac.Handlers[gvk]
if !ok {
logger.Errorf("Unhandled kind: %v", gvk)
return nil, fmt.Errorf("unhandled kind: %v", gvk)
}
// nil values denote absence of `old` (create) or `new` (delete) objects.
var oldObj, newObj GenericCRD
if len(newBytes) != 0 {
newObj = handler.DeepCopyObject().(GenericCRD)
newDecoder := json.NewDecoder(bytes.NewBuffer(newBytes))
if ac.DisallowUnknownFields {
newDecoder.DisallowUnknownFields()
}
if err := newDecoder.Decode(&newObj); err != nil {
return nil, fmt.Errorf("cannot decode incoming new object: %v", err)
}
}
if len(oldBytes) != 0 {
oldObj = handler.DeepCopyObject().(GenericCRD)
oldDecoder := json.NewDecoder(bytes.NewBuffer(oldBytes))
if ac.DisallowUnknownFields {
oldDecoder.DisallowUnknownFields()
}
if err := oldDecoder.Decode(&oldObj); err != nil {
return nil, fmt.Errorf("cannot decode incoming old object: %v", err)
}
}
var patches duck.JSONPatch
var err error
// Skip this step if the type we're dealing with is a duck type, since it is inherently
// incomplete and this will patch away all of the unspecified fields.
if _, ok := newObj.(duck.Populatable); !ok {
// 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.
rtp, err := roundTripPatch(newBytes, newObj)
if err != nil {
return nil, fmt.Errorf("cannot create patch for round tripped newBytes: %v", err)
}
patches = append(patches, rtp...)
}
// Set up the context for defaulting and validation
if oldObj != nil {
// Copy the old object and set defaults so that we don't reject our own
// defaulting done earlier in the webhook.
oldObj = oldObj.DeepCopyObject().(GenericCRD)
oldObj.SetDefaults(ctx)
s, ok := oldObj.(apis.HasSpec)
if ok {
SetUserInfoAnnotations(s, ctx, req.Resource.Group)
}
if req.SubResource == "" {
ctx = apis.WithinUpdate(ctx, oldObj)
} else {
ctx = apis.WithinSubResourceUpdate(ctx, oldObj, req.SubResource)
}
} else {
ctx = apis.WithinCreate(ctx)
}
ctx = apis.WithUserInfo(ctx, &req.UserInfo)
// Default the new object.
if patches, err = setDefaults(ctx, patches, newObj); err != nil {
logger.Errorw("Failed the resource specific defaulter", zap.Error(err))
// Return the error message as-is to give the defaulter callback
// discretion over (our portion of) the message that the user sees.
return nil, err
}
if patches, err = ac.setUserInfoAnnotations(ctx, patches, newObj, req.Resource.Group); err != nil {
logger.Errorw("Failed the resource user info annotator", zap.Error(err))
return nil, err
}
// None of the validators will accept a nil value for newObj.
if newObj == nil {
return nil, errMissingNewObject
}
if err := validate(ctx, newObj); err != nil {
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)
}
func (ac *AdmissionController) setUserInfoAnnotations(ctx context.Context, patches duck.JSONPatch, new GenericCRD, groupName string) (duck.JSONPatch, error) {
if new == nil {
return patches, nil
}
nh, ok := new.(apis.HasSpec)
if !ok {
return patches, nil
}
b, a := new.DeepCopyObject().(apis.HasSpec), nh
SetUserInfoAnnotations(nh, ctx, groupName)
patch, err := duck.CreatePatch(b, a)
if err != nil {
return nil, err
}
return append(patches, patch...), nil
}
// roundTripPatch generates the JSONPatch that corresponds to round tripping the given bytes through
// the Golang type (JSON -> Golang type -> JSON). Because it is not always true that
// bytes == json.Marshal(json.Unmarshal(bytes)).
//
// For example, if bytes did not contain a 'spec' field and the Golang type specifies its 'spec'
// field without omitempty, then by round tripping through the Golang type, we would have added
// `'spec': {}`.
func roundTripPatch(bytes []byte, unmarshalled interface{}) (duck.JSONPatch, error) {
if unmarshalled == nil {
return duck.JSONPatch{}, nil
}
marshaledBytes, err := json.Marshal(unmarshalled)
if err != nil {
return nil, fmt.Errorf("cannot marshal interface: %v", err)
}
return jsonpatch.CreatePatch(bytes, marshaledBytes)
}
func generateSecret(ctx context.Context, options *ControllerOptions) (*corev1.Secret, error) {
serverKey, serverCert, caCert, err := CreateCerts(ctx, options.ServiceName, options.Namespace)
if err != nil {