mirror of https://github.com/knative/caching.git
Auto-update dependencies (#83)
Produced via: `dep ensure -update knative.dev/test-infra knative.dev/pkg` /assign mattmoor
This commit is contained in:
parent
4362288203
commit
7fb04b4264
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue