diff --git a/pkg/reconciler/revision/reconcile_resources.go b/pkg/reconciler/revision/reconcile_resources.go index cdb90c553794..7f917861ec5b 100644 --- a/pkg/reconciler/revision/reconcile_resources.go +++ b/pkg/reconciler/revision/reconcile_resources.go @@ -19,6 +19,8 @@ package revision import ( "context" "fmt" + "maps" + "strings" "go.uber.org/zap" "knative.dev/pkg/tracker" @@ -36,6 +38,7 @@ import ( "knative.dev/pkg/kmp" "knative.dev/pkg/logging" "knative.dev/pkg/logging/logkey" + "knative.dev/serving/pkg/apis/autoscaling" v1 "knative.dev/serving/pkg/apis/serving/v1" "knative.dev/serving/pkg/networking" "knative.dev/serving/pkg/reconciler/revision/config" @@ -176,26 +179,99 @@ func (c *Reconciler) reconcilePA(ctx context.Context, rev *v1.Revision) error { return fmt.Errorf("revision: %q does not own PodAutoscaler: %q", rev.Name, paName) } + logger.Debugf("Observed PA Status=%#v", pa.Status) + rev.Status.PropagateAutoscalerStatus(&pa.Status) + // Perhaps tha PA spec changed underneath ourselves? // We no longer require immutability, so need to reconcile PA each time. tmpl := resources.MakePA(rev, deployment) logger.Debugf("Desired PASpec: %#v", tmpl.Spec) - if !equality.Semantic.DeepEqual(tmpl.Spec, pa.Spec) { - diff, _ := kmp.SafeDiff(tmpl.Spec, pa.Spec) // Can't realistically fail on PASpec. - logger.Infof("PA %s needs reconciliation, diff(-want,+got):\n%s", pa.Name, diff) - + if !equality.Semantic.DeepEqual(tmpl.Spec, pa.Spec) || !kpaAnnotationsPresent(pa.Annotations, tmpl.Annotations) { want := pa.DeepCopy() want.Spec = tmpl.Spec - if pa, err = c.client.AutoscalingV1alpha1().PodAutoscalers(ns).Update(ctx, want, metav1.UpdateOptions{}); err != nil { - return fmt.Errorf("failed to update PA %q: %w", paName, err) + + kpaProcessAnnotations(want.Annotations, tmpl.Annotations) + + // Can't realistically fail on PASpec. + if diff, _ := kmp.SafeDiff(want.Spec, pa.Spec); diff != "" { + logger.Infof("PA %q spec needs reconciliation, diff(-want,+got):\n%s", pa.Name, diff) + } + + if diff, _ := kmp.SafeDiff(want.Annotations, pa.Annotations); diff != "" { + logger.Infof("PA %q annotations needs reconciliation, diff(-want,+got):\n%s", pa.Name, diff) + } + + _, err := c.client.AutoscalingV1alpha1().PodAutoscalers(ns).Update(ctx, want, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update PA %q: %w", want.Name, err) } } - logger.Debugf("Observed PA Status=%#v", pa.Status) - rev.Status.PropagateAutoscalerStatus(&pa.Status) return nil } +func isDefaultedKPAAnnotation(k string) bool { + switch k { + case autoscaling.ClassAnnotationKey, autoscaling.MetricAnnotationKey: + return true + default: + return false + } +} + +func kpaProcessAnnotations(dst, src map[string]string) { + // Delete autoscaling annotations from destination map + // This ensures that setting these annotation on the Revision is the source of truth + for k := range dst { + if isDefaultedKPAAnnotation(k) { + // Exclude defaulted annotation + continue + } + if strings.HasPrefix(k, autoscaling.GroupName) { + delete(dst, k) + } + } + + // copy source annotations over while preserving existing ones + maps.Copy(dst, src) +} + +func kpaAnnotationsPresent(dst, src map[string]string) bool { + // Check for extra autoscaling annotations that don't exist src + for k := range dst { + if !strings.HasPrefix(k, autoscaling.GroupName) { + continue + } + // Exclude defaulted annotation + if isDefaultedKPAAnnotation(k) { + continue + } + + if _, ok := src[k]; !ok { + // Scaling annotation is in dst but not src + // return false to trigger reconciliation + return false + } + } + + for k, want := range src { + got, ok := dst[k] + + if !ok { + if isDefaultedKPAAnnotation(k) { + continue + } + + return false + } + + if got != want { + return false + } + } + return true +} + func hasDeploymentTimedOut(deployment *appsv1.Deployment) bool { // as per https://kubernetes.io/docs/concepts/workloads/controllers/deployment for _, cond := range deployment.Status.Conditions { diff --git a/pkg/reconciler/revision/resources/deploy.go b/pkg/reconciler/revision/resources/deploy.go index fff3847cb7eb..63d398a6dc2c 100644 --- a/pkg/reconciler/revision/resources/deploy.go +++ b/pkg/reconciler/revision/resources/deploy.go @@ -372,8 +372,6 @@ func MakeDeployment(rev *v1.Revision, cfg *config.Config) (*appsv1.Deployment, e } labels := makeLabels(rev) - anns := makeAnnotations(rev) - annsPod := makeAnnotationsForPod(rev, anns) // Slowly but steadily roll the deployment out, to have the least possible impact. maxUnavailable := intstr.FromInt(0) @@ -382,7 +380,7 @@ func MakeDeployment(rev *v1.Revision, cfg *config.Config) (*appsv1.Deployment, e Name: names.Deployment(rev), Namespace: rev.Namespace, Labels: labels, - Annotations: anns, + Annotations: deploymentAnnotations(rev), OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(rev)}, }, Spec: appsv1.DeploymentSpec{ @@ -399,7 +397,7 @@ func MakeDeployment(rev *v1.Revision, cfg *config.Config) (*appsv1.Deployment, e Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, - Annotations: annsPod, + Annotations: podAnnotations(rev), }, Spec: *podSpec, }, diff --git a/pkg/reconciler/revision/resources/deploy_test.go b/pkg/reconciler/revision/resources/deploy_test.go index 897428fff91e..873c7e4d8e52 100644 --- a/pkg/reconciler/revision/resources/deploy_test.go +++ b/pkg/reconciler/revision/resources/deploy_test.go @@ -1916,12 +1916,8 @@ func TestMakeDeployment(t *testing.T) { ), want: appsv1deployment(func(deploy *appsv1.Deployment) { deploy.Spec.Replicas = ptr.Int32(int32(20)) - deploy.Annotations = map[string]string{ - autoscaling.InitialScaleAnnotationKey: "20", - } deploy.Spec.Template.Annotations = map[string]string{ - autoscaling.InitialScaleAnnotationKey: "20", - DefaultContainerAnnotationName: servingContainerName, + DefaultContainerAnnotationName: servingContainerName, } }), }} diff --git a/pkg/reconciler/revision/resources/imagecache.go b/pkg/reconciler/revision/resources/imagecache.go index 7d871016e87b..632c6643f28a 100644 --- a/pkg/reconciler/revision/resources/imagecache.go +++ b/pkg/reconciler/revision/resources/imagecache.go @@ -32,7 +32,7 @@ func MakeImageCache(rev *v1.Revision, containerName, image string) *caching.Imag Name: kmeta.ChildName(names.ImageCache(rev), "-"+containerName), Namespace: rev.Namespace, Labels: makeLabels(rev), - Annotations: makeAnnotations(rev), + Annotations: imageCacheAnnotations(rev), OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(rev)}, }, Spec: caching.ImageSpec{ diff --git a/pkg/reconciler/revision/resources/meta.go b/pkg/reconciler/revision/resources/meta.go index f89cb1827b8c..ad40f24cb778 100644 --- a/pkg/reconciler/revision/resources/meta.go +++ b/pkg/reconciler/revision/resources/meta.go @@ -17,11 +17,12 @@ limitations under the License. package resources import ( - "maps" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" - "knative.dev/pkg/kmeta" + "knative.dev/pkg/kmap" + "knative.dev/serving/pkg/apis/autoscaling" "knative.dev/serving/pkg/apis/serving" v1 "knative.dev/serving/pkg/apis/serving/v1" ) @@ -46,8 +47,8 @@ const ( // makeLabels constructs the labels we will apply to K8s resources. func makeLabels(revision *v1.Revision) map[string]string { - labels := kmeta.FilterMap(revision.GetLabels(), excludeLabels.Has) - labels = kmeta.UnionMaps(labels, map[string]string{ + labels := kmap.Filter(revision.GetLabels(), excludeLabels.Has) + labels = kmap.Union(labels, map[string]string{ serving.RevisionLabelKey: revision.Name, serving.RevisionUID: string(revision.UID), }) @@ -61,19 +62,30 @@ func makeLabels(revision *v1.Revision) map[string]string { return labels } -func makeAnnotations(revision *v1.Revision) map[string]string { - return kmeta.FilterMap(revision.GetAnnotations(), excludeAnnotations.Has) +func filterExcludedAndAutoscalingAnnotations(val string) bool { + return excludeAnnotations.Has(val) || strings.HasPrefix(val, autoscaling.GroupName) } -func makeAnnotationsForPod(revision *v1.Revision, baseAnnotations map[string]string) map[string]string { - podAnnotations := maps.Clone(baseAnnotations) +func deploymentAnnotations(r *v1.Revision) map[string]string { + return kmap.Filter(r.GetAnnotations(), filterExcludedAndAutoscalingAnnotations) +} + +func imageCacheAnnotations(r *v1.Revision) map[string]string { + return kmap.Filter(r.GetAnnotations(), filterExcludedAndAutoscalingAnnotations) +} + +func podAutoscalerAnnotations(r *v1.Revision) map[string]string { + return kmap.Filter(r.GetAnnotations(), excludeAnnotations.Has) +} + +func podAnnotations(r *v1.Revision) map[string]string { + ann := kmap.Filter(r.GetAnnotations(), filterExcludedAndAutoscalingAnnotations) - // Add default container annotation to the pod meta - if userContainer := revision.Spec.GetContainer(); userContainer.Name != "" { - podAnnotations[DefaultContainerAnnotationName] = userContainer.Name + if userContainer := r.Spec.GetContainer(); userContainer.Name != "" { + ann[DefaultContainerAnnotationName] = userContainer.Name } - return podAnnotations + return ann } // makeSelector constructs the Selector we will apply to K8s resources. diff --git a/pkg/reconciler/revision/resources/meta_test.go b/pkg/reconciler/revision/resources/meta_test.go index 9a70645836a7..d6da85dccff6 100644 --- a/pkg/reconciler/revision/resources/meta_test.go +++ b/pkg/reconciler/revision/resources/meta_test.go @@ -17,12 +17,17 @@ limitations under the License. package resources import ( + "fmt" + "reflect" + "runtime" + "strings" "testing" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/serving/pkg/apis/autoscaling" "knative.dev/serving/pkg/apis/serving" v1 "knative.dev/serving/pkg/apis/serving/v1" ) @@ -104,41 +109,87 @@ func TestMakeLabels(t *testing.T) { } func TestMakeAnnotations(t *testing.T) { + type buildFuncs []func(*v1.Revision) map[string]string + tests := []struct { - name string - rev *v1.Revision - want map[string]string + name string + buildFuncs buildFuncs + revAnn map[string]string + want map[string]string }{{ name: "no user annotations", - rev: &v1.Revision{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "bar", - }, + buildFuncs: buildFuncs{ + deploymentAnnotations, + imageCacheAnnotations, + podAutoscalerAnnotations, + podAnnotations, }, - want: map[string]string{}, + revAnn: map[string]string{}, + want: map[string]string{}, }, { - name: "exclude annotation", - rev: &v1.Revision{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "foo", - Name: "bar", - Annotations: map[string]string{ - serving.RoutingStateModifiedAnnotationKey: "exclude me", - "keep": "keep me", - }, - }, + name: "excluded annotations", + buildFuncs: buildFuncs{ + deploymentAnnotations, + imageCacheAnnotations, + podAutoscalerAnnotations, + podAnnotations, + }, + revAnn: map[string]string{ + serving.RoutingStateModifiedAnnotationKey: "exclude me", + "keep": "keep me", }, want: map[string]string{"keep": "keep me"}, + }, { + name: "exclude autoscaling annotations", + buildFuncs: buildFuncs{ + deploymentAnnotations, + imageCacheAnnotations, + podAnnotations, + }, + revAnn: map[string]string{ + autoscaling.MinScaleAnnotationKey: "1", + "keep": "keep me", + }, + want: map[string]string{"keep": "keep me"}, + }, { + name: "include autoscaling annotations", + buildFuncs: buildFuncs{ + podAutoscalerAnnotations, + }, + revAnn: map[string]string{ + autoscaling.MinScaleAnnotationKey: "1", + "keep": "keep me", + }, + want: map[string]string{ + autoscaling.MinScaleAnnotationKey: "1", + "keep": "keep me", + }, }} for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := makeAnnotations(test.rev) - if diff := cmp.Diff(test.want, got); diff != "" { - t.Error("makeLabels (-want, +got) =", diff) - } - }) + for _, buildFunc := range test.buildFuncs { + funcName := runtime.FuncForPC(reflect.ValueOf(buildFunc).Pointer()).Name() + i := strings.LastIndex(funcName, ".") + funcName = funcName[i+1:] + + testName := fmt.Sprint(test.name, " ", funcName) + + t.Run(testName, func(t *testing.T) { + rev := &v1.Revision{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + Name: "bar", + Annotations: test.revAnn, + }, + } + + got := buildFunc(rev) + + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("%s(-want, +got) = %s", funcName, diff) + } + }) + } } } @@ -151,6 +202,11 @@ func TestMakeAnnotationsForPod(t *testing.T) { }{{ name: "multiple containers single port with base annotation", rev: &v1.Revision{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "asdf": "fdsa", + }, + }, Spec: v1.RevisionSpec{ PodSpec: corev1.PodSpec{ Containers: []corev1.Container{{ @@ -164,7 +220,6 @@ func TestMakeAnnotationsForPod(t *testing.T) { }, }, }, - baseAnnotations: map[string]string{"asdf": "fdsa"}, want: map[string]string{ "asdf": "fdsa", DefaultContainerAnnotationName: "bar", @@ -173,7 +228,7 @@ func TestMakeAnnotationsForPod(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := makeAnnotationsForPod(test.rev, test.baseAnnotations) + got := podAnnotations(test.rev) if diff := cmp.Diff(test.want, got); diff != "" { t.Error("getUserContainerName (-want, +got) =", diff) } diff --git a/pkg/reconciler/revision/resources/pa.go b/pkg/reconciler/revision/resources/pa.go index 32a2cff3cbb1..9988dddef928 100644 --- a/pkg/reconciler/revision/resources/pa.go +++ b/pkg/reconciler/revision/resources/pa.go @@ -35,7 +35,7 @@ func MakePA(rev *v1.Revision, deployment *appsv1.Deployment) *autoscalingv1alpha Name: names.PA(rev), Namespace: rev.Namespace, Labels: makeLabels(rev), - Annotations: makeAnnotations(rev), + Annotations: podAutoscalerAnnotations(rev), OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(rev)}, }, Spec: autoscalingv1alpha1.PodAutoscalerSpec{ diff --git a/pkg/reconciler/revision/table_test.go b/pkg/reconciler/revision/table_test.go index 016828a5b081..2495b35a2e25 100644 --- a/pkg/reconciler/revision/table_test.go +++ b/pkg/reconciler/revision/table_test.go @@ -37,6 +37,7 @@ import ( "knative.dev/pkg/controller" "knative.dev/pkg/logging" pkgreconciler "knative.dev/pkg/reconciler" + "knative.dev/serving/pkg/apis/autoscaling" autoscalingv1alpha1 "knative.dev/serving/pkg/apis/autoscaling/v1alpha1" defaultconfig "knative.dev/serving/pkg/apis/config" "knative.dev/serving/pkg/apis/serving" @@ -334,6 +335,7 @@ func TestReconcile(t *testing.T) { WithLogURL, WithRevisionObservedGeneration(1)), pa("foo", "pa-inactive", + WithReachability(autoscalingv1alpha1.ReachabilityUnreachable), WithNoTraffic("NoTraffic", "This thing is inactive."), WithPAStatusService("pa-inactive")), readyDeploy(deploy(t, "foo", "pa-inactive")), @@ -344,9 +346,9 @@ func TestReconcile(t *testing.T) { WithLogURL, withDefaultContainerStatuses(), MarkDeploying(""), // When we reconcile an "all ready" revision when the PA // is inactive, we should see the following change. - MarkInactive("NoTraffic", "This thing is inactive."), WithRevisionObservedGeneration(1), - MarkResourcesUnavailable(v1.ReasonProgressDeadlineExceeded, - "Initial scale was never achieved")), + MarkInactive("NoTraffic", "This thing is inactive."), + WithRevisionObservedGeneration(1), + MarkResourcesUnavailable(v1.ReasonProgressDeadlineExceeded, "Initial scale was never achieved")), }}, Key: "foo/pa-inactive", }, { @@ -799,6 +801,40 @@ func TestReconcile(t *testing.T) { image("foo", "container-healthy"), }, Key: "foo/container-healthy", + }, { + Name: "update pa annotations when they change", + Objects: []runtime.Object{ + Revision("foo", "update-pa-annotations", + WithLogURL, + MarkRevisionReady, + withDefaultContainerStatuses(), + WithRevisionLabel(serving.RoutingStateLabelKey, "active"), + MarkContainerHealthyTrue(), + // New Annotation + WithRevisionAnn(autoscaling.MinScaleAnnotationKey, "1"), + ), + pa("foo", "update-pa-annotations", + WithPASKSReady, + WithScaleTargetInitialized, + WithTraffic, + WithReachabilityReachable, + WithPAStatusService("something"), + ), + readyDeploy(deploy(t, "foo", "update-pa-annotations", withReplicas(1))), + image("foo", "update-pa-annotations"), + }, + WantUpdates: []clientgotesting.UpdateActionImpl{{ + Object: pa("foo", "update-pa-annotations", + WithPASKSReady, + WithScaleTargetInitialized, + WithTraffic, + WithReachabilityReachable, + WithPAStatusService("something"), + WithAnnotationValue(autoscaling.MinScaleAnnotationKey, "1"), + ), + }}, + // No changes are made to any objects. + Key: "foo/update-pa-annotations", }} table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, _ configmap.Watcher) controller.Reconciler { diff --git a/pkg/testing/functional.go b/pkg/testing/functional.go index 176d1e73abf2..05fb02745173 100644 --- a/pkg/testing/functional.go +++ b/pkg/testing/functional.go @@ -155,7 +155,7 @@ func WithPAContainerConcurrency(cc int64) PodAutoscalerOption { } } -func withAnnotationValue(key, value string) PodAutoscalerOption { +func WithAnnotationValue(key, value string) PodAutoscalerOption { return func(pa *autoscalingv1alpha1.PodAutoscaler) { if pa.Annotations == nil { pa.Annotations = make(map[string]string, 1) @@ -168,21 +168,21 @@ func withAnnotationValue(key, value string) PodAutoscalerOption { // the PodAutoscaler autoscaling.knative.dev/target annotation to the // provided value. func WithTargetAnnotation(target string) PodAutoscalerOption { - return withAnnotationValue(autoscaling.TargetAnnotationKey, target) + return WithAnnotationValue(autoscaling.TargetAnnotationKey, target) } // WithTUAnnotation returns a PodAutoscalerOption which sets // the PodAutoscaler autoscaling.knative.dev/targetUtilizationPercentage // annotation to the provided value. func WithTUAnnotation(tu string) PodAutoscalerOption { - return withAnnotationValue(autoscaling.TargetUtilizationPercentageKey, tu) + return WithAnnotationValue(autoscaling.TargetUtilizationPercentageKey, tu) } // WithWindowAnnotation returns a PodAutoScalerOption which sets // the PodAutoscaler autoscaling.knative.dev/window annotation to the // provided value. func WithWindowAnnotation(window string) PodAutoscalerOption { - return withAnnotationValue(autoscaling.WindowAnnotationKey, window) + return WithAnnotationValue(autoscaling.WindowAnnotationKey, window) } // WithPanicThresholdPercentageAnnotation returns a PodAutoscalerOption @@ -190,7 +190,7 @@ func WithWindowAnnotation(window string) PodAutoscalerOption { // autoscaling.knative.dev/panicThresholdPercentage annotation to the // provided value. func WithPanicThresholdPercentageAnnotation(percentage string) PodAutoscalerOption { - return withAnnotationValue(autoscaling.PanicThresholdPercentageAnnotationKey, percentage) + return WithAnnotationValue(autoscaling.PanicThresholdPercentageAnnotationKey, percentage) } // WithPanicWindowPercentageAnnotation retturn a PodAutoscalerOption @@ -198,12 +198,12 @@ func WithPanicThresholdPercentageAnnotation(percentage string) PodAutoscalerOpti // autoscaling.knative.dev/panicWindowPercentage annotation to the // provided value. func WithPanicWindowPercentageAnnotation(percentage string) PodAutoscalerOption { - return withAnnotationValue(autoscaling.PanicWindowPercentageAnnotationKey, percentage) + return WithAnnotationValue(autoscaling.PanicWindowPercentageAnnotationKey, percentage) } // WithMetricAnnotation adds a metric annotation to the PA. func WithMetricAnnotation(metric string) PodAutoscalerOption { - return withAnnotationValue(autoscaling.MetricAnnotationKey, metric) + return WithAnnotationValue(autoscaling.MetricAnnotationKey, metric) } // WithObservedGeneration returns a PodAutoScalerOption which sets @@ -221,12 +221,12 @@ func WithMetricOwnersRemoved(m *autoscalingv1alpha1.Metric) { // WithUpperScaleBound sets maxScale to the given number. func WithUpperScaleBound(i int) PodAutoscalerOption { - return withAnnotationValue(autoscaling.MaxScaleAnnotationKey, strconv.Itoa(i)) + return WithAnnotationValue(autoscaling.MaxScaleAnnotationKey, strconv.Itoa(i)) } // WithLowerScaleBound sets minScale to the given number. func WithLowerScaleBound(i int) PodAutoscalerOption { - return withAnnotationValue(autoscaling.MinScaleAnnotationKey, strconv.Itoa(i)) + return WithAnnotationValue(autoscaling.MinScaleAnnotationKey, strconv.Itoa(i)) } // K8sServiceOption enables further configuration of the Kubernetes Service. diff --git a/test/e2e/minscale_readiness_test.go b/test/e2e/minscale_readiness_test.go index 479a473f937a..c4f4da022d9a 100644 --- a/test/e2e/minscale_readiness_test.go +++ b/test/e2e/minscale_readiness_test.go @@ -24,6 +24,7 @@ import ( "errors" "fmt" "strconv" + "strings" "testing" "time" @@ -366,6 +367,86 @@ func TestMinScale(t *testing.T) { } } +func TestMinScaleAnnotationChange(t *testing.T) { + t.Parallel() + + minScale := []int{1, 2, 3, 0} + + clients := Setup(t) + + names := test.ResourceNames{ + // Config and Route have different names to avoid false positives + Config: test.ObjectNameForTest(t), + Route: test.ObjectNameForTest(t), + Image: test.HelloWorld, + } + + test.EnsureTearDown(t, clients, &names) + + t.Log("Creating route") + if _, err := v1test.CreateRoute(t, clients, names); err != nil { + t.Fatal("Failed to create Route:", err) + } + + t.Log("Creating configuration") + _, err := v1test.CreateConfiguration(t, clients, names, withMinScale(minScale[0]), + // Make sure we scale down quickly after panic, before the autoscaler get killed by chaosduck. + withWindow(autoscaling.WindowMin), + // Pass low resource requirements to avoid Pod scheduling problems + // on busy clusters. This is adapted from ./test/e2e/scale.go + func(cfg *v1.Configuration) { + cfg.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("50Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("30m"), + corev1.ResourceMemory: resource.MustParse("20Mi"), + }, + } + }) + if err != nil { + t.Fatal("Failed to create Configuration:", err) + } + + err = v1test.WaitForConfigurationState(clients.ServingClient, names.Config, v1test.IsConfigurationReady, "wait for configuration ready") + if err != nil { + t.Fatal("Configuration failed to become ready:", err) + } + + revName := latestRevisionName(t, clients, names.Config, "") + serviceName := PrivateServiceName(t, clients, revName) + revClient := clients.ServingClient.Revisions + + t.Log("Holding revision at minScale after becoming ready") + if lr, ok := ensureDesiredScale(clients, t, serviceName, eq(minScale[0])); !ok { + t.Fatalf("The revision %q observed scale %d < %d after becoming ready", revName, lr, minScale) + } + + for i := 1; i < len(minScale); i++ { + var data []byte + + // ~1 is how you escape a / in JSONPath + annotationPath := "/metadata/annotations/" + strings.ReplaceAll(autoscaling.MinScaleAnnotationKey, "/", "~1") + if minScale[i] > 0 { + data = fmt.Appendf(data, `[{"op":"replace","path":"%s", "value":"%d"}]`, annotationPath, minScale[i]) + } else { + data = fmt.Appendf(data, `[{"op":"remove","path":"%s"}]`, annotationPath) + } + + t.Log("Setting min-scale annotation to", minScale[i]) + _, err = revClient.Patch(context.Background(), revName, types.JSONPatchType, data, metav1.PatchOptions{}) + if err != nil { + t.Fatalf("An error occurred updating revision %v, %v", revName, err) + } + + if lr, err := waitForDesiredScale(clients, serviceName, eq(minScale[i])); err != nil { + t.Fatalf("The revision %q observed scale %d < %d after becoming ready", revName, lr, minScale[i]) + } + } +} + func gte(m int) func(int) bool { return func(n int) bool { return n >= m