17717: address further review comments: remove unused code and refactor
[arvados.git] / lib / costanalyzer / costanalyzer_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package costanalyzer
6
7 import (
8         "bytes"
9         "io"
10         "io/ioutil"
11         "os"
12         "regexp"
13         "testing"
14
15         "git.arvados.org/arvados.git/sdk/go/arvados"
16         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
17         "git.arvados.org/arvados.git/sdk/go/arvadostest"
18         "git.arvados.org/arvados.git/sdk/go/keepclient"
19         "gopkg.in/check.v1"
20 )
21
22 func Test(t *testing.T) {
23         check.TestingT(t)
24 }
25
26 var _ = check.Suite(&Suite{})
27
28 type Suite struct{}
29
30 func (s *Suite) TearDownSuite(c *check.C) {
31         // Undo any changes/additions to the database so they don't affect subsequent tests.
32         arvadostest.ResetEnv()
33 }
34
35 func (s *Suite) SetUpSuite(c *check.C) {
36         arvadostest.StartAPI()
37         arvadostest.StartKeep(2, true)
38
39         // Get the various arvados, arvadosclient, and keep client objects
40         ac := arvados.NewClientFromEnv()
41         arv, err := arvadosclient.MakeArvadosClient()
42         c.Assert(err, check.Equals, nil)
43         arv.ApiToken = arvadostest.ActiveToken
44         kc, err := keepclient.MakeKeepClient(arv)
45         c.Assert(err, check.Equals, nil)
46
47         standardE4sV3JSON := `{
48     "Name": "Standard_E4s_v3",
49     "ProviderType": "Standard_E4s_v3",
50     "VCPUs": 4,
51     "RAM": 34359738368,
52     "Scratch": 64000000000,
53     "IncludedScratch": 64000000000,
54     "AddedScratch": 0,
55     "Price": 0.292,
56     "Preemptible": true
57 }`
58         standardD32sV3JSON := `{
59     "Name": "Standard_D32s_v3",
60     "ProviderType": "Standard_D32s_v3",
61     "VCPUs": 32,
62     "RAM": 137438953472,
63     "Scratch": 256000000000,
64     "IncludedScratch": 256000000000,
65     "AddedScratch": 0,
66     "Price": 1.76,
67     "Preemptible": false
68 }`
69
70         standardA1V2JSON := `{
71     "Name": "a1v2",
72     "ProviderType": "Standard_A1_v2",
73     "VCPUs": 1,
74     "RAM": 2147483648,
75     "Scratch": 10000000000,
76     "IncludedScratch": 10000000000,
77     "AddedScratch": 0,
78     "Price": 0.043,
79     "Preemptible": false
80 }`
81
82         standardA2V2JSON := `{
83     "Name": "a2v2",
84     "ProviderType": "Standard_A2_v2",
85     "VCPUs": 2,
86     "RAM": 4294967296,
87     "Scratch": 20000000000,
88     "IncludedScratch": 20000000000,
89     "AddedScratch": 0,
90     "Price": 0.091,
91     "Preemptible": false
92 }`
93
94         legacyD1V2JSON := `{
95     "properties": {
96         "cloud_node": {
97             "price": 0.073001,
98             "size": "Standard_D1_v2"
99         },
100         "total_cpu_cores": 1,
101         "total_ram_mb": 3418,
102         "total_scratch_mb": 51170
103     }
104 }`
105
106         // Our fixtures do not actually contain file contents. Populate the log collections we're going to use with the node.json file
107         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID, arvadostest.LogCollectionUUID, standardE4sV3JSON)
108         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedContainerRequestUUID2, arvadostest.LogCollectionUUID2, standardD32sV3JSON)
109
110         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.DiagnosticsContainerRequest1LogCollectionUUID, standardA1V2JSON)
111         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsContainerRequest2UUID, arvadostest.DiagnosticsContainerRequest2LogCollectionUUID, standardA1V2JSON)
112         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher1ContainerRequestUUID, arvadostest.Hasher1LogCollectionUUID, standardA1V2JSON)
113         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher2ContainerRequestUUID, arvadostest.Hasher2LogCollectionUUID, standardA2V2JSON)
114         createNodeJSON(c, arv, ac, kc, arvadostest.CompletedDiagnosticsHasher3ContainerRequestUUID, arvadostest.Hasher3LogCollectionUUID, legacyD1V2JSON)
115 }
116
117 func (s *Suite) SetUpTest(c *check.C) {
118         Command = &command{}
119 }
120
121 func createNodeJSON(c *check.C, arv *arvadosclient.ArvadosClient, ac *arvados.Client, kc *keepclient.KeepClient, crUUID string, logUUID string, nodeJSON string) {
122         // Get the CR
123         var cr arvados.ContainerRequest
124         err := ac.RequestAndDecode(&cr, "GET", "arvados/v1/container_requests/"+crUUID, nil, nil)
125         c.Assert(err, check.Equals, nil)
126         c.Assert(cr.LogUUID, check.Equals, logUUID)
127
128         // Get the log collection
129         var coll arvados.Collection
130         err = ac.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+cr.LogUUID, nil, nil)
131         c.Assert(err, check.IsNil)
132
133         // Create a node.json file -- the fixture doesn't actually contain the contents of the collection.
134         fs, err := coll.FileSystem(ac, kc)
135         c.Assert(err, check.IsNil)
136         f, err := fs.OpenFile("node.json", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
137         c.Assert(err, check.IsNil)
138         _, err = io.WriteString(f, nodeJSON)
139         c.Assert(err, check.IsNil)
140         err = f.Close()
141         c.Assert(err, check.IsNil)
142
143         // Flush the data to Keep
144         mtxt, err := fs.MarshalManifest(".")
145         c.Assert(err, check.IsNil)
146         c.Assert(mtxt, check.NotNil)
147
148         // Update collection record
149         err = ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+cr.LogUUID, nil, map[string]interface{}{
150                 "collection": map[string]interface{}{
151                         "manifest_text": mtxt,
152                 },
153         })
154         c.Assert(err, check.IsNil)
155 }
156
157 func (*Suite) TestUsage(c *check.C) {
158         var stdout, stderr bytes.Buffer
159         exitcode := Command.RunCommand("costanalyzer.test", []string{"-help", "-log-level=debug"}, &bytes.Buffer{}, &stdout, &stderr)
160         c.Check(exitcode, check.Equals, 1)
161         c.Check(stdout.String(), check.Equals, "")
162         c.Check(stderr.String(), check.Matches, `(?ms).*Usage:.*`)
163 }
164
165 func (*Suite) TestTimestampRange(c *check.C) {
166         var stdout, stderr bytes.Buffer
167         resultsDir := c.MkDir()
168         // Run costanalyzer with a timestamp range. This should pick up two container requests in "Final" state.
169         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, "-begin", "2020-11-02T00:00:00", "-end", "2020-11-03T23:59:00"}, &bytes.Buffer{}, &stdout, &stderr)
170         c.Check(exitcode, check.Equals, 0)
171         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
172
173         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
174         c.Assert(err, check.IsNil)
175         uuid2Report, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
176         c.Assert(err, check.IsNil)
177
178         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00916192")
179         c.Check(string(uuid2Report), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00588088")
180         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
181         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
182
183         aggregateCostReport, err := ioutil.ReadFile(matches[1])
184         c.Assert(err, check.IsNil)
185
186         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,0.01492030")
187 }
188
189 func (*Suite) TestContainerRequestUUID(c *check.C) {
190         var stdout, stderr bytes.Buffer
191         resultsDir := c.MkDir()
192         // Run costanalyzer with 1 container request uuid
193         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID}, &bytes.Buffer{}, &stdout, &stderr)
194         c.Check(exitcode, check.Equals, 0)
195         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
196
197         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
198         c.Assert(err, check.IsNil)
199         // Make sure the 'preemptible' flag was picked up
200         c.Check(string(uuidReport), check.Matches, "(?ms).*,Standard_E4s_v3,true,.*")
201         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
202         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
203         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
204
205         aggregateCostReport, err := ioutil.ReadFile(matches[1])
206         c.Assert(err, check.IsNil)
207
208         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,7.01302889")
209 }
210
211 func (*Suite) TestCollectionUUID(c *check.C) {
212         var stdout, stderr bytes.Buffer
213
214         resultsDir := c.MkDir()
215         // Run costanalyzer with 1 collection uuid, without 'container_request' property
216         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.FooCollection}, &bytes.Buffer{}, &stdout, &stderr)
217         c.Check(exitcode, check.Equals, 2)
218         c.Assert(stderr.String(), check.Matches, "(?ms).*does not have a 'container_request' property.*")
219
220         // Update the collection, attach a 'container_request' property
221         ac := arvados.NewClientFromEnv()
222         var coll arvados.Collection
223
224         // Update collection record
225         err := ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+arvadostest.FooCollection, nil, map[string]interface{}{
226                 "collection": map[string]interface{}{
227                         "properties": map[string]interface{}{
228                                 "container_request": arvadostest.CompletedContainerRequestUUID,
229                         },
230                 },
231         })
232         c.Assert(err, check.IsNil)
233
234         stdout.Truncate(0)
235         stderr.Truncate(0)
236
237         // Run costanalyzer with 1 collection uuid
238         resultsDir = c.MkDir()
239         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.FooCollection}, &bytes.Buffer{}, &stdout, &stderr)
240         c.Check(exitcode, check.Equals, 0)
241         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
242
243         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
244         c.Assert(err, check.IsNil)
245         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
246         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
247         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
248
249         aggregateCostReport, err := ioutil.ReadFile(matches[1])
250         c.Assert(err, check.IsNil)
251
252         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,7.01302889")
253 }
254
255 func (*Suite) TestDoubleContainerRequestUUID(c *check.C) {
256         var stdout, stderr bytes.Buffer
257         resultsDir := c.MkDir()
258         // Run costanalyzer with 2 container request uuids
259         exitcode := Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedContainerRequestUUID, arvadostest.CompletedContainerRequestUUID2}, &bytes.Buffer{}, &stdout, &stderr)
260         c.Check(exitcode, check.Equals, 0)
261         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
262
263         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
264         c.Assert(err, check.IsNil)
265         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
266
267         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
268         c.Assert(err, check.IsNil)
269         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
270
271         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
272         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
273
274         aggregateCostReport, err := ioutil.ReadFile(matches[1])
275         c.Assert(err, check.IsNil)
276
277         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
278         stdout.Truncate(0)
279         stderr.Truncate(0)
280
281         // Now move both container requests into an existing project, and then re-run
282         // the analysis with the project uuid. The results should be identical.
283         ac := arvados.NewClientFromEnv()
284         var cr arvados.ContainerRequest
285         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID, nil, map[string]interface{}{
286                 "container_request": map[string]interface{}{
287                         "owner_uuid": arvadostest.AProjectUUID,
288                 },
289         })
290         c.Assert(err, check.IsNil)
291         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID2, nil, map[string]interface{}{
292                 "container_request": map[string]interface{}{
293                         "owner_uuid": arvadostest.AProjectUUID,
294                 },
295         })
296         c.Assert(err, check.IsNil)
297
298         // Run costanalyzer with the project uuid
299         resultsDir = c.MkDir()
300         exitcode = Command.RunCommand("costanalyzer.test", []string{"-cache=false", "-log-level", "debug", "-output", resultsDir, arvadostest.AProjectUUID}, &bytes.Buffer{}, &stdout, &stderr)
301         c.Check(exitcode, check.Equals, 0)
302         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
303
304         uuidReport, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID + ".csv")
305         c.Assert(err, check.IsNil)
306         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
307
308         uuidReport2, err = ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
309         c.Assert(err, check.IsNil)
310         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
311
312         re = regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
313         matches = re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
314
315         aggregateCostReport, err = ioutil.ReadFile(matches[1])
316         c.Assert(err, check.IsNil)
317
318         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
319 }
320
321 func (*Suite) TestUncommittedContainerRequest(c *check.C) {
322         var stdout, stderr bytes.Buffer
323         // Run costanalyzer with 2 container request uuids, one of which is in the Uncommitted state, without output directory specified
324         exitcode := Command.RunCommand("costanalyzer.test", []string{arvadostest.UncommittedContainerRequestUUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
325         c.Check(exitcode, check.Equals, 0)
326         c.Assert(stderr.String(), check.Not(check.Matches), "(?ms).*supplied uuids in .*")
327         c.Assert(stderr.String(), check.Matches, "(?ms).*No container associated with container request .*")
328
329         // Check that the total amount was printed to stdout
330         c.Check(stdout.String(), check.Matches, "0.00588088\n")
331 }
332
333 func (*Suite) TestMultipleContainerRequestUUIDWithReuse(c *check.C) {
334         var stdout, stderr bytes.Buffer
335         // Run costanalyzer with 2 container request uuids, without output directory specified
336         exitcode := Command.RunCommand("costanalyzer.test", []string{arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
337         c.Check(exitcode, check.Equals, 0)
338         c.Assert(stderr.String(), check.Not(check.Matches), "(?ms).*supplied uuids in .*")
339
340         // Check that the total amount was printed to stdout
341         c.Check(stdout.String(), check.Matches, "0.01492030\n")
342
343         stdout.Truncate(0)
344         stderr.Truncate(0)
345
346         // Run costanalyzer with 2 container request uuids
347         resultsDir := c.MkDir()
348         exitcode = Command.RunCommand("costanalyzer.test", []string{"-output", resultsDir, arvadostest.CompletedDiagnosticsContainerRequest1UUID, arvadostest.CompletedDiagnosticsContainerRequest2UUID}, &bytes.Buffer{}, &stdout, &stderr)
349         c.Check(exitcode, check.Equals, 0)
350         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
351
352         uuidReport, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
353         c.Assert(err, check.IsNil)
354         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00916192")
355
356         uuidReport2, err := ioutil.ReadFile(resultsDir + "/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
357         c.Assert(err, check.IsNil)
358         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00588088")
359
360         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
361         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
362
363         aggregateCostReport, err := ioutil.ReadFile(matches[1])
364         c.Assert(err, check.IsNil)
365
366         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,0.01492030")
367 }