Skip to content

Commit 2e5f864

Browse files
authored
Added /healthz support (#359)
* Added /healthz support. Fixed IsHealthy logic. Added tests for both. Added healthz http handler.
1 parent 670d87c commit 2e5f864

File tree

13 files changed

+192
-36
lines changed

13 files changed

+192
-36
lines changed

cmd/amppkg/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/pkg/errors"
3030

3131
"github.com/ampproject/amppackager/packager/certloader"
32+
"github.com/ampproject/amppackager/packager/healthz"
3233
"github.com/ampproject/amppackager/packager/mux"
3334
"github.com/ampproject/amppackager/packager/rtv"
3435
"github.com/ampproject/amppackager/packager/signer"
@@ -95,6 +96,11 @@ func main() {
9596
}
9697
}
9798

99+
healthz, err := healthz.New(certCache)
100+
if err != nil {
101+
die(errors.Wrap(err, "building healthz"))
102+
}
103+
98104
rtvCache, err := rtv.New()
99105
if err != nil {
100106
die(errors.Wrap(err, "initializing rtv cache"))
@@ -127,7 +133,7 @@ func main() {
127133
Addr: addr,
128134
// Don't use DefaultServeMux, per
129135
// https://blog.cloudflare.com/exposing-go-on-the-internet/.
130-
Handler: logIntercept{mux.New(certCache, signer, validityMap)},
136+
Handler: logIntercept{mux.New(certCache, signer, validityMap, healthz)},
131137
ReadTimeout: 10 * time.Second,
132138
ReadHeaderTimeout: 5 * time.Second,
133139
// If needing to stream the response, disable WriteTimeout and

cmd/gateway_server/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ type gatewayServer struct {
3939
rtvCache *rtv.RTVCache
4040
}
4141

42-
func shouldPackage() bool {
43-
return true
42+
func shouldPackage() error {
43+
return nil
4444
}
4545

4646
func errorToSXGResponse(err error) *pb.SXGResponse {

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,12 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
238238
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
239239
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
240240
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
241+
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg=
241242
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
242243
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
243244
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
244245
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
246+
google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
245247
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
246248
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
247249
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

packager/certcache/certcache.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ const maxOCSPResponseBytes = 1024 * 1024
5252
const ocspCheckInterval = 1 * time.Hour
5353

5454
type CertHandler interface {
55-
GetLatestCert() (*x509.Certificate)
55+
GetLatestCert() *x509.Certificate
56+
IsHealthy() error
5657
}
5758

5859
type CertCache struct {
@@ -128,7 +129,7 @@ func (this *CertCache) Init(stop chan struct{}) error {
128129
// For now this just returns the first entry in the certs field in the cache.
129130
// For follow-on changes, we will transform this to a lambda so that anything
130131
// that needs a cert can do the cert refresh logic (if needed) on demand.
131-
func (this *CertCache) GetLatestCert() (*x509.Certificate) {
132+
func (this *CertCache) GetLatestCert() *x509.Certificate {
132133
// TODO(banaag): check if cert is still valid, refresh if not.
133134
return this.certs[0]
134135
}
@@ -201,31 +202,34 @@ func (this *CertCache) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
201202
// 8. Some idea of what to do when "things go bad".
202203
// What happens when it's been 7 days, no new OCSP response can be obtained,
203204
// and the current response is about to expire?
204-
func (this *CertCache) IsHealthy() bool {
205-
ocsp, _, err := this.readOCSP()
206-
return err != nil || this.isHealthy(ocsp)
205+
func (this *CertCache) IsHealthy() error {
206+
ocsp, _, errorOcsp := this.readOCSP()
207+
if errorOcsp != nil {
208+
return errorOcsp
209+
}
210+
errorHealth := this.isHealthy(ocsp)
211+
if errorHealth != nil {
212+
return errorHealth
213+
}
214+
return nil
207215
}
208216

209-
func (this *CertCache) isHealthy(ocspResp []byte) bool {
217+
func (this *CertCache) isHealthy(ocspResp []byte) error {
210218
if ocspResp == nil {
211-
log.Println("OCSP response not yet fetched.")
212-
return false
219+
return errors.New("OCSP response not yet fetched.")
213220
}
214221
issuer := this.findIssuer()
215222
if issuer == nil {
216-
log.Println("Cannot find issuer certificate in CertFile.")
217-
return false
223+
return errors.New("Cannot find issuer certificate in CertFile.")
218224
}
219225
resp, err := ocsp.ParseResponseForCert(ocspResp, this.certs[0], issuer)
220226
if err != nil {
221-
log.Println("Error parsing OCSP response:", err)
222-
return false
227+
return errors.Wrap(err, "Error parsing OCSP response.")
223228
}
224229
if resp.NextUpdate.Before(time.Now()) {
225-
log.Println("Cached OCSP is stale, NextUpdate:", resp.NextUpdate)
226-
return false
230+
return errors.Errorf("Cached OCSP is stale, NextUpdate: %v", resp.NextUpdate)
227231
}
228-
return true
232+
return nil
229233
}
230234

231235
// Returns the OCSP response and expiry, refreshing if necessary.
@@ -240,8 +244,9 @@ func (this *CertCache) readOCSP() ([]byte, time.Time, error) {
240244
if len(ocsp) == 0 {
241245
return nil, time.Time{}, errors.New("Missing OCSP response.")
242246
}
243-
if !this.isHealthy(ocsp) {
244-
return nil, time.Time{}, errors.New("OCSP failed health check.")
247+
err = this.isHealthy(ocsp)
248+
if err != nil {
249+
return nil, time.Time{}, errors.Wrap(err, "OCSP failed health check.")
245250
}
246251
this.ocspUpdateAfterMu.Lock()
247252
defer this.ocspUpdateAfterMu.Unlock()
@@ -434,7 +439,7 @@ func (this *CertCache) fetchOCSP(orig []byte, ocspUpdateAfter *time.Time) []byte
434439
// OCSP duration must be <=7 days, per
435440
// https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cross-origin-trust.
436441
// Serving these responses may cause UAs to reject the SXG.
437-
if resp.NextUpdate.Sub(resp.ThisUpdate) > time.Hour * 24 * 7 {
442+
if resp.NextUpdate.Sub(resp.ThisUpdate) > time.Hour*24*7 {
438443
log.Printf("OCSP nextUpdate %+v too far ahead of thisUpdate %+v\n", resp.NextUpdate, resp.ThisUpdate)
439444
return orig
440445
}

packager/certcache/certcache_test.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (this *CertCacheSuite) TearDownTest() {
134134
}
135135

136136
func (this *CertCacheSuite) mux() http.Handler {
137-
return mux.New(this.handler, nil, nil)
137+
return mux.New(this.handler, nil, nil, nil)
138138
}
139139

140140
func (this *CertCacheSuite) ocspServerCalled(f func()) bool {
@@ -181,6 +181,35 @@ func (this *CertCacheSuite) TestServesCertificate() {
181181
this.Assert().NotContains(cbor, "sct")
182182
}
183183

184+
func (this *CertCacheSuite) TestCertCacheIsHealthy() {
185+
this.Assert().NoError(this.handler.IsHealthy())
186+
}
187+
188+
func (this *CertCacheSuite) TestCertCacheIsNotHealthy() {
189+
// Prime memory cache with a past-midpoint OCSP:
190+
err := os.Remove(filepath.Join(this.tempDir, "ocsp"))
191+
this.Require().NoError(err, "deleting OCSP tempfile")
192+
this.fakeOCSP, err = FakeOCSPResponse(time.Now().Add(-4 * 24 * time.Hour))
193+
this.Require().NoError(err, "creating stale OCSP response")
194+
this.Require().True(this.ocspServerCalled(func() {
195+
this.handler, err = this.New()
196+
this.Require().NoError(err, "reinstantiating CertCache")
197+
}))
198+
199+
// Prime disk cache with a bad OCSP:
200+
freshOCSP := []byte("0xdeadbeef")
201+
this.fakeOCSP = freshOCSP
202+
err = ioutil.WriteFile(filepath.Join(this.tempDir, "ocsp"), freshOCSP, 0644)
203+
this.Require().NoError(err, "writing fresh OCSP response to disk")
204+
205+
// On update, verify network is not called (fresh OCSP from disk is used):
206+
this.Assert().True(this.ocspServerCalled(func() {
207+
this.handler.readOCSP()
208+
}))
209+
210+
this.Assert().Error(this.handler.IsHealthy())
211+
}
212+
184213
func (this *CertCacheSuite) TestServes404OnMissingCertificate() {
185214
resp := pkgt.Get(this.T(), this.mux(), "/amppkg/cert/lalala")
186215
this.Assert().Equal(http.StatusNotFound, resp.StatusCode, "incorrect status: %#v", resp)

packager/certcache/storage.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88
"runtime"
99
"sync"
1010

11-
"github.com/pkg/errors"
1211
"github.com/gofrs/flock"
12+
"github.com/pkg/errors"
1313
)
1414

1515
// This is an abstraction over a single file on a remote storage mechanism. It

packager/healthz/healthz.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package healthz
16+
17+
import (
18+
"fmt"
19+
"github.com/ampproject/amppackager/packager/certcache"
20+
"net/http"
21+
)
22+
23+
type Healthz struct {
24+
certHandler certcache.CertHandler
25+
}
26+
27+
func New(certHandler certcache.CertHandler) (*Healthz, error) {
28+
return &Healthz{certHandler}, nil
29+
}
30+
31+
func (this *Healthz) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
32+
// Follow https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
33+
err := this.certHandler.IsHealthy()
34+
if err != nil {
35+
resp.WriteHeader(500)
36+
resp.Write([]byte(fmt.Sprintf("not healthy: %v", err)))
37+
} else {
38+
resp.WriteHeader(200)
39+
resp.Write([]byte("ok"))
40+
}
41+
}

packager/healthz/healthz_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package healthz
16+
17+
import (
18+
"crypto/x509"
19+
"net/http"
20+
"testing"
21+
22+
"github.com/ampproject/amppackager/packager/mux"
23+
"github.com/pkg/errors"
24+
25+
pkgt "github.com/ampproject/amppackager/packager/testing"
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
type fakeHealthyCertHandler struct {
31+
}
32+
33+
func (this fakeHealthyCertHandler) GetLatestCert() *x509.Certificate {
34+
return pkgt.Certs[0]
35+
}
36+
37+
func (this fakeHealthyCertHandler) IsHealthy() error {
38+
return nil
39+
}
40+
41+
type fakeNotHealthyCertHandler struct {
42+
}
43+
44+
func (this fakeNotHealthyCertHandler) GetLatestCert() *x509.Certificate {
45+
return pkgt.Certs[0]
46+
}
47+
48+
func (this fakeNotHealthyCertHandler) IsHealthy() error {
49+
return errors.New("random error")
50+
}
51+
52+
func TestHealthzOk(t *testing.T) {
53+
handler, err := New(fakeHealthyCertHandler{})
54+
require.NoError(t, err)
55+
resp := pkgt.Get(t, mux.New(nil, nil, nil, handler), "/healthz")
56+
assert.Equal(t, http.StatusOK, resp.StatusCode, "ok", resp)
57+
}
58+
59+
func TestHealthzFail(t *testing.T) {
60+
handler, err := New(fakeNotHealthyCertHandler{})
61+
require.NoError(t, err)
62+
resp := pkgt.Get(t, mux.New(nil, nil, nil, handler), "/healthz")
63+
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode, "error", resp)
64+
}

packager/mux/mux.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@ type mux struct {
4545
certCache http.Handler
4646
signer http.Handler
4747
validityMap http.Handler
48+
healthz http.Handler
4849
}
4950

5051
// The main entry point. Use the return value for http.Server.Handler.
51-
func New(certCache http.Handler, signer http.Handler, validityMap http.Handler) http.Handler {
52-
return &mux{certCache, signer, validityMap}
52+
func New(certCache http.Handler, signer http.Handler, validityMap http.Handler, healthz http.Handler) http.Handler {
53+
return &mux{certCache, signer, validityMap, healthz}
5354
}
5455

5556
func tryTrimPrefix(s, prefix string) (string, bool) {
@@ -97,6 +98,8 @@ func (this *mux) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
9798
params["certName"] = unescaped
9899
this.certCache.ServeHTTP(resp, req)
99100
}
101+
} else if path == util.HealthzPath {
102+
this.healthz.ServeHTTP(resp, req)
100103
} else if path == util.ValidityMapPath {
101104
this.validityMap.ServeHTTP(resp, req)
102105
} else {

packager/signer/signer.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ type Signer struct {
128128
client *http.Client
129129
urlSets []util.URLSet
130130
rtvCache *rtv.RTVCache
131-
shouldPackage func() bool
131+
shouldPackage func() error
132132
overrideBaseURL *url.URL
133133
requireHeaders bool
134134
forwardedRequestHeaders []string
@@ -139,7 +139,7 @@ func noRedirects(req *http.Request, via []*http.Request) error {
139139
}
140140

141141
func New(certHandler certcache.CertHandler, key crypto.PrivateKey, urlSets []util.URLSet,
142-
rtvCache *rtv.RTVCache, shouldPackage func() bool, overrideBaseURL *url.URL,
142+
rtvCache *rtv.RTVCache, shouldPackage func() error, overrideBaseURL *url.URL,
143143
requireHeaders bool, forwardedRequestHeaders []string) (*Signer, error) {
144144
client := http.Client{
145145
CheckRedirect: noRedirects,
@@ -307,8 +307,8 @@ func (this *Signer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
307307
}
308308
}()
309309

310-
if !this.shouldPackage() {
311-
log.Println("Not packaging because server is unhealthy; see above log statements.")
310+
if err := this.shouldPackage(); err != nil {
311+
log.Println("Not packaging because server is unhealthy; see above log statements.", err)
312312
proxy(resp, fetchResp, nil)
313313
return
314314
}
@@ -460,7 +460,7 @@ func (this *Signer) serveSignedExchange(resp http.ResponseWriter, fetchResp *htt
460460
fetchResp.Header.Get("Content-Security-Policy")))
461461

462462
exchange := signedexchange.NewExchange(
463-
accept.SxgVersion, /*uri=*/signURL.String(), /*method=*/"GET",
463+
accept.SxgVersion /*uri=*/, signURL.String() /*method=*/, "GET",
464464
http.Header{}, fetchResp.StatusCode, fetchResp.Header, []byte(transformed))
465465
if err := exchange.MiEncodePayload(miRecordSize); err != nil {
466466
util.NewHTTPError(http.StatusInternalServerError, "Error MI-encoding: ", err).LogAndRespond(resp)

0 commit comments

Comments
 (0)