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