internal/https: add health checking support and use it

CL 454935 broke the Kubernetes ingress by requiring IAP on health
checks. Move /healthz handling into internal/https, where it
automatically bypasses authentication and removes some duplicate trivial
implementations.

Unfortunately, GKE is not capable of inferring health check parameters
from a multi-container pod like relui, so we have to change our
BackendConfig. That sets off a yak shave -- I made the questionable
decision to use the same backend for all our IAP services, and the
coordinator doesn't currently support /healthz. Split all them up and
delete the devapp configuration I was using for testing way back in the
day.

Change-Id: I45e866d30508a07e9a805de70af731dd64c22d7f
Reviewed-on: https://go-review.googlesource.com/c/build/+/455215
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Heschi Kreinick <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/coordinator/deployment-prod.yaml b/cmd/coordinator/deployment-prod.yaml
index 15e6246..ddfaebe 100644
--- a/cmd/coordinator/deployment-prod.yaml
+++ b/cmd/coordinator/deployment-prod.yaml
@@ -67,7 +67,7 @@
   namespace: prod
   name: coordinator-internal-iap
   annotations:
-    cloud.google.com/backend-config: '{"default": "build-ingress-iap-backend"}'
+    cloud.google.com/backend-config: '{"default": "coordinator-iap-backend"}'
     cloud.google.com/neg: '{"ingress": false}'
     cloud.google.com/app-protocols: '{"https":"HTTP2"}'
 spec:
@@ -95,3 +95,15 @@
   selector:
     app: coordinator
   type: NodePort
+---
+apiVersion: cloud.google.com/v1
+kind: BackendConfig
+metadata:
+  namespace: prod
+  name: coordinator-iap-backend
+spec:
+  iap:
+    enabled: true
+    oauthclientCredentials:
+      secretName: iap-oauth
+  timeoutSec: 86400  # For long-running gomote RPCs. See https://go.dev/issue/56423.
diff --git a/cmd/relui/deployment-prod.yaml b/cmd/relui/deployment-prod.yaml
index e49eb8e..0f87e19 100644
--- a/cmd/relui/deployment-prod.yaml
+++ b/cmd/relui/deployment-prod.yaml
@@ -43,6 +43,11 @@
             - "--serving-files-base=gs://golang"
             - "--edge-cache-url=https://dl.google.com/go"
             - "--website-upload-url=https://go.dev/dl/upload"
+          readinessProbe:
+            httpGet:
+              path: /healthz
+              port: 444
+              scheme: HTTPS
           ports:
             - containerPort: 444
           env:
@@ -100,7 +105,7 @@
   namespace: prod
   name: relui-internal
   annotations:
-    cloud.google.com/backend-config: '{"default": "build-ingress-iap-backend"}'
+    cloud.google.com/backend-config: '{"default": "relui-iap-backend"}'
     cloud.google.com/neg: '{"ingress": false}'
     cloud.google.com/app-protocols: '{"https":"HTTP2"}'
 spec:
@@ -111,3 +116,19 @@
   selector:
     app: relui
   type: NodePort
+---
+apiVersion: cloud.google.com/v1
+kind: BackendConfig
+metadata:
+  namespace: prod
+  name: relui-iap-backend
+spec:
+  iap:
+    enabled: true
+    oauthclientCredentials:
+      secretName: iap-oauth
+  healthCheck:
+    timeoutSec: 10
+    checkIntervalSec: 15
+    type: HTTPS
+    requestPath: /healthz
diff --git a/deploy/build-ingress.yaml b/deploy/build-ingress.yaml
index bb22ac3..4df8635 100644
--- a/deploy/build-ingress.yaml
+++ b/deploy/build-ingress.yaml
@@ -18,13 +18,6 @@
     http:
       paths:
       - pathType: ImplementationSpecific
-        path: /owners
-        backend:
-          service:
-            name: devapp-internal-iap
-            port:
-              number: 444
-      - pathType: ImplementationSpecific
         path: /*
         backend:
           service:
@@ -137,26 +130,6 @@
     enabled: true
     responseCodeName: FOUND
 ---
-apiVersion: cloud.google.com/v1
-kind: BackendConfig
-metadata:
-  namespace: prod
-  name: build-ingress-iap-backend
-spec:
-  iap:
-    enabled: true
-    oauthclientCredentials:
-      secretName: iap-oauth
-  timeoutSec: 86400  # For long-running gomote RPCs. See https://go.dev/issue/56423.
----
-apiVersion: cloud.google.com/v1
-kind: BackendConfig
-metadata:
-  namespace: prod
-  name: build-ingress-maintnerd-backend
-spec:
-  timeoutSec: 60  # For long-poll support on the /logs endpoint. See go.dev/issue/53569.
----
 apiVersion: networking.gke.io/v1
 kind: ManagedCertificate
 metadata:
diff --git a/devapp/deployment-prod.yaml b/devapp/deployment-prod.yaml
index a7f61c4..4dc5c88 100644
--- a/devapp/deployment-prod.yaml
+++ b/devapp/deployment-prod.yaml
@@ -46,24 +46,6 @@
 kind: Service
 metadata:
   namespace: prod
-  name: devapp-internal-iap
-  annotations:
-    cloud.google.com/backend-config: '{"default": "build-ingress-iap-backend"}'
-    cloud.google.com/neg: '{"ingress": false}'
-    cloud.google.com/app-protocols: '{"https":"HTTP2"}'
-spec:
-  ports:
-    - port: 444
-      targetPort: 444
-      name: https
-  selector:
-    app: devapp
-  type: NodePort
----
-apiVersion: v1
-kind: Service
-metadata:
-  namespace: prod
   name: devapp-internal
   annotations:
     cloud.google.com/neg: '{"ingress": false}'
diff --git a/devapp/server.go b/devapp/server.go
index dcfd5b6..ec92619 100644
--- a/devapp/server.go
+++ b/devapp/server.go
@@ -64,7 +64,6 @@
 		userMapping: map[int]*maintner.GitHubUser{},
 	}
 	s.mux.Handle("/", http.FileServer(http.Dir(s.staticDir)))
-	s.mux.HandleFunc("/healthz", handleHealthz)
 	s.mux.HandleFunc("/favicon.ico", s.handleFavicon)
 	s.mux.HandleFunc("/release", s.withTemplate("/release.tmpl", s.handleRelease))
 	s.mux.HandleFunc("/reviews", s.withTemplate("/reviews.tmpl", s.handleReviews))
@@ -203,11 +202,6 @@
 	return issues
 }
 
-func handleHealthz(w http.ResponseWriter, r *http.Request) {
-	w.WriteHeader(http.StatusOK)
-	w.Write([]byte("ok"))
-}
-
 func (s *server) handleFavicon(w http.ResponseWriter, r *http.Request) {
 	// Need to specify content type for consistent tests, without this it's
 	// determined from mime.types on the box the test is running on
diff --git a/internal/https/https.go b/internal/https/https.go
index 767269e..81f2512 100644
--- a/internal/https/https.go
+++ b/internal/https/https.go
@@ -34,6 +34,8 @@
 	SelfSignedAddr string
 	// If non-empty, listen on this address and serve HTTP.
 	HTTPAddr string
+	// If non-empty, respond unconditionally with 200 OK to requests on this path.
+	HealthPath string
 }
 
 var DefaultOptions = &Options{}
@@ -47,6 +49,7 @@
 	set.StringVar(&DefaultOptions.AutocertAddr, "listen-https-autocert", "", "if non-empty, listen on this address and serve HTTPS using a Let's Encrypt cert stored in autocert-bucket")
 	set.StringVar(&DefaultOptions.SelfSignedAddr, "listen-https-selfsigned", "", "if non-empty, listen on this address and serve HTTPS using a self-signed cert")
 	set.StringVar(&DefaultOptions.HTTPAddr, "listen-http", "", "if non-empty, listen on this address and serve HTTP")
+	set.StringVar(&DefaultOptions.HealthPath, "health-path", "/healthz", "if non-empty, respond unconditionally with 200 OK to requests on this path")
 }
 
 // ListenAndServe runs the servers configured by DefaultOptions. It always
@@ -60,6 +63,18 @@
 func ListenAndServeOpts(ctx context.Context, handler http.Handler, opts *Options) error {
 	errc := make(chan error, 3)
 
+	if opts.HealthPath != "" {
+		wrapped := handler
+		handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if r.URL.Path == opts.HealthPath {
+				w.WriteHeader(http.StatusOK)
+				w.Write([]byte("ok"))
+			} else {
+				wrapped.ServeHTTP(w, r)
+			}
+		})
+	}
+
 	if opts.HTTPAddr != "" {
 		server := &http.Server{Addr: opts.HTTPAddr, Handler: handler}
 		defer server.Close()
diff --git a/maintner/maintnerd/deployment-prod.yaml b/maintner/maintnerd/deployment-prod.yaml
index f2d3ec9..2b3266e 100644
--- a/maintner/maintnerd/deployment-prod.yaml
+++ b/maintner/maintnerd/deployment-prod.yaml
@@ -74,3 +74,11 @@
   selector:
     app: maintnerd
   type: NodePort
+---
+apiVersion: cloud.google.com/v1
+kind: BackendConfig
+metadata:
+  namespace: prod
+  name: build-ingress-maintnerd-backend
+spec:
+  timeoutSec: 60  # For long-poll support on the /logs endpoint. See go.dev/issue/53569.
diff --git a/perf/app/app.go b/perf/app/app.go
index be4b0f8..4dcb32e 100644
--- a/perf/app/app.go
+++ b/perf/app/app.go
@@ -59,7 +59,6 @@
 	mux.HandleFunc("/search", a.search)
 	mux.HandleFunc("/compare", a.compare)
 	mux.HandleFunc("/cron/syncinflux", a.syncInflux)
-	mux.HandleFunc("/healthz", a.healthz)
 	a.dashboardRegisterOnMux(mux)
 }
 
@@ -82,9 +81,3 @@
 	//q := r.Form.Get("q")
 	a.compare(w, r)
 }
-
-// healthz handles /healthz.
-func (a *App) healthz(w http.ResponseWriter, r *http.Request) {
-	w.WriteHeader(http.StatusOK)
-	w.Write([]byte("ok"))
-}