From 8953ad39666f0bc1c7e7edb5629263d00b77c350 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Sat, 30 Jun 2018 14:23:06 +0000 Subject: [PATCH] Revision controller table test skeleton. This sets up the skeleton for writing table tests for the Revision controller, and adds the first non-trivial test of a basic reconciliation. Related: https://github.com/knative/serving/issues/1219 --- pkg/controller/revision/queueing_test.go | 186 ++++++++++- pkg/controller/revision/revision.go | 13 +- pkg/controller/revision/revision_test.go | 173 +--------- pkg/controller/revision/table_test.go | 395 +++++++++++++++++++++++ pkg/controller/testing/listers.go | 168 ++++++++++ 5 files changed, 753 insertions(+), 182 deletions(-) create mode 100644 pkg/controller/revision/table_test.go diff --git a/pkg/controller/revision/queueing_test.go b/pkg/controller/revision/queueing_test.go index 937e551cb13a..f4fa21c9dc0d 100644 --- a/pkg/controller/revision/queueing_test.go +++ b/pkg/controller/revision/queueing_test.go @@ -20,19 +20,191 @@ import ( "testing" "time" + fakebuildclientset "github.com/knative/build/pkg/client/clientset/versioned/fake" + buildinformers "github.com/knative/build/pkg/client/informers/externalversions" + "github.com/knative/serving/pkg" + "github.com/knative/serving/pkg/apis/serving" + "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "github.com/knative/serving/pkg/autoscaler" + fakeclientset "github.com/knative/serving/pkg/client/clientset/versioned/fake" + informers "github.com/knative/serving/pkg/client/informers/externalversions" + "github.com/knative/serving/pkg/configmap" + ctrl "github.com/knative/serving/pkg/controller" + "github.com/knative/serving/pkg/logging" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + fakevpaclientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned/fake" + vpainformers "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/informers/externalversions" + kubeinformers "k8s.io/client-go/informers" + fakekubeclientset "k8s.io/client-go/kubernetes/fake" . "github.com/knative/serving/pkg/controller/testing" + . "github.com/knative/serving/pkg/logging/testing" ) -/* TODO tests: -- syncHandler returns error (in processNextWorkItem) -- invalid key in workqueue (in processNextWorkItem) -- object cannot be converted to key (in enqueueConfiguration) -- invalid key given to syncHandler -- resource doesn't exist in lister (from syncHandler) -*/ +type nopResolver struct{} + +func (r *nopResolver) Resolve(_ *appsv1.Deployment) error { + return nil +} + +const ( + testAutoscalerImage = "autoscalerImage" + testFluentdImage = "fluentdImage" + testFluentdSidecarOutputConfig = ` + + @type elasticsearch + +` + testNamespace = "test" + testQueueImage = "queueImage" +) + +func getTestRevision() *v1alpha1.Revision { + return &v1alpha1.Revision{ + ObjectMeta: metav1.ObjectMeta{ + SelfLink: "/apis/serving/v1alpha1/namespaces/test/revisions/test-rev", + Name: "test-rev", + Namespace: testNamespace, + Labels: map[string]string{ + "testLabel1": "foo", + "testLabel2": "bar", + serving.RouteLabelKey: "test-route", + }, + Annotations: map[string]string{ + "testAnnotation": "test", + }, + UID: "test-rev-uid", + }, + Spec: v1alpha1.RevisionSpec{ + // corev1.Container has a lot of setting. We try to pass many + // of them here to verify that we pass through the settings to + // derived objects. + Container: corev1.Container{ + Image: "gcr.io/repo/image", + Command: []string{"echo"}, + Args: []string{"hello", "world"}, + WorkingDir: "/tmp", + Env: []corev1.EnvVar{{ + Name: "EDITOR", + Value: "emacs", + }}, + LivenessProbe: &corev1.Probe{ + TimeoutSeconds: 42, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "health", + }, + }, + TimeoutSeconds: 43, + }, + TerminationMessagePath: "/dev/null", + }, + ServingState: v1alpha1.RevisionServingStateActive, + ConcurrencyModel: v1alpha1.RevisionRequestConcurrencyModelMulti, + }, + } +} + +func getTestControllerConfig() *ControllerConfig { + return &ControllerConfig{ + QueueSidecarImage: testQueueImage, + AutoscalerImage: testAutoscalerImage, + } +} + +func newTestController(t *testing.T, servingObjects ...runtime.Object) ( + kubeClient *fakekubeclientset.Clientset, + buildClient *fakebuildclientset.Clientset, + servingClient *fakeclientset.Clientset, + vpaClient *fakevpaclientset.Clientset, + controller *Controller, + kubeInformer kubeinformers.SharedInformerFactory, + buildInformer buildinformers.SharedInformerFactory, + servingInformer informers.SharedInformerFactory, + configMapWatcher configmap.Watcher, + vpaInformer vpainformers.SharedInformerFactory) { + + controllerConfig := getTestControllerConfig() + + // Create fake clients + kubeClient = fakekubeclientset.NewSimpleClientset() + buildClient = fakebuildclientset.NewSimpleClientset() + servingClient = fakeclientset.NewSimpleClientset(servingObjects...) + vpaClient = fakevpaclientset.NewSimpleClientset() + + configMapWatcher = configmap.NewFixedWatcher(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkg.GetServingSystemNamespace(), + Name: ctrl.GetNetworkConfigMapName(), + }, + }, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkg.GetServingSystemNamespace(), + Name: logging.ConfigName, + }, + Data: map[string]string{ + "zap-logger-config": "{\"level\": \"error\",\n\"outputPaths\": [\"stdout\"],\n\"errorOutputPaths\": [\"stderr\"],\n\"encoding\": \"json\"}", + "loglevel.queueproxy": "info", + }, + }, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkg.GetServingSystemNamespace(), + Name: ctrl.GetObservabilityConfigMapName(), + }, + Data: map[string]string{ + "logging.enable-var-log-collection": "true", + "logging.fluentd-sidecar-image": testFluentdImage, + "logging.fluentd-sidecar-output-config": testFluentdSidecarOutputConfig, + }, + }, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: pkg.GetServingSystemNamespace(), + Name: autoscaler.ConfigName, + }, + Data: map[string]string{ + "max-scale-up-rate": "1.0", + "single-concurrency-target": "1.0", + "multi-concurrency-target": "1.0", + "stable-window": "5m", + "panic-window": "10s", + "scale-to-zero-threshold": "10m", + "concurrency-quantum-of-time": "100ms", + }, + }) + + // Create informer factories with fake clients. The second parameter sets the + // resync period to zero, disabling it. + kubeInformer = kubeinformers.NewSharedInformerFactory(kubeClient, 0) + buildInformer = buildinformers.NewSharedInformerFactory(buildClient, 0) + servingInformer = informers.NewSharedInformerFactory(servingClient, 0) + vpaInformer = vpainformers.NewSharedInformerFactory(vpaClient, 0) + + controller = NewController( + ctrl.Options{ + KubeClientSet: kubeClient, + ServingClientSet: servingClient, + ConfigMapWatcher: configMapWatcher, + Logger: TestLogger(t), + }, + vpaClient, + servingInformer.Serving().V1alpha1().Revisions(), + buildInformer.Build().V1alpha1().Builds(), + kubeInformer.Apps().V1().Deployments(), + kubeInformer.Core().V1().Services(), + kubeInformer.Core().V1().Endpoints(), + vpaInformer.Poc().V1alpha1().VerticalPodAutoscalers(), + controllerConfig, + ) + + controller.resolver = &nopResolver{} + + return +} func TestNewRevisionCallsSyncHandler(t *testing.T) { rev := getTestRevision() diff --git a/pkg/controller/revision/revision.go b/pkg/controller/revision/revision.go index a1f3512caee9..3ba9411274b2 100644 --- a/pkg/controller/revision/revision.go +++ b/pkg/controller/revision/revision.go @@ -25,10 +25,6 @@ import ( "sync" "time" - // TODO(mattmoor): Used by the commented checkAndUpdateDeployment logic below. - // "github.com/google/go-cmp/cmp/cmpopts" - // "k8s.io/apimachinery/pkg/api/resource" - "github.com/knative/serving/pkg" "github.com/google/go-cmp/cmp" @@ -460,8 +456,8 @@ func (c *Controller) createDeployment(ctx context.Context, rev *v1alpha1.Revisio if rev.Spec.ServingState == v1alpha1.RevisionServingStateReserve { replicaCount = 0 } - deployment := MakeServingDeployment(rev, c.getLoggingConfig(), c.getNetworkConfig(), c.getObservabilityConfig(), - c.getAutoscalerConfig(), c.controllerConfig, replicaCount) + deployment := MakeServingDeployment(rev, c.getLoggingConfig(), c.getNetworkConfig(), + c.getObservabilityConfig(), c.getAutoscalerConfig(), c.controllerConfig, replicaCount) // Resolve tag image references to digests. if err := c.resolver.Resolve(deployment); err != nil { @@ -921,8 +917,7 @@ func (c *Controller) deleteVPA(ctx context.Context, vpa *vpav1alpha1.VerticalPod } func (c *Controller) updateStatus(rev *v1alpha1.Revision) (*v1alpha1.Revision, error) { - prClient := c.ServingClientSet.ServingV1alpha1().Revisions(rev.Namespace) - newRev, err := prClient.Get(rev.Name, metav1.GetOptions{}) + newRev, err := c.revisionLister.Revisions(rev.Namespace).Get(rev.Name) if err != nil { return nil, err } @@ -931,7 +926,7 @@ func (c *Controller) updateStatus(rev *v1alpha1.Revision) (*v1alpha1.Revision, e newRev.Status = rev.Status // TODO: for CRD there's no updatestatus, so use normal update - return prClient.Update(newRev) + return c.ServingClientSet.ServingV1alpha1().Revisions(rev.Namespace).Update(newRev) // return prClient.UpdateStatus(newRev) } return rev, nil diff --git a/pkg/controller/revision/revision_test.go b/pkg/controller/revision/revision_test.go index 20060388abc2..3f7e8b4548b0 100644 --- a/pkg/controller/revision/revision_test.go +++ b/pkg/controller/revision/revision_test.go @@ -63,65 +63,6 @@ import ( . "github.com/knative/serving/pkg/controller/testing" ) -const ( - testAutoscalerImage = "autoscalerImage" - testFluentdImage = "fluentdImage" - testFluentdSidecarOutputConfig = ` - - @type elasticsearch - -` - testNamespace = "test" - testQueueImage = "queueImage" -) - -func getTestRevision() *v1alpha1.Revision { - return &v1alpha1.Revision{ - ObjectMeta: metav1.ObjectMeta{ - SelfLink: "/apis/serving/v1alpha1/namespaces/test/revisions/test-rev", - Name: "test-rev", - Namespace: testNamespace, - Labels: map[string]string{ - "testLabel1": "foo", - "testLabel2": "bar", - }, - Annotations: map[string]string{ - "testAnnotation": "test", - }, - UID: "test-rev-uid", - }, - Spec: v1alpha1.RevisionSpec{ - // corev1.Container has a lot of setting. We try to pass many - // of them here to verify that we pass through the settings to - // derived objects. - Container: corev1.Container{ - Image: "gcr.io/repo/image", - Command: []string{"echo"}, - Args: []string{"hello", "world"}, - WorkingDir: "/tmp", - Env: []corev1.EnvVar{{ - Name: "EDITOR", - Value: "emacs", - }}, - LivenessProbe: &corev1.Probe{ - TimeoutSeconds: 42, - }, - ReadinessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "health", - }, - }, - TimeoutSeconds: 43, - }, - TerminationMessagePath: "/dev/null", - }, - ServingState: v1alpha1.RevisionServingStateActive, - ConcurrencyModel: v1alpha1.RevisionRequestConcurrencyModelMulti, - }, - } -} - func getTestConfiguration() *v1alpha1.Configuration { return &v1alpha1.Configuration{ ObjectMeta: metav1.ObjectMeta{ @@ -192,19 +133,6 @@ func sumMaps(a map[string]string, b map[string]string) map[string]string { return summedMap } -func getTestControllerConfig() ControllerConfig { - return ControllerConfig{ - QueueSidecarImage: testQueueImage, - AutoscalerImage: testAutoscalerImage, - } -} - -type nopResolver struct{} - -func (r *nopResolver) Resolve(_ *appsv1.Deployment) error { - return nil -} - func newTestControllerWithConfig(t *testing.T, controllerConfig *ControllerConfig, configs ...*corev1.ConfigMap) ( kubeClient *fakekubeclientset.Clientset, buildClient *fakebuildclientset.Clientset, @@ -298,93 +226,6 @@ func newTestControllerWithConfig(t *testing.T, controllerConfig *ControllerConfi return } -func newTestController(t *testing.T, servingObjects ...runtime.Object) ( - kubeClient *fakekubeclientset.Clientset, - buildClient *fakebuildclientset.Clientset, - servingClient *fakeclientset.Clientset, - vpaClient *fakevpaclientset.Clientset, - controller *Controller, - kubeInformer kubeinformers.SharedInformerFactory, - buildInformer buildinformers.SharedInformerFactory, - servingInformer informers.SharedInformerFactory, - configMapWatcher configmap.Watcher, - vpaInformer vpainformers.SharedInformerFactory) { - testControllerConfig := getTestControllerConfig() - - // Create fake clients - kubeClient = fakekubeclientset.NewSimpleClientset() - buildClient = fakebuildclientset.NewSimpleClientset() - servingClient = fakeclientset.NewSimpleClientset(servingObjects...) - vpaClient = fakevpaclientset.NewSimpleClientset() - - configMapWatcher = configmap.NewFixedWatcher(&corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: pkg.GetServingSystemNamespace(), - Name: ctrl.GetNetworkConfigMapName(), - }, - }, &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: pkg.GetServingSystemNamespace(), - Name: logging.ConfigName, - }, - Data: map[string]string{ - "zap-logger-config": "{\"level\": \"error\",\n\"outputPaths\": [\"stdout\"],\n\"errorOutputPaths\": [\"stderr\"],\n\"encoding\": \"json\"}", - "loglevel.queueproxy": "info", - }, - }, &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: pkg.GetServingSystemNamespace(), - Name: ctrl.GetObservabilityConfigMapName(), - }, - Data: map[string]string{ - "logging.enable-var-log-collection": "true", - "logging.fluentd-sidecar-image": testFluentdImage, - "logging.fluentd-sidecar-output-config": testFluentdSidecarOutputConfig, - }, - }, &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: pkg.GetServingSystemNamespace(), - Name: autoscaler.ConfigName, - }, - Data: map[string]string{ - "max-scale-up-rate": "1.0", - "single-concurrency-target": "1.0", - "multi-concurrency-target": "1.0", - "stable-window": "5m", - "panic-window": "10s", - "scale-to-zero-threshold": "10m", - "concurrency-quantum-of-time": "100ms", - }, - }) - - // Create informer factories with fake clients. The second parameter sets the - // resync period to zero, disabling it. - kubeInformer = kubeinformers.NewSharedInformerFactory(kubeClient, 0) - buildInformer = buildinformers.NewSharedInformerFactory(buildClient, 0) - servingInformer = informers.NewSharedInformerFactory(servingClient, 0) - vpaInformer = vpainformers.NewSharedInformerFactory(vpaClient, 0) - - controller = NewController( - ctrl.Options{ - KubeClientSet: kubeClient, - ServingClientSet: servingClient, - ConfigMapWatcher: configMapWatcher, - Logger: TestLogger(t), - }, - vpaClient, - servingInformer.Serving().V1alpha1().Revisions(), - buildInformer.Build().V1alpha1().Builds(), - kubeInformer.Apps().V1().Deployments(), - kubeInformer.Core().V1().Services(), - kubeInformer.Core().V1().Endpoints(), - vpaInformer.Poc().V1alpha1().VerticalPodAutoscalers(), - &testControllerConfig, - ) - - controller.resolver = &nopResolver{} - return -} - func createRevision(t *testing.T, kubeClient *fakekubeclientset.Clientset, kubeInformer kubeinformers.SharedInformerFactory, servingClient *fakeclientset.Clientset, servingInformer informers.SharedInformerFactory, @@ -489,7 +330,7 @@ func (r *fixedResolver) Resolve(deploy *appsv1.Deployment) error { func TestCreateRevCreatesStuff(t *testing.T) { controllerConfig := getTestControllerConfig() - kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, &controllerConfig) + kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, controllerConfig) // Resolve image references to this "digest" digest := "foo@sha256:deadbeef" @@ -839,7 +680,7 @@ func TestResolutionFailed(t *testing.T) { func TestCreateRevDoesNotSetUpFluentdSidecarIfVarLogCollectionDisabled(t *testing.T) { controllerConfig := getTestControllerConfig() - kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, &controllerConfig, &corev1.ConfigMap{ + kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, controllerConfig, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: pkg.GetServingSystemNamespace(), Name: ctrl.GetObservabilityConfigMapName(), @@ -953,7 +794,7 @@ func TestCreateRevUpdateConfigMap_NewRevOwnerReference(t *testing.T) { func TestCreateRevWithWithLoggingURL(t *testing.T) { controllerConfig := getTestControllerConfig() - kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, &controllerConfig, &corev1.ConfigMap{ + kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, controllerConfig, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: pkg.GetServingSystemNamespace(), Name: ctrl.GetObservabilityConfigMapName(), @@ -983,7 +824,7 @@ func TestCreateRevWithWithLoggingURL(t *testing.T) { func TestCreateRevWithVPA(t *testing.T) { controllerConfig := getTestControllerConfig() - kubeClient, _, servingClient, vpaClient, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, &controllerConfig, &corev1.ConfigMap{ + kubeClient, _, servingClient, vpaClient, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, controllerConfig, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: pkg.GetServingSystemNamespace(), Name: autoscaler.ConfigName, @@ -1025,7 +866,7 @@ func TestCreateRevWithVPA(t *testing.T) { func TestUpdateRevWithWithUpdatedLoggingURL(t *testing.T) { controllerConfig := getTestControllerConfig() - kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, &controllerConfig, &corev1.ConfigMap{ + kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, controllerConfig, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: pkg.GetServingSystemNamespace(), Name: ctrl.GetObservabilityConfigMapName(), @@ -1854,7 +1695,7 @@ func TestReserveToActiveRevisionCreatesStuff(t *testing.T) { func TestNoAutoscalerImageCreatesNoAutoscalers(t *testing.T) { controllerConfig := getTestControllerConfig() controllerConfig.AutoscalerImage = "" - kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, &controllerConfig) + kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, controllerConfig) rev := getTestRevision() config := getTestConfiguration() @@ -2006,7 +1847,7 @@ func TestReconcileReplicaCount(t *testing.T) { func getPodAnnotationsForConfig(t *testing.T, configMapValue string, configAnnotationOverride string) map[string]string { controllerConfig := getTestControllerConfig() - kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, &controllerConfig) + kubeClient, _, servingClient, _, controller, kubeInformer, _, servingInformer, _, _ := newTestControllerWithConfig(t, controllerConfig) // Resolve image references to this "digest" digest := "foo@sha256:deadbeef" diff --git a/pkg/controller/revision/table_test.go b/pkg/controller/revision/table_test.go new file mode 100644 index 000000000000..34e5f9280b0a --- /dev/null +++ b/pkg/controller/revision/table_test.go @@ -0,0 +1,395 @@ +/* +Copyright 2018 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 revision + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + fakebuildclientset "github.com/knative/build/pkg/client/clientset/versioned/fake" + "github.com/knative/serving/pkg" + "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "github.com/knative/serving/pkg/autoscaler" + fakeclientset "github.com/knative/serving/pkg/client/clientset/versioned/fake" + "github.com/knative/serving/pkg/controller" + "github.com/knative/serving/pkg/logging" + . "github.com/knative/serving/pkg/logging/testing" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + fakekubeclientset "k8s.io/client-go/kubernetes/fake" + clientgotesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/cache" + + . "github.com/knative/serving/pkg/controller/testing" +) + +// This is heavily based on the way the OpenShift Ingress controller tests its reconciliation method. +func TestReconcileActive(t *testing.T) { + networkConfig := &NetworkConfig{IstioOutboundIPRanges: "*"} + loggingConfig := &logging.Config{} + observabilityConfig := &ObservabilityConfig{} + autoscalerConfig := &autoscaler.Config{} + controllerConfig := getTestControllerConfig() + + // Create short-hand aliases that pass through the above config and Active to getRev and friends. + rev := func(namespace, name, image string) *v1alpha1.Revision { + return getRev(namespace, name, v1alpha1.RevisionServingStateActive, image, + loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + } + deploy := func(namespace, name, image string) *appsv1.Deployment { + return getDeploy(namespace, name, v1alpha1.RevisionServingStateActive, image, + loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + } + svc := func(namespace, name, image string) *corev1.Service { + return getService(namespace, name, v1alpha1.RevisionServingStateActive, image, + loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + } + deployAS := func(namespace, name, image string) *appsv1.Deployment { + return getASDeploy(namespace, name, v1alpha1.RevisionServingStateActive, image, + loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + } + svcAS := func(namespace, name, image string) *corev1.Service { + return getASService(namespace, name, v1alpha1.RevisionServingStateActive, image, + loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + } + + type fields struct { + r *RevisionLister + b *BuildLister + d *DeploymentLister + s *K8sServiceLister + e *EndpointsLister + } + tests := []struct { + name string + fields fields + key string + wantErr bool + wantCreates []metav1.Object + wantUpdates []clientgotesting.UpdateActionImpl + wantDeletes []clientgotesting.DeleteActionImpl + wantQueue []string + }{{ + name: "bad workqueue key", + key: "too/many/parts", + }, { + name: "key not found", + key: "foo/not-found", + }, { + name: "first revision reconciliation", + fields: fields{ + r: &RevisionLister{ + Items: []*v1alpha1.Revision{rev("foo", "first-reconcile", "busybox")}, + }, + }, + wantCreates: []metav1.Object{ + // The first reconciliation of a Revision creates the following resources. + deploy("foo", "first-reconcile", "busybox"), + svc("foo", "first-reconcile", "busybox"), + deployAS("foo", "first-reconcile", "busybox"), + svcAS("foo", "first-reconcile", "busybox"), + }, + wantUpdates: []clientgotesting.UpdateActionImpl{{ + Object: makeStatus( + rev("foo", "first-reconcile", "busybox"), + // After the first reconciliation of a Revision the status looks like this. + v1alpha1.RevisionStatus{ + ServiceName: svc("foo", "first-reconcile", "busybox").Name, + Conditions: []v1alpha1.RevisionCondition{{ + Type: "ResourcesAvailable", + Status: "Unknown", + Reason: "Deploying", + }, { + Type: "ContainerHealthy", + Status: "Unknown", + Reason: "Deploying", + }, { + Type: "Ready", + Status: "Unknown", + Reason: "Deploying", + }}, + }), + }}, + key: "foo/first-reconcile", + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Default the FooLister fields to reduce boilerplate + rl := tt.fields.r + if rl == nil { + rl = &RevisionLister{} + } + bl := tt.fields.b + if bl == nil { + bl = &BuildLister{} + } + dl := tt.fields.d + if dl == nil { + dl = &DeploymentLister{} + } + sl := tt.fields.s + if sl == nil { + sl = &K8sServiceLister{} + } + el := tt.fields.e + if el == nil { + el = &EndpointsLister{} + } + // Add all of the items in our lister fakes into our + // fake clients. + var objs []runtime.Object + var buildObjs []runtime.Object + var kubeObjs []runtime.Object + for _, r := range rl.Items { + objs = append(objs, r) + } + for _, r := range bl.Items { + buildObjs = append(buildObjs, r) + } + for _, r := range dl.Items { + kubeObjs = append(kubeObjs, r) + } + for _, r := range sl.Items { + kubeObjs = append(kubeObjs, r) + } + for _, r := range el.Items { + kubeObjs = append(kubeObjs, r) + } + kubeClient := fakekubeclientset.NewSimpleClientset(kubeObjs...) + client := fakeclientset.NewSimpleClientset(objs...) + buildClient := fakebuildclientset.NewSimpleClientset(buildObjs...) + // Set up our Controller from the fakes. + c := &Controller{ + Base: controller.NewBase(controller.Options{ + KubeClientSet: kubeClient, + BuildClientSet: buildClient, + ServingClientSet: client, + Logger: TestLogger(t), + }, controllerAgentName, "Revisions"), + revisionLister: rl, + buildLister: bl, + deploymentLister: dl, + serviceLister: sl, + endpointsLister: el, + controllerConfig: controllerConfig, + networkConfig: networkConfig, + loggingConfig: loggingConfig, + observabilityConfig: observabilityConfig, + autoscalerConfig: autoscalerConfig, + resolver: &nopResolver{}, + } + + // Run the Reconcile we're testing. + if err := c.Reconcile(tt.key); (err != nil) != tt.wantErr { + t.Errorf("Reconcile() error = %v, wantErr %v", err, tt.wantErr) + } + // Now check that the Reconcile had the desired effects. + expectedNamespace, _, _ := cache.SplitMetaNamespaceKey(tt.key) + + c.WorkQueue.ShutDown() + var hasQueue []string + for { + key, shutdown := c.WorkQueue.Get() + if shutdown { + break + } + hasQueue = append(hasQueue, key.(string)) + } + if diff := cmp.Diff(tt.wantQueue, hasQueue); diff != "" { + t.Errorf("unexpected queue (-want +got): %s", diff) + } + + var createActions []clientgotesting.CreateAction + var updateActions []clientgotesting.UpdateAction + var deleteActions []clientgotesting.DeleteAction + + type HasActions interface { + Actions() []clientgotesting.Action + } + for _, c := range []HasActions{buildClient, client, kubeClient} { + for _, action := range c.Actions() { + switch action.GetVerb() { + case "create": + createActions = append(createActions, + action.(clientgotesting.CreateAction)) + case "update": + updateActions = append(updateActions, + action.(clientgotesting.UpdateAction)) + case "delete": + deleteActions = append(deleteActions, + action.(clientgotesting.DeleteAction)) + default: + t.Errorf("Unexpected verb %v: %+v", action.GetVerb(), action) + } + } + } + + for i, want := range tt.wantCreates { + if i >= len(createActions) { + t.Errorf("Missing create: %v", want) + continue + } + got := createActions[i] + if got.GetNamespace() != expectedNamespace && got.GetNamespace() != pkg.GetServingSystemNamespace() { + t.Errorf("unexpected action[%d]: %#v", i, got) + } + obj := got.GetObject() + if diff := cmp.Diff(want, obj, ignoreLastTransitionTime, safeDeployDiff); diff != "" { + t.Errorf("unexpected create (-want +got): %s", diff) + } + } + if got, want := len(createActions), len(tt.wantCreates); got > want { + for _, extra := range createActions[want:] { + t.Errorf("Extra create: %v", extra) + } + } + + for i, want := range tt.wantUpdates { + if i >= len(updateActions) { + t.Errorf("Missing update: %v", want) + continue + } + got := updateActions[i] + if diff := cmp.Diff(want.GetObject(), got.GetObject(), ignoreLastTransitionTime); diff != "" { + t.Errorf("unexpected update (-want +got): %s", diff) + } + } + if got, want := len(updateActions), len(tt.wantUpdates); got > want { + for _, extra := range updateActions[want:] { + t.Errorf("Extra update: %v", extra) + } + } + + for i, want := range tt.wantDeletes { + if i >= len(deleteActions) { + t.Errorf("Missing delete: %v", want) + continue + } + got := deleteActions[i] + if got.GetName() != want.Name || got.GetNamespace() != expectedNamespace { + t.Errorf("unexpected delete[%d]: %#v", i, got) + } + } + if got, want := len(deleteActions), len(tt.wantDeletes); got > want { + for _, extra := range deleteActions[want:] { + t.Errorf("Extra delete: %v", extra) + } + } + }) + } +} + +var ignoreLastTransitionTime = cmp.FilterPath(func(p cmp.Path) bool { + return strings.HasSuffix(p.String(), "LastTransitionTime.Time") +}, cmp.Ignore()) + +var safeDeployDiff = cmpopts.IgnoreUnexported(resource.Quantity{}) + +var ( + boolTrue = true +) + +func or(name string) []metav1.OwnerReference { + return []metav1.OwnerReference{{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "Revision", + Name: name, + Controller: &boolTrue, + BlockOwnerDeletion: &boolTrue, + }} +} + +func om(namespace, name string) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + } +} + +func makeStatus(rev *v1alpha1.Revision, status v1alpha1.RevisionStatus) *v1alpha1.Revision { + rev.Status = status + return rev +} + +// The input signatures of these functions should be kept in sync for readability. +func getRev(namespace, name string, servingState v1alpha1.RevisionServingStateType, image string, + loggingConfig *logging.Config, networkConfig *NetworkConfig, observabilityConfig *ObservabilityConfig, + autoscalerConfig *autoscaler.Config, controllerConfig *ControllerConfig) *v1alpha1.Revision { + return &v1alpha1.Revision{ + ObjectMeta: om(namespace, name), + Spec: v1alpha1.RevisionSpec{ + Container: corev1.Container{Image: image}, + ServingState: servingState, + }, + } +} + +func getDeploy(namespace, name string, servingState v1alpha1.RevisionServingStateType, image string, + loggingConfig *logging.Config, networkConfig *NetworkConfig, observabilityConfig *ObservabilityConfig, + autoscalerConfig *autoscaler.Config, controllerConfig *ControllerConfig) *appsv1.Deployment { + + var replicaCount int32 = 1 + if servingState == v1alpha1.RevisionServingStateReserve { + replicaCount = 0 + } + rev := getRev(namespace, name, servingState, image, loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + return MakeServingDeployment(rev, loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig, replicaCount) +} + +func getService(namespace, name string, servingState v1alpha1.RevisionServingStateType, image string, + loggingConfig *logging.Config, networkConfig *NetworkConfig, observabilityConfig *ObservabilityConfig, + autoscalerConfig *autoscaler.Config, controllerConfig *ControllerConfig) *corev1.Service { + + rev := getRev(namespace, name, servingState, image, loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + return MakeRevisionK8sService(rev) +} + +func getASDeploy(namespace, name string, servingState v1alpha1.RevisionServingStateType, image string, + loggingConfig *logging.Config, networkConfig *NetworkConfig, observabilityConfig *ObservabilityConfig, + autoscalerConfig *autoscaler.Config, controllerConfig *ControllerConfig) *appsv1.Deployment { + + var replicaCount int32 = 1 + if servingState == v1alpha1.RevisionServingStateReserve { + replicaCount = 0 + } + rev := getRev(namespace, name, servingState, image, loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + return MakeServingAutoscalerDeployment(rev, controllerConfig.AutoscalerImage, replicaCount) +} + +func getASService(namespace, name string, servingState v1alpha1.RevisionServingStateType, image string, + loggingConfig *logging.Config, networkConfig *NetworkConfig, observabilityConfig *ObservabilityConfig, + autoscalerConfig *autoscaler.Config, controllerConfig *ControllerConfig) *corev1.Service { + + rev := getRev(namespace, name, servingState, image, loggingConfig, networkConfig, observabilityConfig, + autoscalerConfig, controllerConfig) + return MakeServingAutoscalerService(rev) +} diff --git a/pkg/controller/testing/listers.go b/pkg/controller/testing/listers.go index 5b3c5a3fd4d3..d650811798bf 100644 --- a/pkg/controller/testing/listers.go +++ b/pkg/controller/testing/listers.go @@ -14,12 +14,20 @@ limitations under the License. package testing import ( + "fmt" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" + buildv1alpha1 "github.com/knative/build/pkg/apis/build/v1alpha1" + buildlisters "github.com/knative/build/pkg/client/listers/build/v1alpha1" "github.com/knative/serving/pkg/apis/serving/v1alpha1" listers "github.com/knative/serving/pkg/client/listers/serving/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + appsv1listers "k8s.io/client-go/listers/apps/v1" + corev1listers "k8s.io/client-go/listers/core/v1" ) // ServiceLister is a lister.ServiceLister fake for testing. @@ -173,3 +181,163 @@ func (r *nsRevisionLister) Get(name string) (*v1alpha1.Revision, error) { } return nil, errors.NewNotFound(schema.GroupResource{}, name) } + +// BuildLister is a lister.BuildLister fake for testing. +type BuildLister struct { + Err error + Items []*buildv1alpha1.Build +} + +// Assert that our fake implements the interface it is faking. +var _ buildlisters.BuildLister = (*BuildLister)(nil) + +func (r *BuildLister) List(selector labels.Selector) ([]*buildv1alpha1.Build, error) { + return r.Items, r.Err +} + +func (r *BuildLister) Builds(namespace string) buildlisters.BuildNamespaceLister { + return &nsBuildLister{r: r, ns: namespace} +} + +type nsBuildLister struct { + r *BuildLister + ns string +} + +// Assert that our fake implements the interface it is faking. +var _ buildlisters.BuildNamespaceLister = (*nsBuildLister)(nil) + +func (r *nsBuildLister) List(selector labels.Selector) ([]*buildv1alpha1.Build, error) { + return r.r.Items, r.r.Err +} + +func (r *nsBuildLister) Get(name string) (*buildv1alpha1.Build, error) { + for _, s := range r.r.Items { + if s.Name == name && r.ns == s.Namespace { + return s, nil + } + } + return nil, errors.NewNotFound(schema.GroupResource{}, name) +} + +// DeploymentLister is a lister.DeploymentLister fake for testing. +type DeploymentLister struct { + Err error + Items []*appsv1.Deployment +} + +// Assert that our fake implements the interface it is faking. +var _ appsv1listers.DeploymentLister = (*DeploymentLister)(nil) + +func (r *DeploymentLister) List(selector labels.Selector) ([]*appsv1.Deployment, error) { + return r.Items, r.Err +} + +func (r *DeploymentLister) Deployments(namespace string) appsv1listers.DeploymentNamespaceLister { + return &nsDeploymentLister{r: r, ns: namespace} +} + +func (r *DeploymentLister) GetDeploymentsForReplicaSet(rs *appsv1.ReplicaSet) ([]*appsv1.Deployment, error) { + return nil, fmt.Errorf("unimplemented GetDeploymentsForReplicaSet(%v)", rs) +} + +type nsDeploymentLister struct { + r *DeploymentLister + ns string +} + +// Assert that our fake implements the interface it is faking. +var _ appsv1listers.DeploymentNamespaceLister = (*nsDeploymentLister)(nil) + +func (r *nsDeploymentLister) List(selector labels.Selector) ([]*appsv1.Deployment, error) { + return r.r.Items, r.r.Err +} + +func (r *nsDeploymentLister) Get(name string) (*appsv1.Deployment, error) { + for _, s := range r.r.Items { + if s.Name == name && r.ns == s.Namespace { + return s, nil + } + } + return nil, errors.NewNotFound(schema.GroupResource{}, name) +} + +// EndpointsLister is a lister.EndpointsLister fake for testing. +type EndpointsLister struct { + Err error + Items []*corev1.Endpoints +} + +// Assert that our fake implements the interface it is faking. +var _ corev1listers.EndpointsLister = (*EndpointsLister)(nil) + +func (r *EndpointsLister) List(selector labels.Selector) ([]*corev1.Endpoints, error) { + return r.Items, r.Err +} + +func (r *EndpointsLister) Endpoints(namespace string) corev1listers.EndpointsNamespaceLister { + return &nsEndpointsLister{r: r, ns: namespace} +} + +type nsEndpointsLister struct { + r *EndpointsLister + ns string +} + +// Assert that our fake implements the interface it is faking. +var _ corev1listers.EndpointsNamespaceLister = (*nsEndpointsLister)(nil) + +func (r *nsEndpointsLister) List(selector labels.Selector) ([]*corev1.Endpoints, error) { + return r.r.Items, r.r.Err +} + +func (r *nsEndpointsLister) Get(name string) (*corev1.Endpoints, error) { + for _, s := range r.r.Items { + if s.Name == name && r.ns == s.Namespace { + return s, nil + } + } + return nil, errors.NewNotFound(schema.GroupResource{}, name) +} + +// K8sServiceLister is a lister.ServiceLister fake for testing. +type K8sServiceLister struct { + Err error + Items []*corev1.Service +} + +// Assert that our fake implements the interface it is faking. +var _ corev1listers.ServiceLister = (*K8sServiceLister)(nil) + +func (r *K8sServiceLister) List(selector labels.Selector) ([]*corev1.Service, error) { + return r.Items, r.Err +} + +func (r *K8sServiceLister) Services(namespace string) corev1listers.ServiceNamespaceLister { + return &nsK8sServiceLister{r: r, ns: namespace} +} + +func (r *K8sServiceLister) GetPodServices(p *corev1.Pod) ([]*corev1.Service, error) { + return nil, fmt.Errorf("unimplemented GetPodServices(%v)", p) +} + +type nsK8sServiceLister struct { + r *K8sServiceLister + ns string +} + +// Assert that our fake implements the interface it is faking. +var _ corev1listers.ServiceNamespaceLister = (*nsK8sServiceLister)(nil) + +func (r *nsK8sServiceLister) List(selector labels.Selector) ([]*corev1.Service, error) { + return r.r.Items, r.r.Err +} + +func (r *nsK8sServiceLister) Get(name string) (*corev1.Service, error) { + for _, s := range r.r.Items { + if s.Name == name && r.ns == s.Namespace { + return s, nil + } + } + return nil, errors.NewNotFound(schema.GroupResource{}, name) +}