diff --git a/go.mod b/go.mod index e57c441f10ca..8aa96dd72b4c 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 - golang.org/x/net v0.46.0 golang.org/x/sync v0.17.0 golang.org/x/sys v0.37.0 golang.org/x/time v0.10.0 @@ -147,6 +146,7 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect diff --git a/pkg/apis/serving/register.go b/pkg/apis/serving/register.go index 7fc51964f1e7..4618289f9ee2 100644 --- a/pkg/apis/serving/register.go +++ b/pkg/apis/serving/register.go @@ -137,6 +137,9 @@ const ( // QueueSidecarEphemeralStorageResourceLimitAnnotationKey is the explicit value of the ephemeral storage limit for queue-proxy's limit resources QueueSidecarEphemeralStorageResourceLimitAnnotationKey = "queue.sidecar." + GroupName + "/ephemeral-storage-resource-limit" + // QueueSidecarImageAnnotationKey is the annotation key for specifying a custom queue-proxy sidecar image + QueueSidecarImageAnnotationKey = "queue.sidecar." + GroupName + "/image" + // VisibilityClusterLocal is the label value for VisibilityLabelKey // that will result to the Route/KService getting a cluster local // domain suffix. @@ -199,6 +202,9 @@ var ( QueueSidecarEphemeralStorageResourceLimitAnnotation = kmap.KeyPriority{ QueueSidecarEphemeralStorageResourceLimitAnnotationKey, } + QueueSidecarImageAnnotation = kmap.KeyPriority{ + QueueSidecarImageAnnotationKey, + } ProgressDeadlineAnnotation = kmap.KeyPriority{ ProgressDeadlineAnnotationKey, } diff --git a/pkg/apis/serving/v1/revision_validation.go b/pkg/apis/serving/v1/revision_validation.go index def2570fef70..bb781fda72e2 100644 --- a/pkg/apis/serving/v1/revision_validation.go +++ b/pkg/apis/serving/v1/revision_validation.go @@ -71,6 +71,7 @@ func (rts *RevisionTemplateSpec) Validate(ctx context.Context) *apis.FieldError // it follows the requirements on the name. errs = errs.Also(validateRevisionName(ctx, rts.Name, rts.GenerateName)) errs = errs.Also(validateQueueSidecarResourceAnnotations(rts.Annotations).ViaField("metadata.annotations")) + errs = errs.Also(validateQueueSidecarImageAnnotation(rts.Annotations).ViaField("metadata.annotations")) errs = errs.Also(validateProgressDeadlineAnnotation(rts.Annotations).ViaField("metadata.annotations")) return errs } @@ -217,6 +218,21 @@ func validateQueueSidecarResourceAnnotations(m map[string]string) *apis.FieldErr return errs } +// validateQueueSidecarImageAnnotation validates the queue sidecar image annotation. +func validateQueueSidecarImageAnnotation(m map[string]string) *apis.FieldError { + if k, v, ok := serving.QueueSidecarImageAnnotation.Get(m); ok { + // Basic validation: image should not be empty + if v == "" { + return apis.ErrInvalidValue(v, apis.CurrentField).ViaKey(k) + } + // Additional validation: image should not contain spaces (basic check) + if strings.Contains(v, " ") { + return apis.ErrInvalidValue(v, apis.CurrentField).ViaKey(k) + } + } + return nil +} + // ValidateProgressDeadlineAnnotation validates the revision progress deadline annotation. func validateProgressDeadlineAnnotation(annos map[string]string) *apis.FieldError { if k, v, _ := serving.ProgressDeadlineAnnotation.Get(annos); v != "" { diff --git a/pkg/apis/serving/v1/revision_validation_test.go b/pkg/apis/serving/v1/revision_validation_test.go index 9373923ad229..0338cbb16eb3 100644 --- a/pkg/apis/serving/v1/revision_validation_test.go +++ b/pkg/apis/serving/v1/revision_validation_test.go @@ -1189,6 +1189,51 @@ func TestValidateQueueSidecarAnnotation(t *testing.T) { } } +func TestValidateQueueSidecarImageAnnotation(t *testing.T) { + cases := []struct { + name string + annotation map[string]string + expectErr *apis.FieldError + }{{ + name: "valid queue sidecar image", + annotation: map[string]string{ + serving.QueueSidecarImageAnnotationKey: "gcr.io/my-project/queue:v1.2.3", + }, + expectErr: nil, + }, { + name: "empty queue sidecar image", + annotation: map[string]string{ + serving.QueueSidecarImageAnnotationKey: "", + }, + expectErr: &apis.FieldError{ + Message: "invalid value: ", + Paths: []string{fmt.Sprintf("[%s]", serving.QueueSidecarImageAnnotationKey)}, + }, + }, { + name: "queue sidecar image with spaces", + annotation: map[string]string{ + serving.QueueSidecarImageAnnotationKey: "invalid image:tag", + }, + expectErr: &apis.FieldError{ + Message: "invalid value: invalid image:tag", + Paths: []string{fmt.Sprintf("[%s]", serving.QueueSidecarImageAnnotationKey)}, + }, + }, { + name: "no annotation", + annotation: map[string]string{}, + expectErr: nil, + }} + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := validateQueueSidecarImageAnnotation(c.annotation) + if got, want := err.Error(), c.expectErr.Error(); got != want { + t.Errorf("Got: %q want: %q", got, want) + } + }) + } +} + func TestValidateTimeoutSecond(t *testing.T) { cases := []struct { name string diff --git a/pkg/queue/health/probe.go b/pkg/queue/health/probe.go index 5b73f5a8dae5..2dd487599575 100644 --- a/pkg/queue/health/probe.go +++ b/pkg/queue/health/probe.go @@ -77,114 +77,102 @@ func TCPProbe(config TCPProbeConfigOptions) error { return nil } -// Returns a transport that uses HTTP/2 if it's known to be supported, and otherwise -// spoofs the request & response versions to HTTP/1.1. -func autoDowngradingTransport(opt HTTPProbeConfigOptions) http.RoundTripper { - t := pkgnet.NewProberTransport() +// proberTransport is a reusable transport optimized for health check probes. +// The transport auto-selects between HTTP/1.1 and H2C based on the request's ProtoMajor field. +var proberTransport = pkgnet.NewProberTransport() + +// cleartextProbeTransport returns a RoundTripper that wraps the prober transport +// and sets the appropriate protocol hint for the given protocol version. +// protoMajor should be 1 for HTTP/1.1 or 2 for H2C. +func cleartextProbeTransport(protoMajor int) http.RoundTripper { return pkgnet.RoundTripperFunc(func(r *http.Request) (*http.Response, error) { - // If the user-container can handle HTTP2, we pass through the request as-is. - // We have to set r.ProtoMajor to 2, since auto transport relies solely on it - // to decide whether to use h2c or http1.1. - if opt.MaxProtoMajor == 2 { - r.ProtoMajor = 2 - return t.RoundTrip(r) - } - - // Otherwise, save the request HTTP version and downgrade it - // to HTTP1 before sending. - version := r.ProtoMajor - r.ProtoMajor = 1 - resp, err := t.RoundTrip(r) - - // Restore the request & response HTTP version before sending back. - r.ProtoMajor = version - if resp != nil { - resp.ProtoMajor = version - } - return resp, err + // Set the protocol hint for the auto-selecting prober transport + r.ProtoMajor = protoMajor + return proberTransport.RoundTrip(r) }) } -var transport = func() *http.Transport { - t := http.DefaultTransport.(*http.Transport).Clone() - t.TLSClientConfig.InsecureSkipVerify = true - return t -}() - func getURL(config HTTPProbeConfigOptions) (*url.URL, error) { return url.Parse(string(config.Scheme) + "://" + net.JoinHostPort(config.Host, config.Port.String()) + "/" + strings.TrimPrefix(config.Path, "/")) } -// http2UpgradeProbe checks that an HTTP with HTTP2 upgrade request -// connection can be understood by the address. -// Returns: the highest known proto version supported (0 if not ready or error) -func http2UpgradeProbe(config HTTPProbeConfigOptions) (int, error) { +// detectHTTPProtocolVersion detects the highest HTTP protocol version supported by the server. +// Attempts H2C first, falls back to HTTP/1 if that fails or returns non-ready status. +// Returns 2 (H2C ready), 1 (HTTP/1 ready), or 0 (not ready/error). +func detectHTTPProtocolVersion(config HTTPProbeConfigOptions) (int, error) { + // http.Client does not fallback to from h2c to http1, we need to make two requests ourselves httpClient := &http.Client{ - Transport: transport, Timeout: config.Timeout, + Transport: cleartextProbeTransport(2), } + url, err := getURL(config) if err != nil { return 0, fmt.Errorf("error constructing probe url %w", err) } + + // do a simple GET request as Kubernetes does, avoid non-standard methods like HEAD //nolint:noctx // timeout is specified on the http.Client above - req, err := http.NewRequest(http.MethodOptions, url.String(), nil) + req, err := http.NewRequest(http.MethodGet, url.String(), nil) if err != nil { return 0, fmt.Errorf("error constructing probe request %w", err) } + req.Header.Add(netheader.UserAgentKey, netheader.KubeProbeUAPrefix+config.KubeMajor+"/"+config.KubeMinor) - // An upgrade will need to have at least these 3 headers. - // This is documented at https://tools.ietf.org/html/rfc7540#section-3.2 - req.Header.Add("Connection", "Upgrade, HTTP2-Settings") - req.Header.Add("Upgrade", "h2c") - req.Header.Add("HTTP2-Settings", "") + if res, err := httpClient.Do(req); err == nil { + defer res.Body.Close() - req.Header.Add(netheader.UserAgentKey, netheader.KubeProbeUAPrefix+config.KubeMajor+"/"+config.KubeMinor) + // ignore non-ready http2 responses and continue with http1, http2 might not be properly supported + if isHTTPProbeReady(res) { + return 2, nil + } + } + // fallback to check http1 + httpClient.Transport = cleartextProbeTransport(1) res, err := httpClient.Do(req) if err != nil { return 0, err } - defer res.Body.Close() - maxProto := 0 + defer res.Body.Close() - if isHTTPProbeUpgradingToH2C(res) { - maxProto = 2 - } else if isHTTPProbeReady(res) { - maxProto = 1 - } else { - return maxProto, fmt.Errorf("HTTP probe did not respond Ready, got status code: %d", res.StatusCode) + if isHTTPProbeReady(res) { + return 1, nil } - return maxProto, nil + return 0, fmt.Errorf("HTTP probe did not respond Ready, got status code: %d", res.StatusCode) } // HTTPProbe checks that HTTP connection can be established to the address. func HTTPProbe(config HTTPProbeConfigOptions) error { if config.MaxProtoMajor == 0 { // If we don't know if the connection supports HTTP2, we will try it. - // Once we get a non-error response, we won't try again. + // NOTE: the result is not cached right now, every probe attempts http2 detection again + // If maxProto is 0, container is not ready, so we don't know whether http2 is supported. // If maxProto is 1, we know we're ready, but we also can't upgrade, so just return. // If maxProto is 2, we know we can upgrade to http2 - maxProto, err := http2UpgradeProbe(config) + maxProto, err := detectHTTPProtocolVersion(config) if err != nil { - return fmt.Errorf("failed to run HTTP2 upgrade probe with error: %w", err) + return fmt.Errorf("failed to run HTTP protocol probe with error: %w", err) } config.MaxProtoMajor = maxProto if config.MaxProtoMajor == 1 { + // probe already passed for HTTP/1.1 during auto detection, return early return nil } } + httpClient := &http.Client{ - Transport: autoDowngradingTransport(config), + Transport: cleartextProbeTransport(config.MaxProtoMajor), Timeout: config.Timeout, } url, err := getURL(config) if err != nil { return fmt.Errorf("error constructing probe url %w", err) } + //nolint:noctx // timeout is specified on the http.Client above req, err := http.NewRequest(http.MethodGet, url.String(), nil) if err != nil { @@ -216,11 +204,6 @@ func HTTPProbe(config HTTPProbeConfigOptions) error { return nil } -// isHTTPProbeUpgradingToH2C checks whether the server indicates it's switching to h2c protocol. -func isHTTPProbeUpgradingToH2C(res *http.Response) bool { - return res.StatusCode == http.StatusSwitchingProtocols && res.Header.Get("Upgrade") == "h2c" -} - // isHTTPProbeReady checks whether we received a successful Response func isHTTPProbeReady(res *http.Response) bool { // response status code between 200-399 indicates success diff --git a/pkg/queue/health/probe_test.go b/pkg/queue/health/probe_test.go index 0aef0af06a02..94e665784d7e 100644 --- a/pkg/queue/health/probe_test.go +++ b/pkg/queue/health/probe_test.go @@ -23,13 +23,10 @@ import ( "net/http/httptest" "net/url" "strings" - "sync/atomic" "testing" "time" "github.com/google/go-cmp/cmp" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" "google.golang.org/grpc" "google.golang.org/grpc/health/grpc_health_v1" corev1 "k8s.io/api/core/v1" @@ -135,92 +132,6 @@ func TestHTTPProbeSuccess(t *testing.T) { } } -func TestHTTPProbeNoAutoHTTP2IfDisabled(t *testing.T) { - h2cHeaders := map[string]string{ - "Connection": "Upgrade, HTTP2-Settings", - "Upgrade": "h2c", - } - expectedPath := "/health" - - var callCount atomic.Int32 - server := newH2cTestServer(t, func(w http.ResponseWriter, r *http.Request) { - count := callCount.Add(1) - if count == 1 { - // This is the h2c handshake, we won't do anything. - for key, value := range h2cHeaders { - if r.Header.Get(key) == value { - t.Errorf("Key %v = %v was NOT supposed to be present in the request", key, value) - } - } - } else { - t.Errorf("Handler should only have one calls, this is call %d", count) - } - }) - - action := newHTTPGetAction(t, server.URL) - action.Path = expectedPath - - config := HTTPProbeConfigOptions{ - Timeout: time.Second, - HTTPGetAction: action, - MaxProtoMajor: 1, - } - if err := HTTPProbe(config); err != nil { - t.Error("Expected probe to succeed but it failed with", err) - } - if count := callCount.Load(); count != 1 { - t.Errorf("Unexpected call count %d", count) - } -} - -func TestHTTPProbeAutoHTTP2(t *testing.T) { - t.Skip("The test and the underlying behavior needs hardening, see #10962") - - h2cHeaders := map[string]string{ - "Connection": "Upgrade, HTTP2-Settings", - "Upgrade": "h2c", - } - expectedPath := "/health" - var callCount atomic.Int32 - - server := newH2cTestServer(t, func(w http.ResponseWriter, r *http.Request) { - count := callCount.Add(1) - switch count { - case 1: - // This is the h2c handshake, we won't do anything. - for key, value := range h2cHeaders { - if r.Header.Get(key) != value { - t.Errorf("Key %v = %v was supposed to be present in the request", key, value) - } - } - case 2: - // This is the expected call. It should not have any of the h2c upgrade stuff, since the h2c test server will handle that for us. - for key, value := range h2cHeaders { - if r.Header.Get(key) == value { - t.Errorf("Key %v = %v was NOT supposed to be present in the request", key, value) - } - } - default: - t.Errorf("Handler should only have two calls, this is call %d", count) - } - }) - - action := newHTTPGetAction(t, server.URL) - action.Path = expectedPath - - config := HTTPProbeConfigOptions{ - Timeout: time.Second, - HTTPGetAction: action, - MaxProtoMajor: 0, - } - if err := HTTPProbe(config); err != nil { - t.Error("Expected probe to succeed but it failed with", err) - } - if count := callCount.Load(); count != 2 { - t.Errorf("Unexpected call count %d", count) - } -} - func TestHTTPSchemeProbeSuccess(t *testing.T) { server := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -289,6 +200,114 @@ func TestHTTPProbeResponseErrorFailure(t *testing.T) { } } +func TestHTTP2ProtocolProbe(t *testing.T) { + tests := []struct { + name string + enableHTTP1 bool + enableH2C bool + statusHTTP1 int + statusHTTP2 int + expectedProto int + expectError bool + }{ + { + name: "server supports H2C only and ready", + enableHTTP1: false, + enableH2C: true, + statusHTTP1: 0, + statusHTTP2: http.StatusOK, + expectedProto: 2, + expectError: false, + }, + { + name: "server supports HTTP/1 only and ready", + enableHTTP1: true, + enableH2C: false, + statusHTTP1: http.StatusOK, + statusHTTP2: 0, + expectedProto: 1, + expectError: false, + }, + { + name: "server supports both H2C and HTTP/1 (should prefer H2C)", + enableHTTP1: true, + enableH2C: true, + statusHTTP1: http.StatusOK, + statusHTTP2: http.StatusOK, + expectedProto: 2, + expectError: false, + }, + { + name: "server supports H2C (status 503) and HTTP/1 (status 200)", + enableHTTP1: true, + enableH2C: true, + statusHTTP1: http.StatusOK, + statusHTTP2: http.StatusServiceUnavailable, + expectedProto: 1, + expectError: false, + }, + { + name: "server supports HTTP/1 but returns not ready (503)", + enableHTTP1: true, + enableH2C: false, + statusHTTP1: http.StatusServiceUnavailable, + expectedProto: 0, + expectError: true, + }, + { + name: "server supports both but returns not ready (500)", + enableHTTP1: true, + enableH2C: true, + statusHTTP1: http.StatusInternalServerError, + statusHTTP2: http.StatusInternalServerError, + expectedProto: 0, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup server with specified protocols + protocols := &http.Protocols{} + protocols.SetHTTP1(tt.enableHTTP1) + protocols.SetUnencryptedHTTP2(tt.enableH2C) + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + status := tt.statusHTTP1 + if r.ProtoMajor == 2 { + status = tt.statusHTTP2 + } + if status > 0 { + w.WriteHeader(status) + } + })) + server.Config.Protocols = protocols + server.Start() + t.Cleanup(server.Close) + + action := newHTTPGetAction(t, server.URL) + config := HTTPProbeConfigOptions{ + Timeout: time.Second, + HTTPGetAction: action, + KubeMajor: "1", + KubeMinor: "28", + } + + proto, err := detectHTTPProtocolVersion(config) + + if tt.expectError && err == nil { + t.Errorf("Expected error but got none") + } + if !tt.expectError && err != nil { + t.Errorf("Expected no error but got: %v", err) + } + if proto != tt.expectedProto { + t.Errorf("Expected proto %d, got %d", tt.expectedProto, proto) + } + }) + } +} + func TestGRPCProbeSuccess(t *testing.T) { // use ephemeral port to prevent port conflict lis, err := net.Listen("tcp", "127.0.0.1:0") @@ -324,17 +343,6 @@ func TestGRPCProbeSuccess(t *testing.T) { close(errChan) } -func newH2cTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { - h2s := &http2.Server{} - t.Helper() - server := httptest.NewServer(h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler(w, r) - }), h2s)) - t.Cleanup(server.Close) - - return server -} - func newTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/reconciler/revision/resources/queue.go b/pkg/reconciler/revision/resources/queue.go index a5754f8e4a32..3697e1aa8dae 100644 --- a/pkg/reconciler/revision/resources/queue.go +++ b/pkg/reconciler/revision/resources/queue.go @@ -348,9 +348,16 @@ func makeQueueContainer(rev *v1.Revision, cfg *config.Config) (*corev1.Container fullDuplexFeature, fullDuplexExists := rev.Annotations[apicfg.AllowHTTPFullDuplexFeatureKey] useQPResourceDefaults := cfg.Features.QueueProxyResourceDefaults == apicfg.Enabled + + // Determine the queue sidecar image: use annotation if present, otherwise use config default + queueImage := cfg.Deployment.QueueSidecarImage + if image, ok := rev.GetAnnotations()[serving.QueueSidecarImageAnnotationKey]; ok && image != "" { + queueImage = image + } + c := &corev1.Container{ Name: QueueContainerName, - Image: cfg.Deployment.QueueSidecarImage, + Image: queueImage, Resources: createQueueResources(cfg.Deployment, rev.GetAnnotations(), userContainer, useQPResourceDefaults), Ports: ports, StartupProbe: nil, diff --git a/pkg/reconciler/revision/resources/queue_test.go b/pkg/reconciler/revision/resources/queue_test.go index 1f7f05a4bc61..a9b67ee48344 100644 --- a/pkg/reconciler/revision/resources/queue_test.go +++ b/pkg/reconciler/revision/resources/queue_test.go @@ -488,6 +488,31 @@ func TestMakeQueueContainer(t *testing.T) { "QUEUE_SERVING_PORT": "8013", }) }), + }, { + name: "queue sidecar image from annotation", + rev: revision("bar", "foo", + withContainers(containers), + WithRevisionAnnotations(map[string]string{ + serving.QueueSidecarImageAnnotationKey: "custom-queue:v1.2.3", + })), + dc: deployment.Config{ + QueueSidecarImage: "default-queue:latest", + }, + want: queueContainer(func(c *corev1.Container) { + c.Image = "custom-queue:v1.2.3" + c.Env = env(map[string]string{}) + }), + }, { + name: "queue sidecar image from config when annotation not present", + rev: revision("bar", "foo", + withContainers(containers)), + dc: deployment.Config{ + QueueSidecarImage: "default-queue:latest", + }, + want: queueContainer(func(c *corev1.Container) { + c.Image = "default-queue:latest" + c.Env = env(map[string]string{}) + }), }} for _, test := range tests { diff --git a/test/e2e/http2_test.go b/test/e2e/http2_test.go index 97efe887fc31..b49bc8e04349 100644 --- a/test/e2e/http2_test.go +++ b/test/e2e/http2_test.go @@ -24,6 +24,7 @@ import ( "net/http" "testing" + v1 "k8s.io/api/core/v1" pkgTest "knative.dev/pkg/test" "knative.dev/pkg/test/spoof" rtesting "knative.dev/serving/pkg/testing/v1" @@ -49,7 +50,13 @@ func TestHelloHTTP2WithPortNameH2C(t *testing.T) { t.Log("Creating a new Service") - resources, err := v1test.CreateServiceReady(t, clients, &names, rtesting.WithNamedPort("h2c")) + resources, err := v1test.CreateServiceReady( + t, clients, &names, rtesting.WithNamedPort("h2c"), + rtesting.WithEnv(v1.EnvVar{ + Name: "RESPONSE", + Value: test.HelloHTTP2Text, + }), + ) if err != nil { t.Fatalf("Failed to create initial Service: %v: %v", names.Service, err) } diff --git a/test/test_images/hellohttp2/hellohttp2.go b/test/test_images/hellohttp2/hellohttp2.go index e1913c348864..424dff43a3f2 100644 --- a/test/test_images/hellohttp2/hellohttp2.go +++ b/test/test_images/hellohttp2/hellohttp2.go @@ -17,29 +17,43 @@ limitations under the License. package main import ( - "flag" "fmt" "log" "net/http" "os" - - "knative.dev/pkg/network" ) +func main() { + log.Print("hellohttp2 app started.") + + mux := http.NewServeMux() + mux.HandleFunc("/", handler) + mux.HandleFunc("/healthz", healthzHandler) + + protocols := &http.Protocols{} + protocols.SetHTTP1(true) + protocols.SetUnencryptedHTTP2(true) + + s := &http.Server{ + Addr: ":" + os.Getenv("PORT"), + Handler: mux, + Protocols: protocols, + } + log.Fatal(s.ListenAndServe()) +} + func handler(w http.ResponseWriter, r *http.Request) { - if r.ProtoMajor == 2 { - log.Print("hellohttp2 received an http2 request.") - fmt.Fprintln(w, "Hello, New World! How about donuts and coffee?") + log.Printf("Request: proto=%s method=%s path=%s", r.Proto, r.Method, r.URL.Path) + + if res, ok := os.LookupEnv("RESPONSE"); ok { + fmt.Fprint(w, res) } else { - log.Print("hellohttp2 received an HTTP 1.1 request.") - w.WriteHeader(http.StatusUpgradeRequired) + fmt.Fprintf(w, "proto=%s method=%s path=%s\n", r.Proto, r.Method, r.URL.Path) } } -func main() { - flag.Parse() - log.Print("hellohttp2 app started.") - - s := network.NewServer(":"+os.Getenv("PORT"), http.HandlerFunc(handler)) - log.Fatal(s.ListenAndServe()) +func healthzHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("Health check: proto=%s method=%s path=%s", r.Proto, r.Method, r.URL.Path) + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "OK") } diff --git a/test/test_images/hellohttp2/service.yaml b/test/test_images/hellohttp2/service.yaml index edc906245382..b0edba4b45c7 100644 --- a/test/test_images/hellohttp2/service.yaml +++ b/test/test_images/hellohttp2/service.yaml @@ -5,9 +5,17 @@ metadata: namespace: default spec: template: + metadata: + annotations: + # Example: Override the queue sidecar image with a custom image + # queue.sidecar.serving.knative.dev/image: ko://knative.dev/serving/cmd/queue spec: containers: - - image: ko://knative.dev/serving/test/test_images/hellohttp2 - ports: - - name: h2c - containerPort: 8080 + - image: ko://knative.dev/serving/test/test_images/hellohttp2 + ports: + - name: h2c + containerPort: 8080 + readinessProbe: + httpGet: + path: /healthz + port: 8080