diff --git a/Gopkg.lock b/Gopkg.lock index 754e08de..f08fc481 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -933,7 +933,7 @@ [[projects]] branch = "master" - digest = "1:09a00f21eaeeae387d973fd3f82cf06a8627f88566b4e6811ba97003dd207eac" + digest = "1:bfb453e78558b31d99d1335840a7d92328fc85fe35c58d4c5179caddb28f7416" name = "knative.dev/pkg" packages = [ "apis", @@ -952,18 +952,18 @@ "metrics/metricskey", ] pruneopts = "T" - revision = "944655d6cc17ceb861b0764046c56a465fe5bb75" + revision = "c259ed6f2e751582d4586c2d3a4e1cb98e526068" [[projects]] branch = "master" - digest = "1:85fe0cadd6ab83f3d7f948c60b6d422dc9cd16664246249968dab5d828ae8dfd" + digest = "1:6e839a9b4183b0fbf0df609fe9a6d226c941fa9221052ba0075b25cb353588a5" name = "knative.dev/test-infra" packages = [ "scripts", "tools/dep-collector", ] pruneopts = "UT" - revision = "2c4cd9df33c648a86d95bdd4119386cba04cde56" + revision = "9363d2f4cafd2ad3c1bfc925f447c91a1776be00" [[projects]] digest = "1:8730e0150dfb2b7e173890c8b9868e7a273082ef8e39f4940e3506a481cf895c" diff --git a/vendor/knative.dev/pkg/apis/duck/const.go b/vendor/knative.dev/pkg/apis/duck/const.go new file mode 100644 index 00000000..4fe4a7aa --- /dev/null +++ b/vendor/knative.dev/pkg/apis/duck/const.go @@ -0,0 +1,25 @@ +/* +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 duck + +const ( + // BindingExcludeLabel is a label that is placed on namespaces and + // resources to exclude them from consideration when binding things. + // It is critical that bindings dealing with Deployments label their + // controller Deployment (or enclosing namespace). + BindingExcludeLabel = "bindings.knative.dev/exclude" +) diff --git a/vendor/knative.dev/pkg/apis/duck/interface.go b/vendor/knative.dev/pkg/apis/duck/interface.go index f99a6363..99cf4ac2 100644 --- a/vendor/knative.dev/pkg/apis/duck/interface.go +++ b/vendor/knative.dev/pkg/apis/duck/interface.go @@ -19,6 +19,8 @@ package duck import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/tools/cache" + "knative.dev/pkg/kmeta" + "knative.dev/pkg/tracker" ) // InformerFactory is used to create Informer/Lister pairs for a schema.GroupVersionResource @@ -26,3 +28,42 @@ type InformerFactory interface { // Get returns a synced Informer/Lister pair for the provided schema.GroupVersionResource. Get(schema.GroupVersionResource) (cache.SharedIndexInformer, cache.GenericLister, error) } + +// OneOfOurs is the union of our Accessor interface and the OwnerRefable interface +// that is implemented by our resources that implement the kmeta.Accessor. +type OneOfOurs interface { + kmeta.Accessor + kmeta.OwnerRefable +} + +// BindableStatus is the interface that the .status of Bindable resources must +// implement to work smoothly with our BaseReconciler. +type BindableStatus interface { + // InitializeConditions seeds the resource's status.conditions field + // with all of the conditions that this Binding surfaces. + InitializeConditions() + + // MarkBindingAvailable notes that this Binding has been properly + // configured. + MarkBindingAvailable() + + // MarkBindingUnavailable notes the provided reason for why the Binding + // has failed. + MarkBindingUnavailable(reason string, message string) + + // SetObservedGeneration updates the .status.observedGeneration to the + // provided generation value. + SetObservedGeneration(int64) +} + +// Bindable may be implemented by Binding resources to use shared libraries. +type Bindable interface { + OneOfOurs + + // GetSubject returns the standard Binding duck's "Subject" field. + GetSubject() tracker.Reference + + // GetBindingStatus returns the status of the Binding, which must + // implement BindableStatus. + GetBindingStatus() BindableStatus +} diff --git a/vendor/knative.dev/pkg/apis/duck/unstructured.go b/vendor/knative.dev/pkg/apis/duck/unstructured.go index 2abf3e77..3a80390d 100644 --- a/vendor/knative.dev/pkg/apis/duck/unstructured.go +++ b/vendor/knative.dev/pkg/apis/duck/unstructured.go @@ -20,16 +20,8 @@ import ( "encoding/json" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "knative.dev/pkg/kmeta" ) -// OneOfOurs is the union of our Accessor interface and the OwnerRefable interface -// that is implemented by our resources that implement the kmeta.Accessor. -type OneOfOurs interface { - kmeta.Accessor - kmeta.OwnerRefable -} - // ToUnstructured takes an instance of a OneOfOurs compatible type and // converts it to unstructured.Unstructured. We take OneOfOurs in place // or runtime.Object because sometimes we get resources that do not have their diff --git a/vendor/knative.dev/pkg/hack/update-codegen.sh b/vendor/knative.dev/pkg/hack/update-codegen.sh index 009a9d2b..3d119e2a 100755 --- a/vendor/knative.dev/pkg/hack/update-codegen.sh +++ b/vendor/knative.dev/pkg/hack/update-codegen.sh @@ -72,7 +72,7 @@ ${CODEGEN_PKG}/generate-groups.sh "deepcopy" \ # Depends on generate-groups.sh to install bin/deepcopy-gen ${GOPATH}/bin/deepcopy-gen --input-dirs \ - knative.dev/pkg/apis,knative.dev/pkg/tracker,knative.dev/pkg/logging,knative.dev/pkg/metrics,knative.dev/pkg/testing \ + knative.dev/pkg/apis,knative.dev/pkg/tracker,knative.dev/pkg/logging,knative.dev/pkg/metrics,knative.dev/pkg/testing,knative.dev/pkg/testing/duck \ -O zz_generated.deepcopy \ --go-header-file ${REPO_ROOT_DIR}/hack/boilerplate/boilerplate.go.txt diff --git a/vendor/knative.dev/pkg/testing/duck/doc.go b/vendor/knative.dev/pkg/testing/duck/doc.go new file mode 100644 index 00000000..bec85972 --- /dev/null +++ b/vendor/knative.dev/pkg/testing/duck/doc.go @@ -0,0 +1,19 @@ +/* +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. +*/ + +// +k8s:deepcopy-gen=package +// +groupName=duck.knative.dev +package duck diff --git a/vendor/knative.dev/pkg/testing/duck/register.go b/vendor/knative.dev/pkg/testing/duck/register.go new file mode 100644 index 00000000..f21a721e --- /dev/null +++ b/vendor/knative.dev/pkg/testing/duck/register.go @@ -0,0 +1,42 @@ +/* +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 duck + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: "duck.knative.dev", Version: "v3"} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes( + SchemeGroupVersion, + &TestBindable{}, + (&TestBindable{}).GetListType(), + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/vendor/knative.dev/pkg/testing/duck/testbindable.go b/vendor/knative.dev/pkg/testing/duck/testbindable.go new file mode 100644 index 00000000..272ea580 --- /dev/null +++ b/vendor/knative.dev/pkg/testing/duck/testbindable.go @@ -0,0 +1,169 @@ +/* +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 duck + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "knative.dev/pkg/apis" + "knative.dev/pkg/apis/duck" + duckv1 "knative.dev/pkg/apis/duck/v1" + duckv1alpha1 "knative.dev/pkg/apis/duck/v1alpha1" + "knative.dev/pkg/tracker" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TestBindable is a simple resource that's compatible with our webhook +type TestBindable struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TestBindableSpec `json:"spec,omitempty"` + Status TestBindableStatus `json:"status,omitempty"` +} + +// Check that TestBindable may be validated and defaulted. +var _ apis.Listable = (*TestBindable)(nil) +var _ duck.Bindable = (*TestBindable)(nil) + +// TestBindableSpec represents test resource spec. +type TestBindableSpec struct { + duckv1alpha1.BindingSpec `json:",inline"` + + Foo string `json:"foo"` +} + +// TestBindableStatus represents the status of our test binding. +type TestBindableStatus struct { + duckv1.Status `json:",inline"` +} + +// GetGroupVersionKind returns the GroupVersionKind. +func (tb *TestBindable) GetGroupVersionKind() schema.GroupVersionKind { + return SchemeGroupVersion.WithKind("TestBindable") +} + +// GetUntypedSpec returns the spec of the resource. +func (tb *TestBindable) GetUntypedSpec() interface{} { + return tb.Spec +} + +// GetListType implements apis.Listable +func (tb *TestBindable) GetListType() runtime.Object { + return &TestBindableList{} +} + +// GetSubject implements psbinding.Bindable +func (tb *TestBindable) GetSubject() tracker.Reference { + return tb.Spec.Subject +} + +// GetBindingStatus implements psbinding.Bindable +func (tb *TestBindable) GetBindingStatus() duck.BindableStatus { + return &tb.Status +} + +// SetObservedGeneration implements psbinding.BindableStatus +func (tbs *TestBindableStatus) SetObservedGeneration(gen int64) { + tbs.ObservedGeneration = gen +} + +// Do implements psbinding.Bindable +func (tb *TestBindable) Do(ctx context.Context, ps *duckv1.WithPod) { + // First undo so that we can just unconditionally append below. + tb.Undo(ctx, ps) + + spec := ps.Spec.Template.Spec + for i := range spec.InitContainers { + spec.InitContainers[i].Env = append(spec.InitContainers[i].Env, corev1.EnvVar{ + Name: "FOO", + Value: tb.Spec.Foo, + }) + } + for i := range spec.Containers { + spec.Containers[i].Env = append(spec.Containers[i].Env, corev1.EnvVar{ + Name: "FOO", + Value: tb.Spec.Foo, + }) + } +} + +func (tb *TestBindable) Undo(ctx context.Context, ps *duckv1.WithPod) { + spec := ps.Spec.Template.Spec + for i, c := range spec.InitContainers { + for j, ev := range c.Env { + if ev.Name == "FOO" { + spec.InitContainers[i].Env = append(spec.InitContainers[i].Env[:j], spec.InitContainers[i].Env[j+1:]...) + break + } + } + } + for i, c := range spec.Containers { + for j, ev := range c.Env { + if ev.Name == "FOO" { + spec.Containers[i].Env = append(spec.Containers[i].Env[:j], spec.Containers[i].Env[j+1:]...) + break + } + } + } +} + +var tbCondSet = apis.NewLivingConditionSet() + +// clearLTT clears the last transition time from our conditions because +// they are hard to ignore in the context of *unstructured.Unstructured, +// which is how psbinding updates statuses. +func (tbs *TestBindableStatus) clearLTT() { + for i := range tbs.Conditions { + tbs.Conditions[i].LastTransitionTime = apis.VolatileTime{} + } +} + +// InitializeConditions populates the TestBindableStatus's conditions field +// with all of its conditions configured to Unknown. +func (tbs *TestBindableStatus) InitializeConditions() { + tbCondSet.Manage(tbs).InitializeConditions() + tbs.clearLTT() +} + +// MarkBindingUnavailable marks the TestBindable's Ready condition to False with +// the provided reason and message. +func (tbs *TestBindableStatus) MarkBindingUnavailable(reason, message string) { + tbCondSet.Manage(tbs).MarkFalse(apis.ConditionReady, reason, message) + tbs.clearLTT() +} + +// MarkBindingAvailable marks the TestBindable's Ready condition to True. +func (tbs *TestBindableStatus) MarkBindingAvailable() { + tbCondSet.Manage(tbs).MarkTrue(apis.ConditionReady) + tbs.clearLTT() +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// TestBindableList is a list of TestBindable resources +type TestBindableList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []TestBindable `json:"items"` +} diff --git a/vendor/knative.dev/pkg/testing/duck/zz_generated.deepcopy.go b/vendor/knative.dev/pkg/testing/duck/zz_generated.deepcopy.go new file mode 100644 index 00000000..49f69373 --- /dev/null +++ b/vendor/knative.dev/pkg/testing/duck/zz_generated.deepcopy.go @@ -0,0 +1,120 @@ +// +build !ignore_autogenerated + +/* +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. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package duck + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestBindable) DeepCopyInto(out *TestBindable) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestBindable. +func (in *TestBindable) DeepCopy() *TestBindable { + if in == nil { + return nil + } + out := new(TestBindable) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestBindable) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestBindableList) DeepCopyInto(out *TestBindableList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestBindable, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestBindableList. +func (in *TestBindableList) DeepCopy() *TestBindableList { + if in == nil { + return nil + } + out := new(TestBindableList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestBindableList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestBindableSpec) DeepCopyInto(out *TestBindableSpec) { + *out = *in + in.BindingSpec.DeepCopyInto(&out.BindingSpec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestBindableSpec. +func (in *TestBindableSpec) DeepCopy() *TestBindableSpec { + if in == nil { + return nil + } + out := new(TestBindableSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestBindableStatus) DeepCopyInto(out *TestBindableStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestBindableStatus. +func (in *TestBindableStatus) DeepCopy() *TestBindableStatus { + if in == nil { + return nil + } + out := new(TestBindableStatus) + in.DeepCopyInto(out) + return out +} diff --git a/vendor/knative.dev/pkg/testutils/clustermanager/e2e-tests/gke.go b/vendor/knative.dev/pkg/testutils/clustermanager/e2e-tests/gke.go index 6adc7cc9..f353fd5b 100644 --- a/vendor/knative.dev/pkg/testutils/clustermanager/e2e-tests/gke.go +++ b/vendor/knative.dev/pkg/testutils/clustermanager/e2e-tests/gke.go @@ -91,14 +91,6 @@ func (gs *GKEClient) Setup(r GKERequest) ClusterOperations { gc.NeedsCleanup = true } - if r.ClusterName == "" { - var err error - r.ClusterName, err = getResourceName(ClusterResource) - if err != nil { - log.Fatalf("Failed getting cluster name: '%v'", err) - } - } - if r.MinNodes == 0 { r.MinNodes = DefaultGKEMinNodes } @@ -192,6 +184,14 @@ func (gc *GKECluster) Acquire() error { request := gc.Request.DeepCopy() // We are going to use request for creating cluster, set its Project request.Project = gc.Project + // Set the cluster name if it doesn't exist + if request.ClusterName == "" { + var err error + request.ClusterName, err = getResourceName(ClusterResource) + if err != nil { + log.Fatalf("Failed getting cluster name: '%v'", err) + } + } // Combine Region with BackupRegions, these will be the regions used for // retrying creation logic diff --git a/vendor/knative.dev/pkg/webhook/psbinding/README.md b/vendor/knative.dev/pkg/webhook/psbinding/README.md new file mode 100644 index 00000000..35a96581 --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/psbinding/README.md @@ -0,0 +1,205 @@ +# "Pod Spec"-able Bindings + +The `psbinding` package provides facilities to make authoring [Bindings](https://docs.google.com/document/d/1t5WVrj2KQZ2u5s0LvIUtfHnSonBv5Vcv8Gl2k5NXrCQ/edit) whose subjects adhere to [`duckv1.PodSpecable`](https://github.com/knative/pkg/blob/master/apis/duck/v1/podspec_types.go#L32) easier. The Bindings doc mentions two key elements of the controller architecture: +1. The standard controller, +1. The mutating webhook (or "admission controller") + +This package provides facilities for bootstrapping both of these elements. To leverage the `psbinding` package, folks should adjust their Binding types to implement `psbinding.Bindable`, which contains a variety of methods that will look familiar to Knative controller authors with two new key methods: `Do` and `Undo` (aka the "mutation" methods). + +The mutation methods on the Binding take in `(context.Context, *duckv1.WithPod)`, and are expected to alter the `*duckv1.WithPod` appropriately to achieve the semantics of the Binding. So for example, if the Binding's runtime contract is the inclusion of a new environment variable `FOO` with some value extracted from the Binding's `spec` then in `Do()` the `duckv1.WithPod` would be altered so that each of the `containers:` contains: + +```yaml +env: +- name: "FOO" + value: "" +``` + +... and `Undo()` would remove these variables. `Do` is invoked for active Bindings, and `Undo` is invoked when they are being deleted, but their subjects remain. + + +We will walk through a simple example Binding whose runtime contract is to mount secrets for talking to Github under `/var/bindings/github`. +[See also](https://github.com/mattmoor/bindings#githubbinding) on which this is based. + +### `Do` and `Undo` + +The `Undo` method itself is simply: remove the named secret volume and any mounts of it: + +```go +func (fb *GithubBinding) Undo(ctx context.Context, ps *duckv1.WithPod) { + spec := ps.Spec.Template.Spec + + // Make sure the PodSpec does NOT have the github volume. + for i, v := range spec.Volumes { + if v.Name == github.VolumeName { + ps.Spec.Template.Spec.Volumes = append(spec.Volumes[:i], spec.Volumes[i+1:]...) + break + } + } + + // Make sure that none of the [init]containers have the github volume mount + for i, c := range spec.InitContainers { + for j, vm := range c.VolumeMounts { + if vm.Name == github.VolumeName { + spec.InitContainers[i].VolumeMounts = append(vm[:j], vm[j+1:]...) + break + } + } + } + for i, c := range spec.Containers { + for j, vm := range c.VolumeMounts { + if vm.Name == github.VolumeName { + spec.Containers[i].VolumeMounts = append(vm[:j], vm[j+1:]...) + break + } + } + } +} +``` + +The `Do` method is the dual of this: ensure that the volume exists, and all containers have it mounted. + +```go +func (fb *GithubBinding) Do(ctx context.Context, ps *duckv1.WithPod) { + + // First undo so that we can just unconditionally append below. + fb.Undo(ctx, ps) + + // Make sure the PodSpec has a Volume like this: + volume := corev1.Volume{ + Name: github.VolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: fb.Spec.Secret.Name, + }, + }, + } + ps.Spec.Template.Spec.Volumes = append(ps.Spec.Template.Spec.Volumes, volume) + + // Make sure that each [init]container in the PodSpec has a VolumeMount like this: + volumeMount := corev1.VolumeMount{ + Name: github.VolumeName, + ReadOnly: true, + MountPath: github.MountPath, + } + spec := ps.Spec.Template.Spec + for i := range spec.InitContainers { + spec.InitContainers[i].VolumeMounts = append(spec.InitContainers[i].VolumeMounts, volumeMount) + } + for i := range spec.Containers { + spec.Containers[i].VolumeMounts = append(spec.Containers[i].VolumeMounts, volumeMount) + } +} +``` + +> Note: if additional context is needed to perform the mutation, then it may be attached-to / extracted-from the supplied `context.Context`. + +### The standard controller + +For simple Bindings (such as our `GithubBinding`), we should be able to implement our `*controller.Impl` by directly leveraging `*psbinding.BaseReconciler` to fully implement reconciliation. + +```go +// NewController returns a new GithubBinding reconciler. +func NewController( + ctx context.Context, + cmw configmap.Watcher, +) *controller.Impl { + logger := logging.FromContext(ctx) + + ghInformer := ghinformer.Get(ctx) + dc := dynamicclient.Get(ctx) + psInformerFactory := podspecable.Get(ctx) + + c := &psbinding.BaseReconciler{ + GVR: v1alpha1.SchemeGroupVersion.WithResource("githubbindings"), + Get: func(namespace string, name string) (psbinding.Bindable, error) { + return ghInformer.Lister().GithubBindings(namespace).Get(name) + }, + DynamicClient: dc, + Recorder: record.NewBroadcaster().NewRecorder( + scheme.Scheme, corev1.EventSource{Component: controllerAgentName}), + } + impl := controller.NewImpl(c, logger, "GithubBindings") + + logger.Info("Setting up event handlers") + + ghInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) + + c.Tracker = tracker.New(impl.EnqueueKey, controller.GetTrackerLease(ctx)) + c.Factory = &duck.CachedInformerFactory{ + Delegate: &duck.EnqueueInformerFactory{ + Delegate: psInformerFactory, + EventHandler: controller.HandleAll(c.Tracker.OnChanged), + }, + } + + // If our `Do` / `Undo` methods need additional context, then we can + // setup a callback to infuse the `context.Context` here: + // c.WithContext = ... + // Note that this can also set up additional informer watch events to + // trigger reconciliation when the infused context changes. + + return impl +} +``` + +> Note: if customized reconciliation logic is needed (e.g. synthesizing additional resources), then the `psbinding.BaseReconciler` may be embedded and a custom `Reconcile()` defined, which can still take advantage of the shared `Finalizer` handling, `Status` manipulation or `Subject`-reconciliation. + +### The mutating webhook + +Setting up the mutating webhook is even simpler: + +```go +func NewWebhook(ctx context.Context, cmw configmap.Watcher) *controller.Impl { + return psbinding.NewAdmissionController(ctx, + // Name of the resource webhook. + "githubbindings.webhook.bindings.mattmoor.dev", + + // The path on which to serve the webhook. + "/githubbindings", + + // How to get all the Bindables for configuring the mutating webhook. + ListAll, + + // How to setup the context prior to invoking Do/Undo. + func(ctx context.Context, b psbinding.Bindable) (context.Context, error) { + return ctx, nil + }, + ) + } +} + +// ListAll enumerates all of the GithubBindings as Bindables so that the webhook +// can reprogram itself as-needed. +func ListAll(ctx context.Context, handler cache.ResourceEventHandler) psbinding.ListAll { + ghInformer := ghinformer.Get(ctx) + + // Whenever a GithubBinding changes our webhook programming might change. + ghInformer.Informer().AddEventHandler(handler) + + return func() ([]psbinding.Bindable, error) { + l, err := ghInformer.Lister().List(labels.Everything()) + if err != nil { + return nil, err + } + bl := make([]psbinding.Bindable, 0, len(l)) + for _, elt := range l { + bl = append(bl, elt) + } + return bl, nil + } +} +``` + +### Putting it together + +With the above defined, then in our webhook's `main.go` we invoke `sharedmain.MainWithContext` passing the additional controller constructors: + +```go + sharedmain.MainWithContext(ctx, "webhook", + // Our other controllers. + // ... + + // For each binding we have our controller and binding webhook. + githubbinding.NewController, githubbinding.NewWebhook, + ) +``` diff --git a/vendor/knative.dev/pkg/webhook/psbinding/controller.go b/vendor/knative.dev/pkg/webhook/psbinding/controller.go new file mode 100644 index 00000000..f6af485c --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/psbinding/controller.go @@ -0,0 +1,121 @@ +/* +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 psbinding + +import ( + "context" + + // Injection stuff + kubeclient "knative.dev/pkg/client/injection/kube/client" + mwhinformer "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1beta1/mutatingwebhookconfiguration" + secretinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/secret" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/cache" + "knative.dev/pkg/apis/duck" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/controller" + "knative.dev/pkg/logging" + "knative.dev/pkg/system" + "knative.dev/pkg/webhook" +) + +// Bindable is implemented by Binding resources whose subjects are PodSpecable +// and that want to leverage this shared logic to simplify binding authorship. +type Bindable interface { + duck.Bindable + + // Do performs this binding's mutation with the specified context on the + // provided PodSpecable. The provided context may be decorated by + // passing a BindableContext to both NewAdmissionController and + // BaseReconciler. + Do(context.Context, *duckv1.WithPod) + + // Undo is the dual of Do, it undoes the binding. + Undo(context.Context, *duckv1.WithPod) +} + +// Mutation is the type of the Do/Undo methods. +type Mutation func(context.Context, *duckv1.WithPod) + +// ListAll is the type of methods for enumerating all of the Bindables on the +// cluster in order to index the covered types to program the admission webhook. +type ListAll func() ([]Bindable, error) + +// GetListAll is a factory method for the ListAll method, which may also be +// supplied with a ResourceEventHandler to register a callback with the Informer +// that sits behind the returned ListAll so that the handler can queue work +// whenever the result of ListAll changes. +type GetListAll func(context.Context, cache.ResourceEventHandler) ListAll + +// BindableContext is the type of context decorator methods that may be supplied +// to NewAdmissionController and BaseReconciler. +type BindableContext func(context.Context, Bindable) (context.Context, error) + +// NewAdmissionController constructs the webhook portion of the pair of +// reconcilers that implement the semantics of our Binding. +func NewAdmissionController( + ctx context.Context, + name, path string, + gla GetListAll, + WithContext BindableContext, +) *controller.Impl { + + // Extract the assorted things from our context. + client := kubeclient.Get(ctx) + mwhInformer := mwhinformer.Get(ctx) + secretInformer := secretinformer.Get(ctx) + options := webhook.GetOptions(ctx) + + // Construct the reconciler for the mutating webhook configuration. + wh := &Reconciler{ + Name: name, + HandlerPath: path, + SecretName: options.SecretName, + + // This is the user-provided context-decorator, which allows + // them to infuse the context passed to Do/Undo. + WithContext: WithContext, + + Client: client, + MWHLister: mwhInformer.Lister(), + SecretLister: secretInformer.Lister(), + } + c := controller.NewImpl(wh, logging.FromContext(ctx), name) + + // It doesn't matter what we enqueue because we will always Reconcile + // the named MWH resource. + handler := controller.HandleAll(c.EnqueueSentinel(types.NamespacedName{})) + + // Reconcile when the named MutatingWebhookConfiguration changes. + mwhInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterWithName(name), + Handler: handler, + }) + + // Reconcile when the cert bundle changes. + secretInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterWithNameAndNamespace(system.Namespace(), wh.SecretName), + Handler: handler, + }) + + // Give the reconciler a way to list all of the Bindable resources, + // and configure the controller to handle changes to those resources. + wh.ListAll = gla(ctx, handler) + + return c +} diff --git a/vendor/knative.dev/pkg/webhook/psbinding/doc.go b/vendor/knative.dev/pkg/webhook/psbinding/doc.go new file mode 100644 index 00000000..70944d0a --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/psbinding/doc.go @@ -0,0 +1,43 @@ +/* +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 requ ired 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 psbinding provides facilities to make authoring Bindings that +// work with "Pod Spec"-able subjects easier. There are two key components +// 1. The AdmissionController, which lives in psbinding.go (controller.go) +// sets it up. +// 2. The BaseReconciler, which lives in reconciler.go and can either be +// used directly as a Reconciler, or it can be wrapped as a base +// implementation for a customized reconciler that wants to take advantage +// of its facilities (e.g. for updating status and manipulating finalizers). +// +// The core concept to consuming psbinding is the Bindable interface. By +// implementing Bindable on your binding resource, you enable the BaseReconciler +// to take over a significant amount of the boilerplate reconciliation (maybe +// all of it). A lot of the interface methods will seem pretty standard to +// Knative folks, but the two key methods to call our are Do and Undo. These +// "mutation" methods carry the business logic for the "Pod Spec"-able binding. +// +// The mutation methods have the signature: +// func(context.Context, *v1alpha1.WithPod) +// These methods are called to have the Binding perform its mutation on the +// supplied WithPod (our "Pod Spec"-able wrapper type). However, in some +// cases the binding may need additional context. Similar to apis.Validatable +// and apis.Defaultable these mutations take a context.Context, and similar +// to our "resourcesemantics" webhook, the "psbinding" package provides a hook +// to allow consumers to infuse this context.Context with additional... context. +// The signature of these hooks is BindableContext, and they may be supplied to +// both the AdmissionController and the BaseReconciler. +package psbinding diff --git a/vendor/knative.dev/pkg/webhook/psbinding/index.go b/vendor/knative.dev/pkg/webhook/psbinding/index.go new file mode 100644 index 00000000..2ad47b10 --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/psbinding/index.go @@ -0,0 +1,81 @@ +/* +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 requ ired 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 psbinding + +import "k8s.io/apimachinery/pkg/labels" + +// exactKey is the type for keys that match exactly. +type exactKey struct { + Group string + Kind string + Namespace string + Name string +} + +// exactMatcher is our reverse index from subjects to the Bindings that apply to +// them. +type exactMatcher map[exactKey]Bindable + +// Add writes a key into the reverse index. +func (em exactMatcher) Add(key exactKey, b Bindable) { + em[key] = b +} + +// Get fetches the key from the reverse index, if present. +func (em exactMatcher) Get(key exactKey) (bindable Bindable, present bool) { + b, ok := em[key] + return b, ok +} + +// inexactKey is the type for keys that match inexactly (via selector) +type inexactKey struct { + Group string + Kind string + Namespace string +} + +// pair holds selectors and bindables for a particular inexactKey. +type pair struct { + selector labels.Selector + sb Bindable +} + +// inexactMatcher is our reverse index from subjects to the Bindings that apply to +// them. +type inexactMatcher map[inexactKey][]pair + +// Add writes a key into the reverse index. +func (im inexactMatcher) Add(key inexactKey, selector labels.Selector, b Bindable) { + pl := im[key] + pl = append(pl, pair{ + selector: selector, + sb: b, + }) + im[key] = pl +} + +// Get fetches the key from the reverse index, if present. +func (im inexactMatcher) Get(key inexactKey, ls labels.Set) (bindable Bindable, present bool) { + // Iterate over the list of pairs matched for this GK + namespace and return the first + // Bindable that matches our selector. + for _, p := range im[key] { + if p.selector.Matches(ls) { + return p.sb, true + } + } + return nil, false +} diff --git a/vendor/knative.dev/pkg/webhook/psbinding/psbinding.go b/vendor/knative.dev/pkg/webhook/psbinding/psbinding.go new file mode 100644 index 00000000..c2270bd9 --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/psbinding/psbinding.go @@ -0,0 +1,327 @@ +/* +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 psbinding + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "sync" + + "github.com/markbates/inflect" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/kubernetes" + admissionlisters "k8s.io/client-go/listers/admissionregistration/v1beta1" + corelisters "k8s.io/client-go/listers/core/v1" + "knative.dev/pkg/apis/duck" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/controller" + "knative.dev/pkg/kmp" + "knative.dev/pkg/logging" + "knative.dev/pkg/ptr" + "knative.dev/pkg/system" + "knative.dev/pkg/webhook" + certresources "knative.dev/pkg/webhook/certificates/resources" +) + +// Reconciler implements an AdmissionController for altering PodSpecable +// resources that are the subject of a particular type of Binding. +// The two key methods are: +// 1. reconcileMutatingWebhook: which enumerates all of the Bindings and +// compiles a list of resource types that should be intercepted by our +// webhook. It also builds an index that can be used to efficiently +// handle Admit requests. +// 2. Admit: which leverages the index built by the Reconciler to apply +// mutations to resources. +type Reconciler struct { + Name string + HandlerPath string + SecretName string + + Client kubernetes.Interface + MWHLister admissionlisters.MutatingWebhookConfigurationLister + SecretLister corelisters.SecretLister + ListAll ListAll + + // WithContext is a callback that infuses the context supplied to + // Do/Undo with additional context to enable them to complete their + // respective tasks. + WithContext BindableContext + + // lock protects access to exact and inexact + lock sync.RWMutex + exact exactMatcher + inexact inexactMatcher +} + +var _ controller.Reconciler = (*Reconciler)(nil) +var _ webhook.AdmissionController = (*Reconciler)(nil) + +// We need to specifically exclude our deployment(s) from consideration, but this provides a way +// of excluding other things as well. +var ( + ExclusionSelector = metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: duck.BindingExcludeLabel, + Operator: metav1.LabelSelectorOpNotIn, + Values: []string{"true"}, + }}, + // TODO(mattmoor): Consider also having a GVR-based one, e.g. + // foobindings.blah.knative.dev/exclude: "true" + } +) + +// Reconcile implements controller.Reconciler +func (ac *Reconciler) Reconcile(ctx context.Context, key string) error { + // Look up the webhook secret, and fetch the CA cert bundle. + secret, err := ac.SecretLister.Secrets(system.Namespace()).Get(ac.SecretName) + if err != nil { + logging.FromContext(ctx).Errorf("Error fetching secret: %v", err) + return err + } + caCert, ok := secret.Data[certresources.CACert] + if !ok { + return fmt.Errorf("secret %q is missing %q key", ac.SecretName, certresources.CACert) + } + + // Reconcile the webhook configuration. + return ac.reconcileMutatingWebhook(ctx, caCert) +} + +// Path implements AdmissionController +func (ac *Reconciler) Path() string { + return ac.HandlerPath +} + +// Admit implements AdmissionController +func (ac *Reconciler) Admit(ctx context.Context, request *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { + switch request.Operation { + case admissionv1beta1.Create, admissionv1beta1.Update: + default: + logging.FromContext(ctx).Infof("Unhandled webhook operation, letting it through %v", request.Operation) + return &admissionv1beta1.AdmissionResponse{Allowed: true} + } + + orig := &duckv1.WithPod{} + decoder := json.NewDecoder(bytes.NewBuffer(request.Object.Raw)) + if err := decoder.Decode(&orig); err != nil { + return webhook.MakeErrorStatus("unable to decode object: %v", err) + } + + // Look up the Bindable for this resource. + fb := func() Bindable { + ac.lock.RLock() + defer ac.lock.RUnlock() + + // Always try to find an exact match first. + if sb, ok := ac.exact.Get(exactKey{ + Group: request.Kind.Group, + Kind: request.Kind.Kind, + Namespace: request.Namespace, + Name: orig.Name, + }); ok { + return sb + } + + // Next look for inexact matches. + if sb, ok := ac.inexact.Get(inexactKey{ + Group: request.Kind.Group, + Kind: request.Kind.Kind, + Namespace: request.Namespace, + }, labels.Set(orig.Labels)); ok { + return sb + } + return nil + }() + if fb == nil { + // This doesn't apply! + return &admissionv1beta1.AdmissionResponse{Allowed: true} + } + + // Callback into the user's code to setup the context with additional + // information needed to perform the mutation. + if ac.WithContext != nil { + var err error + ctx, err = ac.WithContext(ctx, fb) + if err != nil { + return webhook.MakeErrorStatus("unable to setup binding context: %v", err) + } + } + + // Mutate a copy according to the deletion state of the Bindable. + delta := orig.DeepCopy() + if fb.GetDeletionTimestamp() != nil { + fb.Undo(ctx, delta) + } else { + fb.Do(ctx, delta) + } + + // Synthesize a patch from the changes and return it in our AdmissionResponse + patchBytes, err := duck.CreateBytePatch(orig, delta) + if err != nil { + return webhook.MakeErrorStatus("unable to create patch with binding: %v", err) + } + return &admissionv1beta1.AdmissionResponse{ + Patch: patchBytes, + Allowed: true, + PatchType: func() *admissionv1beta1.PatchType { + pt := admissionv1beta1.PatchTypeJSONPatch + return &pt + }(), + } +} + +func (ac *Reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byte) error { + // Build a deduplicated list of all of the GVKs we see. + // We seed the Kubernetes built-ins. + gks := map[schema.GroupKind]sets.String{ + appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind(): sets.NewString("v1"), + appsv1.SchemeGroupVersion.WithKind("StatefulSet").GroupKind(): sets.NewString("v1"), + appsv1.SchemeGroupVersion.WithKind("DaemonSet").GroupKind(): sets.NewString("v1"), + batchv1.SchemeGroupVersion.WithKind("Job").GroupKind(): sets.NewString("v1"), + } + + // When reconciling the webhook, enumerate all of the bindings, so that + // we can index them to efficiently respond to webhook requests. + fbs, err := ac.ListAll() + if err != nil { + return err + } + exact := make(exactMatcher, len(fbs)) + inexact := make(inexactMatcher, len(fbs)) + for _, fb := range fbs { + ref := fb.GetSubject() + gv, err := schema.ParseGroupVersion(ref.APIVersion) + if err != nil { + return err + } + gk := schema.GroupKind{ + Group: gv.Group, + Kind: ref.Kind, + } + set := gks[gk] + if set == nil { + set = sets.NewString() + } + set.Insert(gv.Version) + gks[gk] = set + + if ref.Name != "" { + exact.Add(exactKey{ + Group: gk.Group, + Kind: gk.Kind, + Namespace: ref.Namespace, + Name: ref.Name, + }, fb) + } else { + selector, err := metav1.LabelSelectorAsSelector(ref.Selector) + if err != nil { + return err + } + inexact.Add(inexactKey{ + Group: gk.Group, + Kind: gk.Kind, + Namespace: ref.Namespace, + }, selector, fb) + } + } + + // Update our indices + func() { + ac.lock.Lock() + defer ac.lock.Unlock() + ac.exact = exact + ac.inexact = inexact + }() + + var rules []admissionregistrationv1beta1.RuleWithOperations + for gk, versions := range gks { + plural := strings.ToLower(inflect.Pluralize(gk.Kind)) + + rules = append(rules, admissionregistrationv1beta1.RuleWithOperations{ + Operations: []admissionregistrationv1beta1.OperationType{ + admissionregistrationv1beta1.Create, + admissionregistrationv1beta1.Update, + }, + Rule: admissionregistrationv1beta1.Rule{ + APIGroups: []string{gk.Group}, + APIVersions: versions.List(), + 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] + }) + + configuredWebhook, err := ac.MWHLister.Get(ac.Name) + if err != nil { + return fmt.Errorf("error retrieving webhook: %v", err) + } + webhook := configuredWebhook.DeepCopy() + + // Use the "Equivalent" match policy so that we don't need to enumerate versions for same-types. + // This is only supported by 1.15+ clusters. + matchPolicy := admissionregistrationv1beta1.Equivalent + + for i, wh := range webhook.Webhooks { + if wh.Name != webhook.Name { + continue + } + webhook.Webhooks[i].MatchPolicy = &matchPolicy + webhook.Webhooks[i].Rules = rules + webhook.Webhooks[i].NamespaceSelector = &ExclusionSelector + webhook.Webhooks[i].ObjectSelector = &ExclusionSelector // 1.15+ only + webhook.Webhooks[i].ClientConfig.CABundle = caCert + if webhook.Webhooks[i].ClientConfig.Service == nil { + return fmt.Errorf("missing service reference for webhook: %s", wh.Name) + } + webhook.Webhooks[i].ClientConfig.Service.Path = ptr.String(ac.Path()) + } + + if ok, err := kmp.SafeEqual(configuredWebhook, webhook); err != nil { + return fmt.Errorf("error diffing webhooks: %v", err) + } else if !ok { + logging.FromContext(ctx).Info("Updating webhook") + mwhclient := ac.Client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations() + if _, err := mwhclient.Update(webhook); err != nil { + return fmt.Errorf("failed to update webhook: %v", err) + } + } else { + logging.FromContext(ctx).Info("Webhook is valid") + } + return nil +} diff --git a/vendor/knative.dev/pkg/webhook/psbinding/reconciler.go b/vendor/knative.dev/pkg/webhook/psbinding/reconciler.go new file mode 100644 index 00000000..5569e21b --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/psbinding/reconciler.go @@ -0,0 +1,375 @@ +/* +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 psbinding + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + + "github.com/pkg/errors" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "knative.dev/pkg/apis" + "knative.dev/pkg/apis/duck" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/controller" + "knative.dev/pkg/kmeta" + "knative.dev/pkg/logging" + "knative.dev/pkg/tracker" +) + +// BaseReconciler helps implement controller.Reconciler for Binding resources. +type BaseReconciler struct { + // The GVR of the "primary key" resource for this reconciler. + // This is used along with the DynamicClient for updating the status + // and managing finalizers of the resources being reconciled. + GVR schema.GroupVersionResource + + // Get is a callback that fetches the Bindable with the provided name + // and namespace (for this GVR). + Get func(namespace string, name string) (Bindable, error) + + // WithContext is a callback that infuses the context supplied to + // Do/Undo with additional context to enable them to complete their + // respective tasks. + WithContext BindableContext + + // DynamicClient is used to patch subjects and apply mutations to + // Bindable resources (determined by GVR) to reflect status updates. + DynamicClient dynamic.Interface + + // Factory is used for producing listers for the object references we + // encounter. + Factory duck.InformerFactory + + // The tracker builds an index of what resources are watching other + // resources so that we can immediately react to changes to changes in + // tracked resources. + Tracker tracker.Interface + + // Recorder is an event recorder for recording Event resources to the + // Kubernetes API. + Recorder record.EventRecorder +} + +// Check that our Reconciler implements controller.Reconciler +var _ controller.Reconciler = (*BaseReconciler)(nil) + +// Reconcile implements controller.Reconciler +func (r *BaseReconciler) Reconcile(ctx context.Context, key string) error { + // Convert the namespace/name string into a distinct namespace and name + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + logging.FromContext(ctx).Errorf("invalid resource key: %s", key) + return nil + } + + // Get the resource with this namespace/name. + original, err := r.Get(namespace, name) + if apierrs.IsNotFound(err) { + // The resource may no longer exist, in which case we stop processing. + logging.FromContext(ctx).Errorf("resource %q no longer exists", key) + return nil + } else if err != nil { + return err + } + // Don't modify the informers copy. + resource := original.DeepCopyObject().(Bindable) + + // Reconcile this copy of the resource and then write back any status + // updates regardless of whether the reconciliation errored out. + reconcileErr := r.reconcile(ctx, resource) + if equality.Semantic.DeepEqual(original.GetBindingStatus(), resource.GetBindingStatus()) { + // If we didn't change anything then don't call updateStatus. + // This is important because the copy we loaded from the informer's + // cache may be stale and we don't want to overwrite a prior update + // to status with this stale state. + } else if err = r.UpdateStatus(ctx, resource); err != nil { + logging.FromContext(ctx).Warnw("Failed to update resource status", zap.Error(err)) + r.Recorder.Eventf(resource, corev1.EventTypeWarning, "UpdateFailed", + "Failed to update status for %q: %v", resource.GetName(), err) + return err + } + if reconcileErr != nil { + r.Recorder.Event(resource, corev1.EventTypeWarning, "InternalError", reconcileErr.Error()) + } + return reconcileErr +} + +// reconcile is a reference implementation of a simple Binding flow, it and +// Reconcile may be overridden and the exported helpers used to implement a +// customized reconciliation flow. +func (r *BaseReconciler) reconcile(ctx context.Context, fb Bindable) error { + if fb.GetDeletionTimestamp() != nil { + // Check for a DeletionTimestamp. If present, elide the normal + // reconcile logic and do our finalizer handling. + return r.ReconcileDeletion(ctx, fb) + } + // Make sure that our conditions have been initialized. + fb.GetBindingStatus().InitializeConditions() + + // Make sure that the resource has a Finalizer configured, which + // enables us to undo our binding upon deletion. + if err := r.EnsureFinalizer(ctx, fb); err != nil { + return err + } + + // Perform our Binding's Do() method on the subject(s) of the Binding. + if err := r.ReconcileSubject(ctx, fb, fb.Do); err != nil { + return err + } + + // Update the observed generation once we have successfully reconciled + // our spec. + fb.GetBindingStatus().SetObservedGeneration(fb.GetGeneration()) + return nil +} + +// ReconcileDeletion handles reconcile a resource that is being deleted, which +// amounts to properly finalizing the resource. +func (r *BaseReconciler) ReconcileDeletion(ctx context.Context, fb Bindable) error { + // If we are not the controller finalizing this resource, then we + // are done. + if !r.IsFinalizing(ctx, fb) { + return nil + } + + // If it is our turn to finalize the Binding, then first undo the effect + // of our Binding on the resource. + logging.FromContext(ctx).Infof("Removing the binding for %s", fb.GetName()) + if err := r.ReconcileSubject(ctx, fb, fb.Undo); apierrs.IsNotFound(err) { + // If the subject has been deleted, then there is nothing to undo. + } else if err != nil { + return err + } + + // Once the Binding has been undone, remove our finalizer allowing the + // Binding resource's deletion to progress. + return r.RemoveFinalizer(ctx, fb) +} + +// IsFinalizing determines whether it is our reconciler's turn to finalize a +// resource in the process of being deleted. This means that our finalizer is +// at the head of the metadata.finalizers list. +func (r *BaseReconciler) IsFinalizing(ctx context.Context, fb kmeta.Accessor) bool { + return len(fb.GetFinalizers()) != 0 && fb.GetFinalizers()[0] == r.GVR.GroupResource().String() +} + +// EnsureFinalizer makes sure that the provided resource has a finalizer in the +// form of this BaseReconciler's GVR's stringified GroupResource. +func (r *BaseReconciler) EnsureFinalizer(ctx context.Context, fb kmeta.Accessor) error { + // If it has the finalizer, then we're done. + finalizers := sets.NewString(fb.GetFinalizers()...) + if finalizers.Has(r.GVR.GroupResource().String()) { + return nil + } + + // If it doesn't have our finalizer, then synthesize a patch to add it. + patch, err := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": append(fb.GetFinalizers(), r.GVR.GroupResource().String()), + "resourceVersion": fb.GetResourceVersion(), + }, + }) + if err != nil { + return err + } + + // ... and apply it. + _, err = r.DynamicClient.Resource(r.GVR).Namespace(fb.GetNamespace()).Patch(fb.GetName(), + types.MergePatchType, patch, metav1.PatchOptions{}) + return err +} + +// RemoveFinalizer is the dual of EnsureFinalizer, it removes our finalizer from +// the Binding resource +func (r *BaseReconciler) RemoveFinalizer(ctx context.Context, fb kmeta.Accessor) error { + logging.FromContext(ctx).Info("Removing Finalizer") + + // Synthesize a patch removing our finalizer from the head of the + // finalizer list. + patch, err := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": fb.GetFinalizers()[1:], + "resourceVersion": fb.GetResourceVersion(), + }, + }) + if err != nil { + return err + } + + // ... and apply it. + _, err = r.DynamicClient.Resource(r.GVR).Namespace(fb.GetNamespace()).Patch(fb.GetName(), + types.MergePatchType, patch, metav1.PatchOptions{}) + return err +} + +// ReconcileSubject handles applying the provided Binding "mutation" (Do or +// Undo) to the Binding's subject(s). +func (r *BaseReconciler) ReconcileSubject(ctx context.Context, fb Bindable, mutation Mutation) error { + // Access the subject of our Binding and have the tracker queue this + // Bindable whenever it changes. + subject := fb.GetSubject() + if err := r.Tracker.TrackReference(subject, fb); err != nil { + logging.FromContext(ctx).Errorf("Error tracking subject %v: %v", subject, err) + return err + } + + // Determine the GroupVersionResource of the subject reference + gv, err := schema.ParseGroupVersion(subject.APIVersion) + if err != nil { + logging.FromContext(ctx).Errorf("Error parsing GroupVersion %v: %v", subject.APIVersion, err) + return err + } + gvr := apis.KindToResource(gv.WithKind(subject.Kind)) + + // Use the GVR of the subject(s) to get ahold of a lister that we can + // use to fetch our PodSpecable resources. + _, lister, err := r.Factory.Get(gvr) + if err != nil { + return fmt.Errorf("error getting a lister for resource '%+v': %v", gvr, err) + } + + // Based on the type of subject reference, build up a list of referents. + var referents []*duckv1.WithPod + if subject.Name != "" { + // If name is specified, then fetch it from the lister and turn + // it into a singleton list. + psObj, err := lister.ByNamespace(subject.Namespace).Get(subject.Name) + if apierrs.IsNotFound(err) { + fb.GetBindingStatus().MarkBindingUnavailable("SubjectMissing", err.Error()) + return err + } else if err != nil { + return fmt.Errorf("error fetching Pod Speccable %v: %v", subject, err) + } + referents = append(referents, psObj.(*duckv1.WithPod)) + } else { + // Otherwise, the subject is referenced by selector, so compile + // the selector and pass it to the lister. + selector, err := metav1.LabelSelectorAsSelector(subject.Selector) + if err != nil { + return err + } + psObjs, err := lister.ByNamespace(subject.Namespace).List(selector) + if err != nil { + return fmt.Errorf("error fetching Pod Speccable %v: %v", subject, err) + } + // Type cast the returned resources into our referent list. + for _, psObj := range psObjs { + referents = append(referents, psObj.(*duckv1.WithPod)) + } + } + + // Callback into the user's code to setup the context with additional + // information needed to perform the mutation. + if r.WithContext != nil { + ctx, err = r.WithContext(ctx, fb) + if err != nil { + return err + } + } + + // For each of the referents, apply the mutation. + eg := errgroup.Group{} + for _, ps := range referents { + ps := ps + eg.Go(func() error { + // Do the binding to the pod speccable. + orig := ps.DeepCopy() + mutation(ctx, ps) + + // If nothing changed, then bail early. + if equality.Semantic.DeepEqual(orig, ps) { + return nil + } + + // If we encountered changes, then synthesize and apply + // a patch. + patchBytes, err := duck.CreateBytePatch(orig, ps) + if err != nil { + return err + } + + // TODO(mattmoor): This might fail because a binding changed after + // a Job started or completed, which can be fine. Consider treating + // certain error codes as acceptable. + _, err = r.DynamicClient.Resource(gvr).Namespace(ps.Namespace).Patch( + ps.Name, types.JSONPatchType, patchBytes, metav1.PatchOptions{}) + if err != nil { + return errors.Wrap(err, "failed binding subject "+ps.Name) + } + return nil + }) + } + + // Based on the success of the referent binding, update the Binding's readiness. + if err := eg.Wait(); err != nil { + fb.GetBindingStatus().MarkBindingUnavailable("BindingFailed", err.Error()) + return err + } + fb.GetBindingStatus().MarkBindingAvailable() + return nil +} + +// UpdateStatus updates the status of the resource. Caller is responsible for +// checking for semantic differences before calling. +func (r *BaseReconciler) UpdateStatus(ctx context.Context, desired Bindable) error { + actual, err := r.Get(desired.GetNamespace(), desired.GetName()) + if err != nil { + logging.FromContext(ctx).Errorf("Error fetching actual: %v", err) + return err + } + + // Convert to unstructured for use with the dynamic client. + ua, err := duck.ToUnstructured(actual) + if err != nil { + logging.FromContext(ctx).Errorf("Error converting actual: %v", err) + return err + } + ud, err := duck.ToUnstructured(desired) + if err != nil { + logging.FromContext(ctx).Errorf("Error converting desired: %v", err) + return err + } + + // One last check that status changed. + actualStatus := ua.Object["status"] + desiredStatus := ud.Object["status"] + if reflect.DeepEqual(actualStatus, desiredStatus) { + return nil + } + + // Copy the status over to the refetched resource to avoid updating + // anything other than status. + forUpdate := ua + forUpdate.Object["status"] = desiredStatus + _, err = r.DynamicClient.Resource(r.GVR).Namespace(desired.GetNamespace()).UpdateStatus( + forUpdate, metav1.UpdateOptions{}) + return err +} diff --git a/vendor/knative.dev/pkg/webhook/testing/listers.go b/vendor/knative.dev/pkg/webhook/testing/listers.go index 190fab41..8b15de7c 100644 --- a/vendor/knative.dev/pkg/webhook/testing/listers.go +++ b/vendor/knative.dev/pkg/webhook/testing/listers.go @@ -33,6 +33,7 @@ import ( istiolisters "knative.dev/pkg/client/istio/listers/networking/v1alpha3" "knative.dev/pkg/reconciler/testing" pkgtesting "knative.dev/pkg/testing" + pkgducktesting "knative.dev/pkg/testing/duck" ) var clientSetSchemes = []func(*runtime.Scheme) error{ @@ -40,6 +41,7 @@ var clientSetSchemes = []func(*runtime.Scheme) error{ fakeistioclientset.AddToScheme, autoscalingv2beta1.AddToScheme, pkgtesting.AddToScheme, + pkgducktesting.AddToScheme, } // Listers is used to synthesize informer-style Listers from fixed lists of resources in tests. @@ -95,6 +97,11 @@ func (l *Listers) GetTestObjects() []runtime.Object { return l.sorter.ObjectsForSchemeFunc(pkgtesting.AddToScheme) } +// GetDuckObjects filters the Listers initial list of objects to types defined in knative/pkg +func (l *Listers) GetDuckObjects() []runtime.Object { + return l.sorter.ObjectsForSchemeFunc(pkgducktesting.AddToScheme) +} + // GetHorizontalPodAutoscalerLister gets lister for HorizontalPodAutoscaler resources. func (l *Listers) GetHorizontalPodAutoscalerLister() autoscalingv2beta1listers.HorizontalPodAutoscalerLister { return autoscalingv2beta1listers.NewHorizontalPodAutoscalerLister(l.IndexerFor(&autoscalingv2beta1.HorizontalPodAutoscaler{})) diff --git a/vendor/knative.dev/test-infra/scripts/presubmit-tests.sh b/vendor/knative.dev/test-infra/scripts/presubmit-tests.sh index 0f95529b..bfa16972 100755 --- a/vendor/knative.dev/test-infra/scripts/presubmit-tests.sh +++ b/vendor/knative.dev/test-infra/scripts/presubmit-tests.sh @@ -156,8 +156,10 @@ function default_build_test_runner() { markdown_build_tests || failed=1 # For documentation PRs, just check the md files (( IS_DOCUMENTATION_PR )) && return ${failed} + # Don't merge these two lines, or return code will always be 0. + local go_pkg_dirs + go_pkg_dirs="$(go list ./...)" || return 1 # Skip build test if there is no go code - local go_pkg_dirs="$(go list ./...)" [[ -z "${go_pkg_dirs}" ]] && return ${failed} # Ensure all the code builds subheader "Checking that go code builds"