Merge branch '17150-system-root-token'
[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": false
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 createNodeJSON(c *check.C, arv *arvadosclient.ArvadosClient, ac *arvados.Client, kc *keepclient.KeepClient, crUUID string, logUUID string, nodeJSON string) {
118         // Get the CR
119         var cr arvados.ContainerRequest
120         err := ac.RequestAndDecode(&cr, "GET", "arvados/v1/container_requests/"+crUUID, nil, nil)
121         c.Assert(err, check.Equals, nil)
122         c.Assert(cr.LogUUID, check.Equals, logUUID)
123
124         // Get the log collection
125         var coll arvados.Collection
126         err = ac.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+cr.LogUUID, nil, nil)
127         c.Assert(err, check.IsNil)
128
129         // Create a node.json file -- the fixture doesn't actually contain the contents of the collection.
130         fs, err := coll.FileSystem(ac, kc)
131         c.Assert(err, check.IsNil)
132         f, err := fs.OpenFile("node.json", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0777)
133         c.Assert(err, check.IsNil)
134         _, err = io.WriteString(f, nodeJSON)
135         c.Assert(err, check.IsNil)
136         err = f.Close()
137         c.Assert(err, check.IsNil)
138
139         // Flush the data to Keep
140         mtxt, err := fs.MarshalManifest(".")
141         c.Assert(err, check.IsNil)
142         c.Assert(mtxt, check.NotNil)
143
144         // Update collection record
145         err = ac.RequestAndDecode(&coll, "PUT", "arvados/v1/collections/"+cr.LogUUID, nil, map[string]interface{}{
146                 "collection": map[string]interface{}{
147                         "manifest_text": mtxt,
148                 },
149         })
150         c.Assert(err, check.IsNil)
151 }
152
153 func (*Suite) TestUsage(c *check.C) {
154         var stdout, stderr bytes.Buffer
155         exitcode := Command.RunCommand("costanalyzer.test", []string{"-help", "-log-level=debug"}, &bytes.Buffer{}, &stdout, &stderr)
156         c.Check(exitcode, check.Equals, 1)
157         c.Check(stdout.String(), check.Equals, "")
158         c.Check(stderr.String(), check.Matches, `(?ms).*Usage:.*`)
159 }
160
161 func (*Suite) TestContainerRequestUUID(c *check.C) {
162         var stdout, stderr bytes.Buffer
163         // Run costanalyzer with 1 container request uuid
164         exitcode := Command.RunCommand("costanalyzer.test", []string{"-uuid", arvadostest.CompletedContainerRequestUUID, "-output", "results"}, &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("results/" + arvadostest.CompletedContainerRequestUUID + ".csv")
169         c.Assert(err, check.IsNil)
170         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
171         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
172         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
173
174         aggregateCostReport, err := ioutil.ReadFile(matches[1])
175         c.Assert(err, check.IsNil)
176
177         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,7.01302889")
178 }
179
180 func (*Suite) TestDoubleContainerRequestUUID(c *check.C) {
181         var stdout, stderr bytes.Buffer
182         // Run costanalyzer with 2 container request uuids
183         exitcode := Command.RunCommand("costanalyzer.test", []string{"-uuid", arvadostest.CompletedContainerRequestUUID, "-uuid", arvadostest.CompletedContainerRequestUUID2, "-output", "results"}, &bytes.Buffer{}, &stdout, &stderr)
184         c.Check(exitcode, check.Equals, 0)
185         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
186
187         uuidReport, err := ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID + ".csv")
188         c.Assert(err, check.IsNil)
189         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
190
191         uuidReport2, err := ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
192         c.Assert(err, check.IsNil)
193         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
194
195         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
196         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
197
198         aggregateCostReport, err := ioutil.ReadFile(matches[1])
199         c.Assert(err, check.IsNil)
200
201         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
202         stdout.Truncate(0)
203         stderr.Truncate(0)
204
205         // Now move both container requests into an existing project, and then re-run
206         // the analysis with the project uuid. The results should be identical.
207         ac := arvados.NewClientFromEnv()
208         var cr arvados.ContainerRequest
209         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID, nil, map[string]interface{}{
210                 "container_request": map[string]interface{}{
211                         "owner_uuid": arvadostest.AProjectUUID,
212                 },
213         })
214         c.Assert(err, check.IsNil)
215         err = ac.RequestAndDecode(&cr, "PUT", "arvados/v1/container_requests/"+arvadostest.CompletedContainerRequestUUID2, nil, map[string]interface{}{
216                 "container_request": map[string]interface{}{
217                         "owner_uuid": arvadostest.AProjectUUID,
218                 },
219         })
220         c.Assert(err, check.IsNil)
221
222         // Run costanalyzer with the project uuid
223         exitcode = Command.RunCommand("costanalyzer.test", []string{"-uuid", arvadostest.AProjectUUID, "-cache=false", "-log-level", "debug", "-output", "results"}, &bytes.Buffer{}, &stdout, &stderr)
224         c.Check(exitcode, check.Equals, 0)
225         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
226
227         uuidReport, err = ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID + ".csv")
228         c.Assert(err, check.IsNil)
229         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,7.01302889")
230
231         uuidReport2, err = ioutil.ReadFile("results/" + arvadostest.CompletedContainerRequestUUID2 + ".csv")
232         c.Assert(err, check.IsNil)
233         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,42.27031111")
234
235         re = regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
236         matches = re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
237
238         aggregateCostReport, err = ioutil.ReadFile(matches[1])
239         c.Assert(err, check.IsNil)
240
241         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,49.28334000")
242 }
243
244 func (*Suite) TestMultipleContainerRequestUUIDWithReuse(c *check.C) {
245         var stdout, stderr bytes.Buffer
246         // Run costanalyzer with 2 container request uuids
247         exitcode := Command.RunCommand("costanalyzer.test", []string{"-uuid", arvadostest.CompletedDiagnosticsContainerRequest1UUID, "-uuid", arvadostest.CompletedDiagnosticsContainerRequest2UUID, "-output", "results"}, &bytes.Buffer{}, &stdout, &stderr)
248         c.Check(exitcode, check.Equals, 0)
249         c.Assert(stderr.String(), check.Matches, "(?ms).*supplied uuids in .*")
250
251         uuidReport, err := ioutil.ReadFile("results/" + arvadostest.CompletedDiagnosticsContainerRequest1UUID + ".csv")
252         c.Assert(err, check.IsNil)
253         c.Check(string(uuidReport), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00916192")
254
255         uuidReport2, err := ioutil.ReadFile("results/" + arvadostest.CompletedDiagnosticsContainerRequest2UUID + ".csv")
256         c.Assert(err, check.IsNil)
257         c.Check(string(uuidReport2), check.Matches, "(?ms).*TOTAL,,,,,,,,,0.00588088")
258
259         re := regexp.MustCompile(`(?ms).*supplied uuids in (.*?)\n`)
260         matches := re.FindStringSubmatch(stderr.String()) // matches[1] contains a string like 'results/2020-11-02-18-57-45-aggregate-costaccounting.csv'
261
262         aggregateCostReport, err := ioutil.ReadFile(matches[1])
263         c.Assert(err, check.IsNil)
264
265         c.Check(string(aggregateCostReport), check.Matches, "(?ms).*TOTAL,0.01492030")
266 }