From c8efc290b52432df3f236f76a42d6c7a0d513999 Mon Sep 17 00:00:00 2001 From: Sukhil Suresh Date: Tue, 24 Jul 2018 15:09:07 -0400 Subject: [PATCH 1/2] Initial PR for issue #1448 Avoid http retries by activator (for forwarding request to the revision pod) when user has defined an HTTPGet readiness probe * TCPSocket and Exec action based user-defined readinessProbe are not supported * Follow-up PR will address on how to better handle the scenario when user has NOT defined a readinessProbek Co-authored-by: Shash Signed-off-by: Shash --- cmd/activator/main.go | 27 ++- pkg/activator/activator.go | 17 +- pkg/activator/dedupe_test.go | 14 +- pkg/activator/prober.go | 123 +++++++++++++ pkg/activator/prober_test.go | 325 +++++++++++++++++++++++++++++++++ pkg/activator/revision.go | 3 + pkg/activator/revision_test.go | 6 +- 7 files changed, 496 insertions(+), 19 deletions(-) create mode 100644 pkg/activator/prober.go create mode 100644 pkg/activator/prober_test.go diff --git a/cmd/activator/main.go b/cmd/activator/main.go index 0a600e1f6e9c..ed7225b01832 100644 --- a/cmd/activator/main.go +++ b/cmd/activator/main.go @@ -17,16 +17,17 @@ limitations under the License. package main import ( - "bytes" "flag" "fmt" - "io" - "io/ioutil" "log" "net/http" + "time" + + "bytes" + "io" + "io/ioutil" "net/http/httputil" "net/url" - "time" "github.com/knative/serving/pkg/logging/logkey" @@ -135,14 +136,26 @@ func (a *activationHandler) handler(w http.ResponseWriter, r *http.Request) { http.Error(w, msg, int(status)) return } + + var transport http.RoundTripper + if endpoint.IsVerified() { + transport = http.DefaultTransport + if r.ProtoMajor == 2 { + transport = h2cutil.NewTransport() + } + } else { + transport = retryRoundTripper{ + logger: a.logger, + } + } + target := &url.URL{ Scheme: "http", Host: fmt.Sprintf("%s:%d", endpoint.FQDN, endpoint.Port), } + proxy := httputil.NewSingleHostReverseProxy(target) - proxy.Transport = retryRoundTripper{ - logger: a.logger, - } + proxy.Transport = transport // TODO: Clear the host to avoid 404's. // https://github.com/knative/serving/issues/964 diff --git a/pkg/activator/activator.go b/pkg/activator/activator.go index b02fca1e9c44..343181b030f4 100644 --- a/pkg/activator/activator.go +++ b/pkg/activator/activator.go @@ -37,6 +37,19 @@ type revisionID struct { // Endpoint is a fully-qualified domain name / port pair for an active revision. type Endpoint struct { - FQDN string - Port int32 + FQDN string + Port int32 + Verified VerificationStatus +} + +type VerificationStatus string + +const ( + Unknown VerificationStatus = "Unknown" + Pass VerificationStatus = "Pass" + Fail VerificationStatus = "Fail" +) + +func (e *Endpoint) IsVerified() bool { + return e.Verified == Pass } diff --git a/pkg/activator/dedupe_test.go b/pkg/activator/dedupe_test.go index 2760637b7c16..d880950a3804 100644 --- a/pkg/activator/dedupe_test.go +++ b/pkg/activator/dedupe_test.go @@ -25,7 +25,7 @@ import ( ) func TestSingleRevision_SingleRequest_Success(t *testing.T) { - want := Endpoint{"ip", 8080} + want := Endpoint{FQDN: "ip", Port: 8080} f := newFakeActivator(t, map[revisionID]activationResult{ revisionID{"default", "rev1"}: activationResult{ @@ -53,7 +53,7 @@ func TestSingleRevision_SingleRequest_Success(t *testing.T) { } func TestSingleRevision_MultipleRequests_Success(t *testing.T) { - ep := Endpoint{"ip", 8080} + ep := Endpoint{FQDN: "ip", Port: 8080} f := newFakeActivator(t, map[revisionID]activationResult{ revisionID{"default", "rev1"}: activationResult{ @@ -82,8 +82,8 @@ func TestSingleRevision_MultipleRequests_Success(t *testing.T) { } func TestMultipleRevisions_MultipleRequests_Success(t *testing.T) { - ep1 := Endpoint{"ip1", 8080} - ep2 := Endpoint{"ip2", 8080} + ep1 := Endpoint{FQDN: "ip1", Port: 8080} + ep2 := Endpoint{FQDN: "ip2", Port: 8080} f := newFakeActivator(t, map[revisionID]activationResult{ revisionID{"default", "rev1"}: activationResult{ @@ -121,7 +121,7 @@ func TestMultipleRevisions_MultipleRequests_Success(t *testing.T) { } func TestMultipleRevisions_MultipleRequests_PartialSuccess(t *testing.T) { - ep1 := Endpoint{"ip1", 8080} + ep1 := Endpoint{FQDN: "ip1", Port: 8080} status2 := Status(http.StatusInternalServerError) error2 := fmt.Errorf("test error") f := newFakeActivator(t, @@ -191,7 +191,7 @@ func TestSingleRevision_MultipleRequests_FailureRecovery(t *testing.T) { } // Later activation succeeds - successEp := Endpoint{"ip", 8080} + successEp := Endpoint{FQDN: "ip", Port: 8080} successStatus := Status(0) f.responses[revisionID{"default", "rev1"}] = activationResult{ endpoint: successEp, @@ -216,7 +216,7 @@ func TestSingleRevision_MultipleRequests_FailureRecovery(t *testing.T) { } func TestShutdown_ReturnError(t *testing.T) { - ep := Endpoint{"ip", 8080} + ep := Endpoint{FQDN: "ip", Port: 8080} f := newFakeActivator(t, map[revisionID]activationResult{ revisionID{"default", "rev1"}: activationResult{ diff --git a/pkg/activator/prober.go b/pkg/activator/prober.go new file mode 100644 index 000000000000..5802980b6187 --- /dev/null +++ b/pkg/activator/prober.go @@ -0,0 +1,123 @@ +/* +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 activator + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "go.uber.org/zap" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + maxRetry = 60 + defaultPeriodSeconds = int32(1 * time.Second) + defaultTimeoutSeconds = int32(1 * time.Second) +) + +func verifyRevisionRoutability(revision *v1alpha1.Revision, endpoint *Endpoint, logger *zap.SugaredLogger) { + // Proceed only if user has a HTTPGet readinessProbe defined + if revision.Spec.Container.ReadinessProbe == nil || + revision.Spec.Container.ReadinessProbe.HTTPGet == nil { + endpoint.Verified = Unknown + return + } + + endpoint.Verified = Fail + probe := createHttpGetProbe(revision, *endpoint) + + // Number of seconds after the readiness probes are initiated + time.Sleep(time.Second * int32ToDuration(probe.InitialDelaySeconds)) + + retryCount := 1 + retryInterval := time.Second * int32ToDuration(probe.PeriodSeconds) + + for retryCount = 1; retryCount < maxRetry; retryCount++ { + ready, err := checkHttpGetProbe(probe, logger) + if err != nil { + logger.Errorf("error checking probe", zap.String("error", err.Error())) + } + + if ready { + endpoint.Verified = Pass + break + } + + // How often (in seconds) to perform the probe + time.Sleep(retryInterval) + } + + logger.Infof("took %d probe retries for readiness", retryCount, zap.Any("endpoint", endpoint)) +} + +// Function creates HTTP readiness probe for revision +func createHttpGetProbe(revision *v1alpha1.Revision, endpoint Endpoint) *v1.Probe { + probe := revision.Spec.Container.ReadinessProbe.DeepCopy() + probe.HTTPGet.Scheme = "http" + probe.HTTPGet.Host = endpoint.FQDN + probe.HTTPGet.Port.Type = intstr.Int + probe.HTTPGet.Port.IntVal = endpoint.Port + + if probe.TimeoutSeconds == 0 { + probe.TimeoutSeconds = defaultTimeoutSeconds + } + if probe.PeriodSeconds == 0 { + probe.PeriodSeconds = defaultPeriodSeconds + } + + return probe +} + +func checkHttpGetProbe(probe *v1.Probe, logger *zap.SugaredLogger) (ready bool, err error) { + if probe == nil { + return false, errors.New("probe cannot be nil") + } + + if probe.HTTPGet == nil { + return false, errors.New("probe HTTPGet cannot be nil") + } + + host := fmt.Sprintf("%s:%d", probe.HTTPGet.Host, probe.HTTPGet.Port.IntVal) + if err != nil { + return false, err + } + + probeUrl := url.URL{ + Scheme: string(probe.HTTPGet.Scheme), + Host: host, + Path: probe.HTTPGet.Path, + } + + logger.Debug("checking probe url: %s", probeUrl.String()) + + client := http.Client{Timeout: time.Second * int32ToDuration(probe.TimeoutSeconds)} + res, err := client.Get(probeUrl.String()) + if err != nil { + return false, err + } + + return res.StatusCode == http.StatusOK, nil +} + +func int32ToDuration(i int32) time.Duration { + return time.Duration(int64(i)) +} diff --git a/pkg/activator/prober_test.go b/pkg/activator/prober_test.go new file mode 100644 index 000000000000..3836f64f9113 --- /dev/null +++ b/pkg/activator/prober_test.go @@ -0,0 +1,325 @@ +/* +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 activator + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "testing" + "time" + + "github.com/knative/serving/pkg/apis/serving/v1alpha1" + "github.com/pkg/errors" + "go.uber.org/zap" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +type Matcher int + +const ( + Exact Matcher = 0 + Contains Matcher = 1 +) + +type testData struct { + probe *v1.Probe + want result + errorMatcher Matcher +} + +type result struct { + ready bool + err error +} + +func (r result) String() string { + var errorString string + if r.err != nil { + errorString = r.err.Error() + } + return fmt.Sprintf("result(ready: %t, err: %s)", r.ready, errorString) +} + +func (r result) Match(other result, matcher Matcher) bool { + if r.ready != other.ready { + return false + } + if (r.err == nil) && (other.err != nil) { + return false + } + if (r.err != nil) && (other.err == nil) { + return false + } + if r.err == nil && other.err == nil { + return true + } + + switch matcher { + case Exact: + return r.err.Error() == other.err.Error() + case Contains: + return strings.Contains(r.err.Error(), other.err.Error()) + default: + return false + } +} + +func TestCheckHttpGetReadiness(t *testing.T) { + server := getTestHttpServer(t) + defer server.Close() + + url, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("error parsing test server url(%s): %s", server.URL, err.Error()) + } + + logger := zap.NewNop().Sugar() + + testCases := generateHttpGetTestCases(t, url) + for testName, testData := range testCases { + ready, err := checkHttpGetProbe(testData.probe, logger) + got := result{ready, err} + + if !got.Match(testData.want, testData.errorMatcher) { + t.Fatalf("%s. want: %v. got: %v", testName, testData.want, got) + } + } +} + +func TestCheckHttpGetReadiness_RequestInitialDelay(t *testing.T) { + var requestTimestamp time.Time + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/health": + requestTimestamp = time.Now() + w.WriteHeader(http.StatusOK) + default: + w.WriteHeader(http.StatusNotFound) + } + }) + + server := httptest.NewServer(handler) + defer server.Close() + + url, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("error parsing test server url(%s): %s", server.URL, err.Error()) + } + + probe := getTestHttpGetProbe(t, url) + probe.InitialDelaySeconds = 2 + + revision := &v1alpha1.Revision{ + Spec: v1alpha1.RevisionSpec{ + Container: v1.Container{ + ReadinessProbe: probe, + }, + }, + } + + urlPort, err := strconv.ParseInt(url.Port(), 10, 32) + if err != nil { + t.Fatalf("error parsing port(%s) : %s", url.Port(), err.Error()) + } + + endpoint := Endpoint{ + FQDN: url.Hostname(), + Port: int32(urlPort), + } + + invocationTimestamp := time.Now() + verifyRevisionRoutability(revision, &endpoint, zap.NewNop().Sugar()) + + if !endpoint.IsVerified() { + t.Fatalf("expected endpoint to be verified") + } + + gotInitialDelay := requestTimestamp.Sub(invocationTimestamp) + wantInitialDelay := time.Second * time.Duration(int64(probe.InitialDelaySeconds)) + if gotInitialDelay < wantInitialDelay { + t.Fatalf("less than expected initial delay. got: %v, want: %v", gotInitialDelay, wantInitialDelay) + } +} + +func TestCheckHttpGetReadiness_RequestRetryInterval(t *testing.T) { + requestRetryCount := 0 + expectedRetryCount := 2 + + var requestTimestamps []time.Time + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/health": + requestRetryCount++ + requestTimestamps = append(requestTimestamps, time.Now()) + if requestRetryCount == expectedRetryCount { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusNotFound) + } + default: + w.WriteHeader(http.StatusNotFound) + } + }) + + server := httptest.NewServer(handler) + defer server.Close() + + url, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("error parsing test server url(%s): %s", server.URL, err.Error()) + } + + probe := getTestHttpGetProbe(t, url) + probe.PeriodSeconds = 2 + + revision := &v1alpha1.Revision{ + Spec: v1alpha1.RevisionSpec{ + Container: v1.Container{ + ReadinessProbe: probe, + }, + }, + } + + urlPort, err := strconv.ParseInt(url.Port(), 10, 32) + if err != nil { + t.Fatalf("error parsing port(%s) : %s", url.Port(), err.Error()) + } + + endpoint := Endpoint{ + FQDN: url.Hostname(), + Port: int32(urlPort), + } + + verifyRevisionRoutability(revision, &endpoint, zap.NewNop().Sugar()) + if !endpoint.IsVerified() { + t.Fatalf("expected endpoint to be verified") + } + + if requestRetryCount != expectedRetryCount { + t.Fatalf("retry count mismatch. got: %d. want: %d", requestRetryCount, expectedRetryCount) + } + + gotRetryDuration := requestTimestamps[1].Sub(requestTimestamps[0]) + wantRetryDuration := time.Duration(int64(probe.PeriodSeconds)) + if gotRetryDuration < wantRetryDuration { + t.Fatalf("less than expected duration. got: %#v. want: %#v", gotRetryDuration, wantRetryDuration) + } +} + +func TestCheckHttpGetReadiness_ClientTimesOut(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/health": + responseDelayDuration := 2 * time.Second + time.Sleep(responseDelayDuration) + w.WriteHeader(http.StatusOK) + default: + w.WriteHeader(http.StatusNotFound) + } + }) + + server := httptest.NewServer(handler) + defer server.Close() + + url, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("error parsing test server url(%s): %s", server.URL, err.Error()) + } + + probe := getTestHttpGetProbe(t, url) + probe.TimeoutSeconds = 1 + ready, err := checkHttpGetProbe(probe, zap.NewNop().Sugar()) + + got := result{ready, err} + want := result{false, errors.New("request canceled")} + if !got.Match(want, Contains) { + t.Fatalf("health server request times out. want: %v. got: %v", want, got) + } +} + +func generateHttpGetTestCases(t *testing.T, url *url.URL) (testCases map[string]testData) { + t.Helper() + + testCases = make(map[string]testData) + + error := errors.New("probe cannot be nil") + testCases["probe is nil"] = testData{nil, result{false, error}, Exact} + + error = errors.New("probe HTTPGet cannot be nil") + testCases["probe HTTPGet is nil"] = testData{&v1.Probe{}, result{false, error}, Exact} + + probe := getTestHttpGetProbe(t, url) + testCases["probe is not nil"] = testData{probe, result{true, nil}, Exact} + + badProbe := probe.DeepCopy() + badProbe.HTTPGet.Host = "bad_host_name" + error = errors.New("no such host") + testCases["probe with bad host name"] = testData{badProbe, result{false, error}, Contains} + + badProbe = probe.DeepCopy() + badProbe.HTTPGet.Path = "bad_host_path" + testCases["probe with bad host path"] = testData{badProbe, result{false, nil}, Exact} + + return testCases +} + +func getTestHttpServer(t *testing.T) *httptest.Server { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/health": + w.WriteHeader(http.StatusOK) + default: + w.WriteHeader(http.StatusNotFound) + } + }) + return httptest.NewServer(handler) +} + +func getTestHttpGetProbe(t *testing.T, url *url.URL) *v1.Probe { + t.Helper() + + return &v1.Probe{ + Handler: v1.Handler{ + HTTPGet: &v1.HTTPGetAction{ + Host: url.Hostname(), + Path: "/health", + Scheme: v1.URISchemeHTTP, + Port: getProbePort(t, url), + }, + }, + } +} + +func getProbePort(t *testing.T, url *url.URL) (probePort intstr.IntOrString) { + t.Helper() + + urlPort, err := strconv.ParseInt(url.Port(), 10, 32) + if err != nil { + t.Fatalf("error parsing port(%s) : %s", url.Port(), err.Error()) + } + probePort = intstr.IntOrString{ + Type: intstr.Int, + IntVal: int32(urlPort), + } + return probePort +} diff --git a/pkg/activator/revision.go b/pkg/activator/revision.go index c08c829e1ba9..b76a5ee4e7b3 100644 --- a/pkg/activator/revision.go +++ b/pkg/activator/revision.go @@ -138,6 +138,9 @@ func (r *revisionActivator) ActiveEndpoint(namespace, name string) (end Endpoint FQDN: fqdn, Port: port, } + + verifyRevisionRoutability(revision, &end, logger) + return end, 0, nil } diff --git a/pkg/activator/revision_test.go b/pkg/activator/revision_test.go index 07c93d0183de..fb3a6a6bb75a 100644 --- a/pkg/activator/revision_test.go +++ b/pkg/activator/revision_test.go @@ -45,7 +45,7 @@ func TestActiveEndpoint_Active_StaysActive(t *testing.T) { got, status, err := a.ActiveEndpoint(testNamespace, testRevision) - want := Endpoint{testServiceFQDN, 8080} + want := Endpoint{testServiceFQDN, 8080, Unknown} if got != want { t.Errorf("Wrong endpoint. Want %+v. Got %+v.", want, got) } @@ -68,7 +68,7 @@ func TestActiveEndpoint_Reserve_BecomesActive(t *testing.T) { got, status, err := a.ActiveEndpoint(testNamespace, testRevision) - want := Endpoint{testServiceFQDN, 8080} + want := Endpoint{testServiceFQDN, 8080, Unknown} if got != want { t.Errorf("Wrong endpoint. Want %+v. Got %+v.", want, got) } @@ -144,7 +144,7 @@ func TestActiveEndpoint_Reserve_WaitsForReady(t *testing.T) { time.Sleep(3 * time.Second) select { case result := <-ch: - want := Endpoint{testServiceFQDN, 8080} + want := Endpoint{testServiceFQDN, 8080, Unknown} if result.endpoint != want { t.Errorf("Unexpected endpoint. Want %+v. Got %+v.", want, result.endpoint) } From 01fe640090a8b0fb61341845136ff8ec7abf647f Mon Sep 17 00:00:00 2001 From: Sukhil Suresh Date: Tue, 24 Jul 2018 15:10:02 -0400 Subject: [PATCH 2/2] Add e2e test for issue #1448 * Add autoscaling e2e test for the case when user has defined HTTPGet readinessProbe * Increase timeout for execution of e2e test to 20m instead of using the default 10m --- test/crd.go | 14 ++++++ test/e2e-tests.sh | 2 +- test/e2e/autoscale_test.go | 48 +++++++++++++++++---- test/e2e/test_images/autoscale/autoscale.go | 12 +++++- 4 files changed, 65 insertions(+), 11 deletions(-) diff --git a/test/crd.go b/test/crd.go index 06bda039d253..d4ab4b9217a3 100644 --- a/test/crd.go +++ b/test/crd.go @@ -101,6 +101,20 @@ func Configuration(namespace string, names ResourceNames, imagePath string) *v1a } } +func ConfigurationWithHTTPGetReadinessProbe(namespace string, names ResourceNames, imagePath string, healthPath string) *v1alpha1.Configuration { + probe := &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: healthPath, + }, + }, + } + + configuration := Configuration(namespace, names, imagePath) + configuration.Spec.RevisionTemplate.Spec.Container.ReadinessProbe = probe + return configuration +} + func ConfigurationWithBuild(namespace string, names ResourceNames, build *buildv1alpha1.BuildSpec, imagePath string) *v1alpha1.Configuration { return &v1alpha1.Configuration{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index 8f3a7acad583..2e58d12e3857 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -106,7 +106,7 @@ function run_e2e_tests() { kubectl label namespace $2 istio-injection=enabled --overwrite local options="" (( EMIT_METRICS )) && options="-emitmetrics" - report_go_test -v -tags=e2e -count=1 ./test/$1 -dockerrepo gcr.io/knative-tests/test-images/$1 ${options} + report_go_test -v -tags=e2e -count=1 -timeout=20m ./test/$1 -dockerrepo gcr.io/knative-tests/test-images/$1 ${options} local result=$? [[ ${result} -ne 0 ]] && dump_cluster_state diff --git a/test/e2e/autoscale_test.go b/test/e2e/autoscale_test.go index 3b690e4788d0..abca3a5529fe 100644 --- a/test/e2e/autoscale_test.go +++ b/test/e2e/autoscale_test.go @@ -110,28 +110,60 @@ func tearDown(clients *test.Clients, names test.ResourceNames, logger *zap.Sugar TearDown(clients, names, logger) } -func TestAutoscaleUpDownUp(t *testing.T) { +func TestAutoscaleUpDownUp_WithoutReadinessProbe(t *testing.T) { //add test case specific name to its own logger - logger := test.GetContextLogger("TestAutoscaleUpDownUp") + logger := test.GetContextLogger("TestAutoscaleUpDownUp_WithoutReadinessProbe") clients := setup(t, logger) - imagePath := strings.Join( - []string{ - test.Flags.DockerRepo, - "autoscale"}, - "/") + imagePath := strings.Join([]string{test.Flags.DockerRepo, "autoscale"}, "/") logger.Infof("Creating a new Route and Configuration") names, err := CreateRouteAndConfig(clients, logger, imagePath) if err != nil { t.Fatalf("Failed to create Route and Configuration: %v", err) } + + test.CleanupOnInterrupt(func() { tearDown(clients, names, logger) }, logger) + defer tearDown(clients, names, logger) + + testAutoscaleUpDownUp(t, logger, clients, names) +} + +func TestAutoscaleUpDownUp_WithReadinessProbe(t *testing.T) { + //add test case specific name to its own logger + logger := test.GetContextLogger("TestAutoscaleUpDownUp_WithReadinessProbe") + + clients := setup(t, logger) + imagePath := strings.Join([]string{test.Flags.DockerRepo, "autoscale"}, "/") + + logger.Infof("Creating a new Route and Configuration") + + names := test.ResourceNames{ + Config: test.AppendRandomString(configName, logger), + Route: test.AppendRandomString(routeName, logger), + } + + configuration := test.ConfigurationWithHTTPGetReadinessProbe(test.Flags.Namespace, names, imagePath, "/health") + _, err := clients.Configs.Create(configuration) + if err != nil { + t.Fatalf("Failed to create Configuration: %v", err) + } + + _, err = clients.Routes.Create(test.Route(test.Flags.Namespace, names)) + if err != nil { + t.Fatalf("Failed to create Route: %v", err) + } + test.CleanupOnInterrupt(func() { tearDown(clients, names, logger) }, logger) defer tearDown(clients, names, logger) + testAutoscaleUpDownUp(t, logger, clients, names) +} + +func testAutoscaleUpDownUp(t *testing.T, logger *zap.SugaredLogger, clients *test.Clients, names test.ResourceNames) { logger.Infof(`When the Revision can have traffic routed to it, the Route is marked as Ready.`) - err = test.WaitForRouteState( + err := test.WaitForRouteState( clients.Routes, names.Route, test.IsRouteReady, diff --git a/test/e2e/test_images/autoscale/autoscale.go b/test/e2e/test_images/autoscale/autoscale.go index 0576e1c4e854..4466d6276354 100644 --- a/test/e2e/test_images/autoscale/autoscale.go +++ b/test/e2e/test_images/autoscale/autoscale.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "log" "math" "net/http" ) @@ -71,7 +72,7 @@ func primes(N int) []int { return primes } -func handler(w http.ResponseWriter, r *http.Request) { +func defaultHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) p := primes(40000000) @@ -79,7 +80,14 @@ func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "The largest prime under 40000000 is %d. Enjoy your noodles!", largest) } +func healthHandler(w http.ResponseWriter, r *http.Request) { + log.Print("autoscale app received a health request.") + w.WriteHeader(http.StatusOK) +} + func main() { - http.HandleFunc("/", handler) + log.Print("autoscale app started.") + http.HandleFunc("/health", healthHandler) + http.HandleFunc("/", defaultHandler) http.ListenAndServe(":8080", nil) }