pkg/webhook/psbinding/table_test.go

2316 lines
61 KiB
Go

/*
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"
"errors"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
jsonpatch "gomodules.xyz/jsonpatch/v2"
fakek8s "k8s.io/client-go/kubernetes/fake"
"knative.dev/pkg/apis"
"knative.dev/pkg/client/injection/ducks/duck/v1/podspecable"
kubeclient "knative.dev/pkg/client/injection/kube/client/fake"
mwhinformer "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/mutatingwebhookconfiguration"
_ "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/mutatingwebhookconfiguration/fake"
dynamicclient "knative.dev/pkg/injection/clients/dynamicclient/fake"
secretinformer "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret"
_ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake"
pkgreconciler "knative.dev/pkg/reconciler"
"knative.dev/pkg/tracker"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
clientgotesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"knative.dev/pkg/apis/duck"
duckv1 "knative.dev/pkg/apis/duck/v1"
duckv1alpha1 "knative.dev/pkg/apis/duck/v1alpha1"
"knative.dev/pkg/configmap"
"knative.dev/pkg/controller"
"knative.dev/pkg/ptr"
"knative.dev/pkg/system"
"knative.dev/pkg/webhook"
certresources "knative.dev/pkg/webhook/certificates/resources"
. "knative.dev/pkg/reconciler/testing"
. "knative.dev/pkg/testing/duck"
. "knative.dev/pkg/webhook/testing"
)
func checkDeploymentIsPatched(t *testing.T, r *TableRow) {
t.Helper()
ac := r.Reconciler.(webhook.AdmissionController)
d := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
Labels: map[string]string{
"foo": "bar",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
}},
},
},
},
}
b, err := json.Marshal(d)
if err != nil {
t.Fatal("Unable to serialize deployment:", err)
}
req := &admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
Kind: metav1.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
Namespace: d.Namespace,
Object: runtime.RawExtension{Raw: b},
}
// It is allowed, and patched to include the environment variable.
resp := ac.Admit(r.Ctx, req)
ExpectAllowed(t, resp)
ExpectPatches(t, resp.Patch, []jsonpatch.JsonPatchOperation{{
Operation: "add",
Path: "/spec/template/spec/containers/0/env",
Value: []interface{}{map[string]interface{}{
"name": "FOO",
"value": "the-value",
}},
}})
}
func checkDeploymentIsPatchedBack(t *testing.T, r *TableRow) {
t.Helper()
ac := r.Reconciler.(webhook.AdmissionController)
d := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
Labels: map[string]string{
"foo": "bar",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "the-value",
}},
}},
},
},
},
}
b, err := json.Marshal(d)
if err != nil {
t.Fatal("Unable to serialize deployment:", err)
}
req := &admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
Kind: metav1.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
Namespace: d.Namespace,
Object: runtime.RawExtension{Raw: b},
}
// It is allowed, and patched to REMOVE the environment variable.
resp := ac.Admit(r.Ctx, req)
ExpectAllowed(t, resp)
ExpectPatches(t, resp.Patch, []jsonpatch.JsonPatchOperation{{
Operation: "remove",
Path: "/spec/template/spec/containers/0/env",
}})
}
func checkDeploymentIsNotPatched(t *testing.T, r *TableRow) {
t.Helper()
ac := r.Reconciler.(webhook.AdmissionController)
d := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "off-it",
Labels: map[string]string{
"foo": "baz",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
}},
},
},
},
}
b, err := json.Marshal(d)
if err != nil {
t.Fatal("Unable to serialize deployment:", err)
}
req := &admissionv1.AdmissionRequest{
Operation: admissionv1.Create,
Kind: metav1.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
Namespace: d.Namespace,
Object: runtime.RawExtension{Raw: b},
}
// It is allowed, but not patched.
resp := ac.Admit(r.Ctx, req)
ExpectAllowed(t, resp)
if want, got := "", string(resp.Patch); want != got {
t.Errorf("Admit() = %s, got %s", got, want)
}
}
func checkDeleteIgnored(t *testing.T, r *TableRow) {
t.Helper()
ac := r.Reconciler.(webhook.AdmissionController)
d := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
Labels: map[string]string{
"foo": "bar",
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
}},
},
},
},
}
b, err := json.Marshal(d)
if err != nil {
t.Fatal("Unable to serialize deployment:", err)
}
req := &admissionv1.AdmissionRequest{
Operation: admissionv1.Delete,
Kind: metav1.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
Namespace: d.Namespace,
Object: runtime.RawExtension{Raw: b},
}
// It is allowed, and patched to include the environment variable.
resp := ac.Admit(r.Ctx, req)
ExpectAllowed(t, resp)
if want, got := "", string(resp.Patch); want != got {
t.Errorf("Admit() = %s, got %s", got, want)
}
}
func TestWebhookReconcile(t *testing.T) {
name, path := "foo.bar.baz", "/blah"
secretName := "webhook-secret"
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: system.Namespace(),
},
Data: map[string][]byte{
certresources.ServerKey: []byte("present"),
certresources.ServerCert: []byte("present"),
certresources.CACert: []byte("present"),
},
}
equivalent := admissionregistrationv1.Equivalent
// The key to use, which for this singleton reconciler doesn't matter (although the
// namespace matters for namespace validation).
key := system.Namespace() + "/does not matter"
table := TableTest{{
Name: "no secret",
Key: key,
WantErr: true,
}, {
Name: "secret missing CA Cert",
Key: key,
Objects: []runtime.Object{&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: system.Namespace(),
},
Data: map[string][]byte{
certresources.ServerKey: []byte("present"),
certresources.ServerCert: []byte("present"),
// certresources.CACert: []byte("missing"),
},
}},
WantErr: true,
}, {
Name: "secret exists, but MWH does not",
Key: key,
Objects: []runtime.Object{secret},
WantErr: true,
}, {
Name: "secret and MWH exist, missing service reference",
Key: key,
Objects: []runtime.Object{secret,
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
}},
},
},
WantErr: true,
}, {
Name: "secret and MWH exist, missing other stuff",
Key: key,
Objects: []runtime.Object{secret,
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
},
},
}},
},
},
WantUpdates: []clientgotesting.UpdateActionImpl{{
Object: &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Path is added.
Path: ptr.String(path),
},
// CABundle is added.
CABundle: []byte("present"),
},
// Rules are added.
Rules: nil,
// MatchPolicy is added.
MatchPolicy: &equivalent,
// Selectors are added.
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
}},
}, {
Name: "secret and MWH exist, added fields are incorrect",
Key: key,
Objects: []runtime.Object{secret,
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Incorrect
Path: ptr.String("incorrect"),
},
// Incorrect
CABundle: []byte("incorrect"),
},
// Incorrect (really just incomplete)
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"pkg.knative.dev"},
APIVersions: []string{"v1alpha1"},
Resources: []string{"innerdefaultresources/*"},
},
}},
// Incorrect
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.NeverReinvocationPolicy),
}},
},
},
WantUpdates: []clientgotesting.UpdateActionImpl{{
Object: &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Path is fixed.
Path: ptr.String(path),
},
// CABundle is fixed.
CABundle: []byte("present"),
},
// Rules are fixed.
Rules: nil,
// MatchPolicy is added.
MatchPolicy: &equivalent,
// Selectors are added.
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
}},
}, {
Name: "failure updating MWH",
Key: key,
WantErr: true,
WithReactors: []clientgotesting.ReactionFunc{
InduceFailure("update", "mutatingwebhookconfigurations"),
},
Objects: []runtime.Object{secret,
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Incorrect
Path: ptr.String("incorrect"),
},
// Incorrect
CABundle: []byte("incorrect"),
},
// Incorrect (really just incomplete)
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"pkg.knative.dev"},
APIVersions: []string{"v1alpha1"},
Resources: []string{"innerdefaultresources/*"},
},
}},
}},
},
},
WantUpdates: []clientgotesting.UpdateActionImpl{{
Object: &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Path is fixed.
Path: ptr.String(path),
},
// CABundle is fixed.
CABundle: []byte("present"),
},
// Rules are fixed.
Rules: nil,
// MatchPolicy is added.
MatchPolicy: &equivalent,
// Selectors are added.
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
}},
}, {
Name: ":fire: everything is fine :fire:",
Key: key,
Objects: []runtime.Object{secret,
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Path is fine.
Path: ptr.String(path),
},
// CABundle is fine.
CABundle: []byte("present"),
},
// Rules are fine.
Rules: nil,
// MatchPolicy is fine.
MatchPolicy: &equivalent,
// Selectors are fine.
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
},
}, {
Name: "a new binding has entered the match",
Key: key,
Objects: []runtime.Object{secret,
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "random.knative.dev/v2beta3",
Kind: "Knoodle",
Namespace: "foo",
Name: "on-it",
},
},
},
},
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Path is fine.
Path: ptr.String(path),
},
// CABundle is fine.
CABundle: []byte("present"),
},
// Rules are fine.
Rules: nil,
// MatchPolicy is fine.
MatchPolicy: &equivalent,
// Selectors are fine.
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
}},
},
},
WantUpdates: []clientgotesting.UpdateActionImpl{{
Object: &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
Path: ptr.String(path),
},
CABundle: []byte("present"),
},
// A new rule is added to intercept the new type.
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"random.knative.dev"},
APIVersions: []string{"v2beta3"},
Resources: []string{"knoodles/*"},
},
}},
MatchPolicy: &equivalent,
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
}},
}, {
Name: "steady state direct bindings",
Key: key,
Objects: []runtime.Object{secret,
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar1",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "random.knative.dev/v2beta3",
Kind: "Knoodle",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "one-value",
},
},
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar2",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "the-value",
},
},
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
Path: ptr.String(path),
},
CABundle: []byte("present"),
},
// A new rule is added to intercept the new type.
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments/*"},
},
}, {
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"random.knative.dev"},
APIVersions: []string{"v2beta3"},
Resources: []string{"knoodles/*"},
},
}},
MatchPolicy: &equivalent,
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
},
// Verify that Admit properly patches deployments after being programmed
// with the binding.
PostConditions: []func(*testing.T, *TableRow){
checkDeploymentIsPatched,
checkDeploymentIsNotPatched,
checkDeleteIgnored,
},
}, {
Name: "steady state selector",
Key: key,
Objects: []runtime.Object{secret,
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar1",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "random.knative.dev/v2beta3",
Kind: "Knoodle",
Namespace: "foo",
Selector: &metav1.LabelSelector{
// Match everything.
MatchLabels: map[string]string{},
},
},
},
Foo: "one-value",
},
},
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar2",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
},
},
},
Foo: "the-value",
},
},
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
Path: ptr.String(path),
},
CABundle: []byte("present"),
},
// A new rule is added to intercept the new type.
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments/*"},
},
}, {
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"random.knative.dev"},
APIVersions: []string{"v2beta3"},
Resources: []string{"knoodles/*"},
},
}},
MatchPolicy: &equivalent,
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
},
// Verify that Admit properly patches deployments after being programmed
// with the binding.
PostConditions: []func(*testing.T, *TableRow){
checkDeploymentIsPatched,
checkDeploymentIsNotPatched,
checkDeleteIgnored,
},
}, {
Name: "tombstoned binding undoes patch",
Key: key,
Objects: []runtime.Object{secret,
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar2",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
},
},
},
Foo: "the-value",
},
},
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
Path: ptr.String(path),
},
CABundle: []byte("present"),
},
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"deployments/*"},
},
}},
MatchPolicy: &equivalent,
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
},
// Verify that Admit properly patches deployments after being programmed
// with the binding.
PostConditions: []func(*testing.T, *TableRow){
checkDeploymentIsPatchedBack,
checkDeploymentIsNotPatched,
checkDeleteIgnored,
},
}, {
Name: "multiple new bindings have entered the match",
Key: key,
Objects: []runtime.Object{secret,
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "random.knative.dev/v2beta3",
Kind: "Knoodle",
Namespace: "foo",
Name: "on-it",
},
},
},
},
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "blah",
Name: "bazinga",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "pseudorandom.knative.dev/v3beta1",
Kind: "Knoogle",
Namespace: "blah",
Name: "oh-yeah",
},
},
},
},
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Path is fine.
Path: ptr.String(path),
},
// CABundle is fine.
CABundle: []byte("present"),
},
// Rules are fine.
Rules: nil,
// MatchPolicy is fine.
MatchPolicy: &equivalent,
// Selectors are fine.
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
}},
},
},
WantUpdates: []clientgotesting.UpdateActionImpl{{
Object: &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
Path: ptr.String(path),
},
CABundle: []byte("present"),
},
// New rules are added to intercept the new types.
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"pseudorandom.knative.dev"},
APIVersions: []string{"v3beta1"},
Resources: []string{"knoogles/*"},
},
}, {
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"random.knative.dev"},
APIVersions: []string{"v2beta3"},
Resources: []string{"knoodles/*"},
},
}},
MatchPolicy: &equivalent,
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
}},
}, {
Name: "a new selector-based binding has entered the match",
Key: key,
Objects: []runtime.Object{secret,
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "random.knative.dev/v2beta3",
Kind: "Knoodle",
Namespace: "foo",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
},
},
},
},
},
&admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
// Path is fine.
Path: ptr.String(path),
},
// CABundle is fine.
CABundle: []byte("present"),
},
// Rules are fine.
Rules: nil,
// MatchPolicy is fine.
MatchPolicy: &equivalent,
// Selectors are fine.
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
},
WantUpdates: []clientgotesting.UpdateActionImpl{{
Object: &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: name,
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: system.Namespace(),
Name: "webhook",
Path: ptr.String(path),
},
CABundle: []byte("present"),
},
// A new rule is added to intercept the new type.
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{"CREATE", "UPDATE"},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"random.knative.dev"},
APIVersions: []string{"v2beta3"},
Resources: []string{"knoodles/*"},
},
}},
MatchPolicy: &equivalent,
NamespaceSelector: &ExclusionSelector,
ObjectSelector: &ExclusionSelector,
ReinvocationPolicy: ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy),
}},
},
}},
}}
table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler {
r := NewReconciler(name, path, secretName, kubeclient.Get(ctx), listers.GetMutatingWebhookConfigurationLister(), listers.GetSecretLister(), nil)
r.ListAll = func() ([]Bindable, error) {
bl := make([]Bindable, 0)
for _, elt := range listers.GetDuckObjects() {
b, ok := elt.(Bindable)
if !ok {
continue
}
bl = append(bl, b)
}
return bl, nil
}
return r
}))
}
func TestNew(t *testing.T) {
ctx, _ := SetupFakeContext(t)
ctx = webhook.WithOptions(ctx, webhook.Options{})
c := NewAdmissionController(ctx, "foo", "/bar",
func(context.Context, cache.ResourceEventHandler) ListAll {
return func() ([]Bindable, error) {
return nil, nil
}
},
func(ctx context.Context, b Bindable) (context.Context, error) {
return ctx, nil
})
if c == nil {
t.Fatal("Expected NewController to return a non-nil value")
}
if want, got := 0, c.WorkQueue().Len(); want != got {
t.Errorf("WorkQueue.Len() = %d, wanted %d", got, want)
}
la, ok := c.Reconciler.(pkgreconciler.LeaderAware)
if !ok {
t.Fatalf("%T is not leader aware", c.Reconciler)
}
if err := la.Promote(pkgreconciler.UniversalBucket(), c.MaybeEnqueueBucketKey); err != nil {
t.Error("Promote() =", err)
}
// Queue has async moving parts so if we check at the wrong moment, this might still be 0.
if wait.PollUntilContextTimeout(ctx, 10*time.Millisecond, 250*time.Millisecond, true, func(ctx context.Context) (bool, error) {
return c.WorkQueue().Len() == 1, nil
}) != nil {
t.Error("Queue length was never 1")
}
}
func TestNewReconciler(t *testing.T) {
ctx, _ := SetupFakeContext(t)
ctx = webhook.WithOptions(ctx, webhook.Options{})
tests := []struct {
name string
selectorOption ReconcilerOption
wantSelector metav1.LabelSelector
}{
{
name: "no selector, use default",
wantSelector: ExclusionSelector,
},
{
name: "ExclusionSelector option",
selectorOption: WithSelector(ExclusionSelector),
wantSelector: ExclusionSelector,
},
{
name: "InclusionSelector option",
selectorOption: WithSelector(InclusionSelector),
wantSelector: InclusionSelector,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := kubeclient.Get(ctx)
mwhInformer := mwhinformer.Get(ctx)
secretInformer := secretinformer.Get(ctx)
withContext := func(ctx context.Context, b Bindable) (context.Context, error) {
return ctx, nil
}
var r *Reconciler
if tt.selectorOption == nil {
r = NewReconciler("foo", "/bar", "foosec", client, mwhInformer.Lister(), secretInformer.Lister(), withContext)
} else {
r = NewReconciler("foo", "/bar", "foosec", client, mwhInformer.Lister(), secretInformer.Lister(), withContext, tt.selectorOption)
}
if diff := cmp.Diff(r.selector, tt.wantSelector); diff != "" {
t.Errorf("Wrong selector configured. Got: %+v, want: %+v, diff: %v", r.selector, tt.wantSelector, diff)
}
})
}
}
func TestBaseReconcile(t *testing.T) {
table := TableTest{{
Name: "bad key",
Key: "this/is/a/bad/key",
}, {
Name: "not found",
Key: "its/missing",
}, {
Name: "add finalizer, add env var",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
Object: mustTU(t, &TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
}),
}},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddFinalizer("foo", "bar", "" /* resource version */),
patchAddLabel("foo"),
patchAddEnv("foo", "on-it", "asdfasdfasdfasdf"),
},
}, {
Name: "failure adding finalizer",
Key: "foo/bar",
WantErr: true,
WithReactors: []clientgotesting.ReactionFunc{
InduceFailure("patch", "testbindables"),
},
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddFinalizer("foo", "bar", "" /* resource version */),
},
}, {
Name: "failure patching deployment",
Key: "foo/bar",
WantErr: true,
WithReactors: []clientgotesting.ReactionFunc{
InduceFailure("patch", "deployments"),
},
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Labels: map[string]string{duck.BindingIncludeLabel: "true"},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
}},
},
},
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddEnv("foo", "on-it", "asdfasdfasdfasdf"),
},
WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
Object: mustTU(t, &TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "False",
Reason: "BindingFailed",
Message: "failed binding subject on-it: inducing failure for patch deployments",
}},
},
},
}),
}},
}, {
Name: "steady state",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "asdfasdfasdfasdf",
}},
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
},
}, {
Name: "finalizing, but not our turn.",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{
"slow.your.role",
"testbindables.duck.knative.dev",
},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "new value",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
},
}, {
Name: "finalizing, missing subject (remove the finalizer).",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{
"testbindables.duck.knative.dev",
},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "new value",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "False",
Reason: "SubjectMissing",
Message: `deployments.apps "on-it" not found`,
}},
},
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchRemoveFinalizer("foo", "bar", "" /* resource version */),
},
}, {
Name: "finalizing forbidden subject",
Key: "foo/bar",
WithReactors: []clientgotesting.ReactionFunc{
// This will cause the duck informer factory to return a Forbidden error on Get(gvr)
// The informer calls list to ensure the type exists - this will
func(a clientgotesting.Action) (handled bool, ret runtime.Object, err error) {
if a.Matches("list", "deployments") {
return true, nil, apierrs.NewForbidden(schema.GroupResource{}, "", errors.New("some-error"))
}
return false, nil, nil
},
},
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "new value",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchRemoveFinalizer("foo", "bar", "" /* resource version */),
},
WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
Object: mustTU(t, &TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "new value",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "False",
Reason: "SubjectUnavailable",
// prefix comes from apierrs.NewForbidden
Message: "forbidden: some-error",
}},
},
},
}),
}},
}, {
Name: "finalizing (unbind, and remove the finalizer)",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{
"testbindables.duck.knative.dev",
},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "value",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "value",
}},
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
patchRemoveEnv("foo", "on-it"),
patchRemoveFinalizer("foo", "bar", "" /* resource version */),
},
}, {
Name: "add finalizer, add env var (via selector)",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{},
},
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddFinalizer("foo", "bar", "" /* resource version */),
patchAddLabel("foo"),
patchAddEnv("foo", "on-it", "asdfasdfasdfasdf"),
},
}, {
Name: "steady state (via selector)",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{},
},
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "asdfasdfasdfasdf",
}},
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
},
}, {
Name: "finalizing, missing subject (remove the finalizer, via selector)",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{
"testbindables.duck.knative.dev",
},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{},
},
},
},
Foo: "new value",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
patchRemoveFinalizer("foo", "bar", "" /* resource version */),
},
}, {
Name: "finalizing (unbind, and remove the finalizer, via selector)",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{
"testbindables.duck.knative.dev",
},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{},
},
},
},
Foo: "value",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "value",
}},
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
patchRemoveEnv("foo", "on-it"),
patchRemoveFinalizer("foo", "bar", "" /* resource version */),
},
}, {
Name: "failure updating status",
Key: "foo/bar",
WantErr: true,
WithReactors: []clientgotesting.ReactionFunc{
InduceFailure("update", "testbindables"),
},
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "asdfasdfasdfasdf",
}},
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
},
WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
Object: mustTU(t, &TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
}),
}},
}, {
Name: "finalizing (error during unbind)",
Key: "foo/bar",
WantErr: true,
WithReactors: []clientgotesting.ReactionFunc{
InduceFailure("patch", "deployments"),
},
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Finalizers: []string{
"testbindables.duck.knative.dev",
},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{},
},
},
},
Foo: "value",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "False",
Reason: "BindingFailed",
Message: "failed binding subject on-it: inducing failure for patch deployments",
}},
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "value",
}},
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
patchRemoveEnv("foo", "on-it"),
},
}}
table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler {
gvr := SchemeGroupVersion.WithResource("testbindables")
ctx = podspecable.WithDuck(ctx)
dc := dynamicclient.Get(ctx)
return &BaseReconciler{
GVR: gvr,
DynamicClient: dc,
Factory: podspecable.Get(ctx),
Tracker: &FakeTracker{},
Recorder: record.NewFakeRecorder(20),
Get: func(namespace, name string) (Bindable, error) {
for _, elt := range listers.GetDuckObjects() {
b, ok := elt.(*TestBindable)
if !ok {
continue
}
if b.Namespace != namespace || b.Name != name {
continue
}
return b, nil
}
return nil, apierrs.NewNotFound(gvr.GroupResource(), name)
},
NamespaceLister: listers.GetNamespaceLister(),
}
}))
}
func TestBaseReconcileWithSubResourcesReconciler(t *testing.T) {
table := TableTest{{
Name: "create new subresource",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "bar",
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "asdfasdfasdfasdf",
}},
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
},
WantCreates: []runtime.Object{
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
},
},
},
},
{
Name: "delete resource and subresource",
Key: "foo/bar",
Objects: []runtime.Object{
&TestBindable{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
DeletionTimestamp: &metav1.Time{Time: time.Now()},
Name: "bar",
Finalizers: []string{"testbindables.duck.knative.dev"},
},
Spec: TestBindableSpec{
BindingSpec: duckv1alpha1.BindingSpec{
Subject: tracker.Reference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "foo",
Name: "on-it",
},
},
Foo: "asdfasdfasdfasdf",
},
Status: TestBindableStatus{
Status: duckv1.Status{
Conditions: []apis.Condition{{
Type: "Ready",
Status: "True",
}},
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "foo",
Name: "on-it",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "foo",
Image: "busybox",
Env: []corev1.EnvVar{{
Name: "FOO",
Value: "asdfasdfasdfasdf",
}},
}},
},
},
},
},
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
},
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: "foo",
},
},
},
WantPatches: []clientgotesting.PatchActionImpl{
patchAddLabel("foo"),
patchRemoveEnv("foo", "on-it"),
patchRemoveFinalizer("foo", "bar", "" /* resource version */),
},
WantDeletes: []clientgotesting.DeleteActionImpl{
deletedConfigmap("foo", "bar"),
},
},
}
table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler {
gvr := SchemeGroupVersion.WithResource("testbindables")
ctx = podspecable.WithDuck(ctx)
dc := dynamicclient.Get(ctx)
kc := kubeclient.Get(ctx)
srr := &fakeSubResourcesReconciler{
client: kc,
}
return &BaseReconciler{
GVR: gvr,
DynamicClient: dc,
Factory: podspecable.Get(ctx),
Tracker: &FakeTracker{},
Recorder: record.NewFakeRecorder(20),
Get: func(namespace, name string) (Bindable, error) {
for _, elt := range listers.GetDuckObjects() {
b, ok := elt.(*TestBindable)
if !ok {
continue
}
if b.Namespace != namespace || b.Name != name {
continue
}
return b, nil
}
return nil, apierrs.NewNotFound(gvr.GroupResource(), name)
},
NamespaceLister: listers.GetNamespaceLister(),
SubResourcesReconciler: srr,
}
}))
}
func mustTU(t *testing.T, ro duck.OneOfOurs) *unstructured.Unstructured {
u, err := duck.ToUnstructured(ro)
if err != nil {
t.Fatalf("ToUnstructured(%+v) = %v", ro, err)
}
return u
}
func patchAddLabel(namespace string) clientgotesting.PatchActionImpl {
action := clientgotesting.PatchActionImpl{}
resource := schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "namespaces",
}
actionImpl := clientgotesting.ActionImpl{
Namespace: namespace,
Verb: "patch",
Resource: resource,
Subresource: "",
}
action.ActionImpl = actionImpl
action.Name = namespace
action.PatchType = types.MergePatchType
patch, _ := json.Marshal(jsonLabelPatch)
action.Patch = patch
return action
}
func patchAddFinalizer(namespace, name, resourceVersion string) clientgotesting.PatchActionImpl {
action := clientgotesting.PatchActionImpl{}
action.Name = name
action.Namespace = namespace
patch := fmt.Sprintf(`{"metadata":{"finalizers":["testbindables.duck.knative.dev"],"resourceVersion":%q}}`, resourceVersion)
action.Patch = []byte(patch)
return action
}
func patchRemoveFinalizer(namespace, name, resourceVersion string) clientgotesting.PatchActionImpl {
action := clientgotesting.PatchActionImpl{}
action.Name = name
action.Namespace = namespace
patch := fmt.Sprintf(`{"metadata":{"finalizers":[],"resourceVersion":%q}}`, resourceVersion)
action.Patch = []byte(patch)
return action
}
func patchAddEnv(namespace, name, value string) clientgotesting.PatchActionImpl {
action := clientgotesting.PatchActionImpl{}
action.Name = name
action.Namespace = namespace
patch := fmt.Sprintf(`[{"op":"add","path":"/spec/template/spec/containers/0/env","value":[{"name":"FOO","value":%q}]}]`, value)
action.Patch = []byte(patch)
return action
}
func patchRemoveEnv(namespace, name string) clientgotesting.PatchActionImpl {
action := clientgotesting.PatchActionImpl{}
action.Name = name
action.Namespace = namespace
patch := `[{"op":"remove","path":"/spec/template/spec/containers/0/env"}]`
action.Patch = []byte(patch)
return action
}
func deletedConfigmap(namespace string, name string) clientgotesting.DeleteActionImpl {
action := clientgotesting.DeleteActionImpl{}
resource := schema.GroupVersionResource{
Group: "core",
Version: "v1",
Resource: "configmaps",
}
actionImpl := clientgotesting.ActionImpl{
Namespace: namespace,
Verb: "delete",
Resource: resource,
Subresource: "",
}
action.ActionImpl = actionImpl
action.Name = name
return action
}
type fakeSubResourcesReconciler struct {
client *fakek8s.Clientset
}
func (d *fakeSubResourcesReconciler) Reconcile(ctx context.Context, fb Bindable) error {
cfg := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: fb.GetName(),
},
}
_, err := d.client.CoreV1().ConfigMaps(fb.GetNamespace()).Create(ctx, &cfg, metav1.CreateOptions{})
return err
}
func (d *fakeSubResourcesReconciler) ReconcileDeletion(ctx context.Context, fb Bindable) error {
return d.client.CoreV1().ConfigMaps(fb.GetNamespace()).Delete(ctx, fb.GetName(), metav1.DeleteOptions{})
}