Do not pipe into `grep -q`, because that stops reading as soon as a
[arvados.git] / tools / keep-block-check / keep-block-check_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bytes"
9         "fmt"
10         "io"
11         "io/ioutil"
12         "log"
13         "os"
14         "regexp"
15         "strings"
16         "testing"
17         "time"
18
19         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
20         "git.arvados.org/arvados.git/sdk/go/arvadostest"
21         "git.arvados.org/arvados.git/sdk/go/keepclient"
22
23         . "gopkg.in/check.v1"
24 )
25
26 // Gocheck boilerplate
27 func Test(t *testing.T) {
28         TestingT(t)
29 }
30
31 // Gocheck boilerplate
32 var _ = Suite(&ServerRequiredSuite{})
33 var _ = Suite(&DoMainTestSuite{})
34
35 type ServerRequiredSuite struct{}
36 type DoMainTestSuite struct{}
37
38 var kc *keepclient.KeepClient
39 var logBuffer bytes.Buffer
40
41 var TestHash = "aaaa09c290d0fb1ca068ffaddf22cbd0"
42 var TestHash2 = "aaaac516f788aec4f30932ffb6395c39"
43
44 var blobSignatureTTL = time.Duration(2*7*24) * time.Hour
45
46 func (s *ServerRequiredSuite) TearDownSuite(c *C) {
47         arvadostest.ResetEnv()
48 }
49
50 func (s *ServerRequiredSuite) SetUpTest(c *C) {
51         logOutput := io.MultiWriter(&logBuffer)
52         log.SetOutput(logOutput)
53 }
54
55 func (s *ServerRequiredSuite) TearDownTest(c *C) {
56         arvadostest.StopKeep(2)
57         log.SetOutput(os.Stdout)
58         log.Printf("%v", logBuffer.String())
59 }
60
61 func (s *DoMainTestSuite) SetUpSuite(c *C) {
62 }
63
64 func (s *DoMainTestSuite) SetUpTest(c *C) {
65         logOutput := io.MultiWriter(&logBuffer)
66         log.SetOutput(logOutput)
67         keepclient.RefreshServiceDiscovery()
68 }
69
70 func (s *DoMainTestSuite) TearDownTest(c *C) {
71         log.SetOutput(os.Stdout)
72         log.Printf("%v", logBuffer.String())
73 }
74
75 func setupKeepBlockCheck(c *C, enforcePermissions bool, keepServicesJSON string) {
76         setupKeepBlockCheckWithTTL(c, enforcePermissions, keepServicesJSON, blobSignatureTTL)
77 }
78
79 func setupKeepBlockCheckWithTTL(c *C, enforcePermissions bool, keepServicesJSON string, ttl time.Duration) {
80         var config apiConfig
81         config.APIHost = os.Getenv("ARVADOS_API_HOST")
82         config.APIToken = arvadostest.DataManagerToken
83         config.APIHostInsecure = arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE"))
84
85         // Start Keep servers
86         arvadostest.StartKeep(2, enforcePermissions)
87
88         // setup keepclients
89         var err error
90         kc, ttl, err = setupKeepClient(config, keepServicesJSON, ttl)
91         c.Assert(ttl, Equals, blobSignatureTTL)
92         c.Check(err, IsNil)
93
94         keepclient.RefreshServiceDiscovery()
95 }
96
97 // Setup test data
98 func setupTestData(c *C) []string {
99         allLocators := []string{}
100
101         // Put a few blocks
102         for i := 0; i < 5; i++ {
103                 hash, _, err := kc.PutB([]byte(fmt.Sprintf("keep-block-check-test-data-%d", i)))
104                 c.Check(err, IsNil)
105                 allLocators = append(allLocators, strings.Split(hash, "+A")[0])
106         }
107
108         return allLocators
109 }
110
111 func setupConfigFile(c *C, fileName string) string {
112         // Setup a config file
113         file, err := ioutil.TempFile(os.TempDir(), fileName)
114         c.Check(err, IsNil)
115
116         // Add config to file. While at it, throw some extra white space
117         fileContent := "ARVADOS_API_HOST=" + os.Getenv("ARVADOS_API_HOST") + "\n"
118         fileContent += "ARVADOS_API_TOKEN=" + arvadostest.DataManagerToken + "\n"
119         fileContent += "\n"
120         fileContent += "ARVADOS_API_HOST_INSECURE=" + os.Getenv("ARVADOS_API_HOST_INSECURE") + "\n"
121         fileContent += " ARVADOS_EXTERNAL_CLIENT = false \n"
122         fileContent += " NotANameValuePairAndShouldGetIgnored \n"
123         fileContent += "ARVADOS_BLOB_SIGNING_KEY=abcdefg\n"
124
125         _, err = file.Write([]byte(fileContent))
126         c.Check(err, IsNil)
127
128         return file.Name()
129 }
130
131 func setupBlockHashFile(c *C, name string, blocks []string) string {
132         // Setup a block hash file
133         file, err := ioutil.TempFile(os.TempDir(), name)
134         c.Check(err, IsNil)
135
136         // Add the hashes to the file. While at it, throw some extra white space
137         fileContent := ""
138         for _, hash := range blocks {
139                 fileContent += fmt.Sprintf(" %s \n", hash)
140         }
141         fileContent += "\n"
142         _, err = file.Write([]byte(fileContent))
143         c.Check(err, IsNil)
144
145         return file.Name()
146 }
147
148 func checkErrorLog(c *C, blocks []string, prefix, suffix string) {
149         for _, hash := range blocks {
150                 expected := `(?ms).*` + prefix + `.*` + hash + `.*` + suffix + `.*`
151                 c.Check(logBuffer.String(), Matches, expected)
152         }
153 }
154
155 func checkNoErrorsLogged(c *C, prefix, suffix string) {
156         expected := prefix + `.*` + suffix
157         match, _ := regexp.MatchString(expected, logBuffer.String())
158         c.Assert(match, Equals, false)
159 }
160
161 func (s *ServerRequiredSuite) TestBlockCheck(c *C) {
162         setupKeepBlockCheck(c, false, "")
163         allLocators := setupTestData(c)
164         err := performKeepBlockCheck(kc, blobSignatureTTL, "", allLocators, true)
165         c.Check(err, IsNil)
166         checkNoErrorsLogged(c, "Error verifying block", "Block not found")
167 }
168
169 func (s *ServerRequiredSuite) TestBlockCheckWithBlobSigning(c *C) {
170         setupKeepBlockCheck(c, true, "")
171         allLocators := setupTestData(c)
172         err := performKeepBlockCheck(kc, blobSignatureTTL, arvadostest.BlobSigningKey, allLocators, true)
173         c.Check(err, IsNil)
174         checkNoErrorsLogged(c, "Error verifying block", "Block not found")
175 }
176
177 func (s *ServerRequiredSuite) TestBlockCheckWithBlobSigningAndTTLFromDiscovery(c *C) {
178         setupKeepBlockCheckWithTTL(c, true, "", 0)
179         allLocators := setupTestData(c)
180         err := performKeepBlockCheck(kc, blobSignatureTTL, arvadostest.BlobSigningKey, allLocators, true)
181         c.Check(err, IsNil)
182         checkNoErrorsLogged(c, "Error verifying block", "Block not found")
183 }
184
185 func (s *ServerRequiredSuite) TestBlockCheck_NoSuchBlock(c *C) {
186         setupKeepBlockCheck(c, false, "")
187         allLocators := setupTestData(c)
188         allLocators = append(allLocators, TestHash)
189         allLocators = append(allLocators, TestHash2)
190         err := performKeepBlockCheck(kc, blobSignatureTTL, "", allLocators, true)
191         c.Check(err, NotNil)
192         c.Assert(err.Error(), Equals, "Block verification failed for 2 out of 7 blocks with matching prefix")
193         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "Block not found")
194 }
195
196 func (s *ServerRequiredSuite) TestBlockCheck_NoSuchBlock_WithMatchingPrefix(c *C) {
197         setupKeepBlockCheck(c, false, "")
198         allLocators := setupTestData(c)
199         allLocators = append(allLocators, TestHash)
200         allLocators = append(allLocators, TestHash2)
201         locatorFile := setupBlockHashFile(c, "block-hash", allLocators)
202         defer os.Remove(locatorFile)
203         locators, err := getBlockLocators(locatorFile, "aaa")
204         c.Check(err, IsNil)
205         err = performKeepBlockCheck(kc, blobSignatureTTL, "", locators, true)
206         c.Check(err, NotNil)
207         // Of the 7 blocks in allLocators, only two match the prefix and hence only those are checked
208         c.Assert(err.Error(), Equals, "Block verification failed for 2 out of 2 blocks with matching prefix")
209         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "Block not found")
210 }
211
212 func (s *ServerRequiredSuite) TestBlockCheck_NoSuchBlock_WithPrefixMismatch(c *C) {
213         setupKeepBlockCheck(c, false, "")
214         allLocators := setupTestData(c)
215         allLocators = append(allLocators, TestHash)
216         allLocators = append(allLocators, TestHash2)
217         locatorFile := setupBlockHashFile(c, "block-hash", allLocators)
218         defer os.Remove(locatorFile)
219         locators, err := getBlockLocators(locatorFile, "999")
220         c.Check(err, IsNil)
221         err = performKeepBlockCheck(kc, blobSignatureTTL, "", locators, true)
222         c.Check(err, IsNil) // there were no matching locators in file and hence nothing was checked
223 }
224
225 func (s *ServerRequiredSuite) TestBlockCheck_BadSignature(c *C) {
226         setupKeepBlockCheck(c, true, "")
227         setupTestData(c)
228         err := performKeepBlockCheck(kc, blobSignatureTTL, "badblobsigningkey", []string{TestHash, TestHash2}, false)
229         c.Assert(err.Error(), Equals, "Block verification failed for 2 out of 2 blocks with matching prefix")
230         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "HTTP 403")
231         // verbose logging not requested
232         c.Assert(strings.Contains(logBuffer.String(), "Verifying block 1 of 2"), Equals, false)
233 }
234
235 var testKeepServicesJSON = `{
236   "kind":"arvados#keepServiceList",
237   "etag":"",
238   "self_link":"",
239   "offset":null, "limit":null,
240   "items":[
241     {"href":"/keep_services/zzzzz-bi6l4-123456789012340",
242      "kind":"arvados#keepService",
243      "uuid":"zzzzz-bi6l4-123456789012340",
244      "service_host":"keep0.zzzzz.arvadosapi.com",
245      "service_port":25107,
246      "service_ssl_flag":false,
247      "service_type":"disk",
248      "read_only":false },
249     {"href":"/keep_services/zzzzz-bi6l4-123456789012341",
250      "kind":"arvados#keepService",
251      "uuid":"zzzzz-bi6l4-123456789012341",
252      "service_host":"keep0.zzzzz.arvadosapi.com",
253      "service_port":25108,
254      "service_ssl_flag":false,
255      "service_type":"disk",
256      "read_only":false }
257     ],
258   "items_available":2 }`
259
260 // Setup block-check using keepServicesJSON with fake keepservers.
261 // Expect error during performKeepBlockCheck due to unreachable keepservers.
262 func (s *ServerRequiredSuite) TestErrorDuringKeepBlockCheck_FakeKeepservers(c *C) {
263         setupKeepBlockCheck(c, false, testKeepServicesJSON)
264         err := performKeepBlockCheck(kc, blobSignatureTTL, "", []string{TestHash, TestHash2}, true)
265         c.Assert(err.Error(), Equals, "Block verification failed for 2 out of 2 blocks with matching prefix")
266         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "")
267 }
268
269 // Test keep-block-check initialization with keepServicesJSON
270 func (s *ServerRequiredSuite) TestKeepBlockCheck_InitializeWithKeepServicesJSON(c *C) {
271         setupKeepBlockCheck(c, false, testKeepServicesJSON)
272         found := 0
273         for k := range kc.LocalRoots() {
274                 if k == "zzzzz-bi6l4-123456789012340" || k == "zzzzz-bi6l4-123456789012341" {
275                         found++
276                 }
277         }
278         c.Check(found, Equals, 2)
279 }
280
281 // Test loadConfig func
282 func (s *ServerRequiredSuite) TestLoadConfig(c *C) {
283         // Setup config file
284         configFile := setupConfigFile(c, "config")
285         defer os.Remove(configFile)
286
287         // load configuration from the file
288         config, blobSigningKey, err := loadConfig(configFile)
289         c.Check(err, IsNil)
290
291         c.Assert(config.APIHost, Equals, os.Getenv("ARVADOS_API_HOST"))
292         c.Assert(config.APIToken, Equals, arvadostest.DataManagerToken)
293         c.Assert(config.APIHostInsecure, Equals, arvadosclient.StringBool(os.Getenv("ARVADOS_API_HOST_INSECURE")))
294         c.Assert(config.ExternalClient, Equals, false)
295         c.Assert(blobSigningKey, Equals, "abcdefg")
296 }
297
298 func (s *DoMainTestSuite) Test_doMain_WithNoConfig(c *C) {
299         args := []string{"-prefix", "a"}
300         var stderr bytes.Buffer
301         code := doMain(args, &stderr)
302         c.Check(code, Equals, 1)
303         c.Check(stderr.String(), Matches, ".*config file not specified\n")
304 }
305
306 func (s *DoMainTestSuite) Test_doMain_WithNoSuchConfigFile(c *C) {
307         args := []string{"-config", "no-such-file"}
308         var stderr bytes.Buffer
309         code := doMain(args, &stderr)
310         c.Check(code, Equals, 1)
311         c.Check(stderr.String(), Matches, ".*no such file or directory\n")
312 }
313
314 func (s *DoMainTestSuite) Test_doMain_WithNoBlockHashFile(c *C) {
315         config := setupConfigFile(c, "config")
316         defer os.Remove(config)
317
318         // Start keepservers.
319         arvadostest.StartKeep(2, false)
320         defer arvadostest.StopKeep(2)
321
322         args := []string{"-config", config}
323         var stderr bytes.Buffer
324         code := doMain(args, &stderr)
325         c.Check(code, Equals, 1)
326         c.Check(stderr.String(), Matches, ".*block-hash-file not specified\n")
327 }
328
329 func (s *DoMainTestSuite) Test_doMain_WithNoSuchBlockHashFile(c *C) {
330         config := setupConfigFile(c, "config")
331         defer os.Remove(config)
332
333         arvadostest.StartKeep(2, false)
334         defer arvadostest.StopKeep(2)
335
336         args := []string{"-config", config, "-block-hash-file", "no-such-file"}
337         var stderr bytes.Buffer
338         code := doMain(args, &stderr)
339         c.Check(code, Equals, 1)
340         c.Check(stderr.String(), Matches, ".*no such file or directory\n")
341 }
342
343 func (s *DoMainTestSuite) Test_doMain(c *C) {
344         // Start keepservers.
345         arvadostest.StartKeep(2, false)
346         defer arvadostest.StopKeep(2)
347
348         config := setupConfigFile(c, "config")
349         defer os.Remove(config)
350
351         locatorFile := setupBlockHashFile(c, "block-hash", []string{TestHash, TestHash2})
352         defer os.Remove(locatorFile)
353
354         args := []string{"-config", config, "-block-hash-file", locatorFile, "-v"}
355         var stderr bytes.Buffer
356         code := doMain(args, &stderr)
357         c.Check(code, Equals, 1)
358         c.Assert(stderr.String(), Matches, "Block verification failed for 2 out of 2 blocks with matching prefix\n")
359         checkErrorLog(c, []string{TestHash, TestHash2}, "Error verifying block", "Block not found")
360         c.Assert(strings.Contains(logBuffer.String(), "Verifying block 1 of 2"), Equals, true)
361 }