1 package updater
2
3 import (
4 "context"
5 "errors"
6 "io"
7 "testing"
8 "time"
9
10 "google.golang.org/grpc"
11 "google.golang.org/protobuf/types/known/emptypb"
12 "google.golang.org/protobuf/types/known/timestamppb"
13
14 "github.com/jmhodges/clock"
15 capb "github.com/letsencrypt/boulder/ca/proto"
16 corepb "github.com/letsencrypt/boulder/core/proto"
17 cspb "github.com/letsencrypt/boulder/crl/storer/proto"
18 "github.com/letsencrypt/boulder/issuance"
19 blog "github.com/letsencrypt/boulder/log"
20 "github.com/letsencrypt/boulder/metrics"
21 "github.com/letsencrypt/boulder/mocks"
22 sapb "github.com/letsencrypt/boulder/sa/proto"
23 "github.com/letsencrypt/boulder/test"
24 "github.com/prometheus/client_golang/prometheus"
25 )
26
27
28
29
30 type fakeGRCC struct {
31 grpc.ClientStream
32 entries []*corepb.CRLEntry
33 nextIdx int
34 err error
35 }
36
37 func (f *fakeGRCC) Recv() (*corepb.CRLEntry, error) {
38 if f.err != nil {
39 return nil, f.err
40 }
41 if f.nextIdx < len(f.entries) {
42 res := f.entries[f.nextIdx]
43 f.nextIdx++
44 return res, nil
45 }
46 return nil, io.EOF
47 }
48
49
50
51
52 type fakeSAC struct {
53 mocks.StorageAuthority
54 grcc fakeGRCC
55 maxNotAfter time.Time
56 leaseError error
57 }
58
59 func (f *fakeSAC) GetRevokedCerts(ctx context.Context, _ *sapb.GetRevokedCertsRequest, _ ...grpc.CallOption) (sapb.StorageAuthority_GetRevokedCertsClient, error) {
60 return &f.grcc, nil
61 }
62
63 func (f *fakeSAC) GetMaxExpiration(_ context.Context, req *emptypb.Empty, _ ...grpc.CallOption) (*timestamppb.Timestamp, error) {
64 return timestamppb.New(f.maxNotAfter), nil
65 }
66
67 func (f *fakeSAC) LeaseCRLShard(_ context.Context, req *sapb.LeaseCRLShardRequest, _ ...grpc.CallOption) (*sapb.LeaseCRLShardResponse, error) {
68 if f.leaseError != nil {
69 return nil, f.leaseError
70 }
71 return &sapb.LeaseCRLShardResponse{IssuerNameID: req.IssuerNameID, ShardIdx: req.MinShardIdx}, nil
72 }
73
74
75
76
77 type fakeGCC struct {
78 grpc.ClientStream
79 chunks [][]byte
80 nextIdx int
81 sendErr error
82 recvErr error
83 }
84
85 func (f *fakeGCC) Send(*capb.GenerateCRLRequest) error {
86 return f.sendErr
87 }
88
89 func (f *fakeGCC) CloseSend() error {
90 return nil
91 }
92
93 func (f *fakeGCC) Recv() (*capb.GenerateCRLResponse, error) {
94 if f.recvErr != nil {
95 return nil, f.recvErr
96 }
97 if f.nextIdx < len(f.chunks) {
98 res := f.chunks[f.nextIdx]
99 f.nextIdx++
100 return &capb.GenerateCRLResponse{Chunk: res}, nil
101 }
102 return nil, io.EOF
103 }
104
105
106
107 type fakeCGC struct {
108 gcc fakeGCC
109 }
110
111 func (f *fakeCGC) GenerateCRL(ctx context.Context, opts ...grpc.CallOption) (capb.CRLGenerator_GenerateCRLClient, error) {
112 return &f.gcc, nil
113 }
114
115
116
117 type fakeUCC struct {
118 grpc.ClientStream
119 sendErr error
120 recvErr error
121 }
122
123 func (f *fakeUCC) Send(*cspb.UploadCRLRequest) error {
124 return f.sendErr
125 }
126
127 func (f *fakeUCC) CloseAndRecv() (*emptypb.Empty, error) {
128 if f.recvErr != nil {
129 return nil, f.recvErr
130 }
131 return &emptypb.Empty{}, nil
132 }
133
134
135
136 type fakeCSC struct {
137 ucc fakeUCC
138 }
139
140 func (f *fakeCSC) UploadCRL(ctx context.Context, opts ...grpc.CallOption) (cspb.CRLStorer_UploadCRLClient, error) {
141 return &f.ucc, nil
142 }
143
144 func TestUpdateShard(t *testing.T) {
145 e1, err := issuance.LoadCertificate("../../test/hierarchy/int-e1.cert.pem")
146 test.AssertNotError(t, err, "loading test issuer")
147 r3, err := issuance.LoadCertificate("../../test/hierarchy/int-r3.cert.pem")
148 test.AssertNotError(t, err, "loading test issuer")
149
150 sentinelErr := errors.New("oops")
151 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
152 defer cancel()
153
154 clk := clock.NewFake()
155 clk.Set(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
156 cu, err := NewUpdater(
157 []*issuance.Certificate{e1, r3},
158 2, 18*time.Hour, 24*time.Hour,
159 6*time.Hour, time.Minute, 1, 1,
160 &fakeSAC{grcc: fakeGRCC{}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)},
161 &fakeCGC{gcc: fakeGCC{}},
162 &fakeCSC{ucc: fakeUCC{}},
163 metrics.NoopRegisterer, blog.NewMock(), clk,
164 )
165 test.AssertNotError(t, err, "building test crlUpdater")
166
167 testChunks := []chunk{
168 {clk.Now(), clk.Now().Add(18 * time.Hour), 0},
169 }
170
171
172 err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks)
173 test.AssertNotError(t, err, "empty CRL")
174 test.AssertMetricWithLabelsEquals(t, cu.updatedCounter, prometheus.Labels{
175 "issuer": "(TEST) Elegant Elephant E1", "result": "success",
176 }, 1)
177 cu.updatedCounter.Reset()
178
179
180 cu.cs = &fakeCSC{ucc: fakeUCC{recvErr: sentinelErr}}
181 err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks)
182 test.AssertError(t, err, "storer error")
183 test.AssertContains(t, err.Error(), "closing CRLStorer upload stream")
184 test.AssertErrorIs(t, err, sentinelErr)
185 test.AssertMetricWithLabelsEquals(t, cu.updatedCounter, prometheus.Labels{
186 "issuer": "(TEST) Elegant Elephant E1", "result": "failed",
187 }, 1)
188 cu.updatedCounter.Reset()
189
190
191 cu.cs = &fakeCSC{ucc: fakeUCC{sendErr: sentinelErr}}
192 err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks)
193 test.AssertError(t, err, "storer error")
194 test.AssertContains(t, err.Error(), "sending CRLStorer metadata")
195 test.AssertErrorIs(t, err, sentinelErr)
196 test.AssertMetricWithLabelsEquals(t, cu.updatedCounter, prometheus.Labels{
197 "issuer": "(TEST) Elegant Elephant E1", "result": "failed",
198 }, 1)
199 cu.updatedCounter.Reset()
200
201
202 cu.ca = &fakeCGC{gcc: fakeGCC{recvErr: sentinelErr}}
203 err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks)
204 test.AssertError(t, err, "CA error")
205 test.AssertContains(t, err.Error(), "receiving CRL bytes")
206 test.AssertErrorIs(t, err, sentinelErr)
207 test.AssertMetricWithLabelsEquals(t, cu.updatedCounter, prometheus.Labels{
208 "issuer": "(TEST) Elegant Elephant E1", "result": "failed",
209 }, 1)
210 cu.updatedCounter.Reset()
211
212
213 cu.ca = &fakeCGC{gcc: fakeGCC{sendErr: sentinelErr}}
214 err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks)
215 test.AssertError(t, err, "CA error")
216 test.AssertContains(t, err.Error(), "sending CA metadata")
217 test.AssertErrorIs(t, err, sentinelErr)
218 test.AssertMetricWithLabelsEquals(t, cu.updatedCounter, prometheus.Labels{
219 "issuer": "(TEST) Elegant Elephant E1", "result": "failed",
220 }, 1)
221 cu.updatedCounter.Reset()
222
223
224 cu.sa = &fakeSAC{grcc: fakeGRCC{err: sentinelErr}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)}
225 err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks)
226 test.AssertError(t, err, "database error")
227 test.AssertContains(t, err.Error(), "retrieving entry from SA")
228 test.AssertErrorIs(t, err, sentinelErr)
229 test.AssertMetricWithLabelsEquals(t, cu.updatedCounter, prometheus.Labels{
230 "issuer": "(TEST) Elegant Elephant E1", "result": "failed",
231 }, 1)
232 cu.updatedCounter.Reset()
233 }
234
235 func TestUpdateShardWithRetry(t *testing.T) {
236 e1, err := issuance.LoadCertificate("../../test/hierarchy/int-e1.cert.pem")
237 test.AssertNotError(t, err, "loading test issuer")
238 r3, err := issuance.LoadCertificate("../../test/hierarchy/int-r3.cert.pem")
239 test.AssertNotError(t, err, "loading test issuer")
240
241 sentinelErr := errors.New("oops")
242 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
243 defer cancel()
244
245 clk := clock.NewFake()
246 clk.Set(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC))
247
248
249 cu, err := NewUpdater(
250 []*issuance.Certificate{e1, r3},
251 2, 18*time.Hour, 24*time.Hour,
252 6*time.Hour, time.Minute, 1, 1,
253 &fakeSAC{grcc: fakeGRCC{err: sentinelErr}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)},
254 &fakeCGC{gcc: fakeGCC{}},
255 &fakeCSC{ucc: fakeUCC{}},
256 metrics.NoopRegisterer, blog.NewMock(), clk,
257 )
258 test.AssertNotError(t, err, "building test crlUpdater")
259
260 testChunks := []chunk{
261 {clk.Now(), clk.Now().Add(18 * time.Hour), 0},
262 }
263
264
265
266 startTime := cu.clk.Now()
267 err = cu.updateShardWithRetry(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks)
268 test.AssertError(t, err, "database error")
269 test.AssertErrorIs(t, err, sentinelErr)
270 test.AssertEquals(t, cu.clk.Now(), startTime)
271
272
273
274
275 cu.maxAttempts = 5
276 startTime = cu.clk.Now()
277 err = cu.updateShardWithRetry(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks)
278 test.AssertError(t, err, "database error")
279 test.AssertErrorIs(t, err, sentinelErr)
280 t.Logf("start: %v", startTime)
281 t.Logf("now: %v", cu.clk.Now())
282 test.Assert(t, startTime.Add(15*0.8*time.Second).Before(cu.clk.Now()), "retries didn't sleep enough")
283 test.Assert(t, startTime.Add(15*1.2*time.Second).After(cu.clk.Now()), "retries slept too much")
284 }
285
View as plain text