18874: Merge branch 'main' into 18874-merge-wb2
[arvados.git] / lib / config / load_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package config
6
7 import (
8         "bytes"
9         "fmt"
10         "io"
11         "io/ioutil"
12         "os"
13         "os/exec"
14         "reflect"
15         "regexp"
16         "runtime"
17         "strings"
18         "testing"
19         "time"
20
21         "git.arvados.org/arvados.git/sdk/go/arvados"
22         "git.arvados.org/arvados.git/sdk/go/arvadostest"
23         "git.arvados.org/arvados.git/sdk/go/ctxlog"
24         "github.com/ghodss/yaml"
25         "github.com/prometheus/client_golang/prometheus"
26         "github.com/sirupsen/logrus"
27         "golang.org/x/sys/unix"
28         check "gopkg.in/check.v1"
29 )
30
31 // Gocheck boilerplate
32 func Test(t *testing.T) {
33         check.TestingT(t)
34 }
35
36 var _ = check.Suite(&LoadSuite{})
37
38 var emptyConfigYAML = `Clusters: {"z1111": {}}`
39
40 // Return a new Loader that reads cluster config from configdata
41 // (instead of the usual default /etc/arvados/config.yml), and logs to
42 // logdst or (if that's nil) c.Log.
43 func testLoader(c *check.C, configdata string, logdst io.Writer) *Loader {
44         logger := ctxlog.TestLogger(c)
45         if logdst != nil {
46                 lgr := logrus.New()
47                 lgr.Out = logdst
48                 logger = lgr
49         }
50         ldr := NewLoader(bytes.NewBufferString(configdata), logger)
51         ldr.Path = "-"
52         return ldr
53 }
54
55 type LoadSuite struct{}
56
57 func (s *LoadSuite) SetUpSuite(c *check.C) {
58         os.Unsetenv("ARVADOS_API_HOST")
59         os.Unsetenv("ARVADOS_API_HOST_INSECURE")
60         os.Unsetenv("ARVADOS_API_TOKEN")
61 }
62
63 func (s *LoadSuite) TestEmpty(c *check.C) {
64         cfg, err := testLoader(c, "", nil).Load()
65         c.Check(cfg, check.IsNil)
66         c.Assert(err, check.ErrorMatches, `config does not define any clusters`)
67 }
68
69 func (s *LoadSuite) TestNoConfigs(c *check.C) {
70         cfg, err := testLoader(c, emptyConfigYAML, nil).Load()
71         c.Assert(err, check.IsNil)
72         c.Assert(cfg.Clusters, check.HasLen, 1)
73         cc, err := cfg.GetCluster("z1111")
74         c.Assert(err, check.IsNil)
75         c.Check(cc.ClusterID, check.Equals, "z1111")
76         c.Check(cc.API.MaxRequestAmplification, check.Equals, 4)
77         c.Check(cc.API.MaxItemsPerResponse, check.Equals, 1000)
78 }
79
80 func (s *LoadSuite) TestNullKeyDoesNotOverrideDefault(c *check.C) {
81         ldr := testLoader(c, `{"Clusters":{"z1111":{"API":}}}`, nil)
82         ldr.SkipDeprecated = true
83         cfg, err := ldr.Load()
84         c.Assert(err, check.IsNil)
85         c1, err := cfg.GetCluster("z1111")
86         c.Assert(err, check.IsNil)
87         c.Check(c1.ClusterID, check.Equals, "z1111")
88         c.Check(c1.API.MaxRequestAmplification, check.Equals, 4)
89         c.Check(c1.API.MaxItemsPerResponse, check.Equals, 1000)
90 }
91
92 func (s *LoadSuite) TestMungeLegacyConfigArgs(c *check.C) {
93         f, err := ioutil.TempFile("", "")
94         c.Check(err, check.IsNil)
95         defer os.Remove(f.Name())
96         io.WriteString(f, "Debug: true\n")
97         oldfile := f.Name()
98
99         f, err = ioutil.TempFile("", "")
100         c.Check(err, check.IsNil)
101         defer os.Remove(f.Name())
102         io.WriteString(f, emptyConfigYAML)
103         newfile := f.Name()
104
105         for _, trial := range []struct {
106                 argsIn  []string
107                 argsOut []string
108         }{
109                 {
110                         []string{"-config", oldfile},
111                         []string{"-old-config", oldfile},
112                 },
113                 {
114                         []string{"-config=" + oldfile},
115                         []string{"-old-config=" + oldfile},
116                 },
117                 {
118                         []string{"-config", newfile},
119                         []string{"-config", newfile},
120                 },
121                 {
122                         []string{"-config=" + newfile},
123                         []string{"-config=" + newfile},
124                 },
125                 {
126                         []string{"-foo", oldfile},
127                         []string{"-foo", oldfile},
128                 },
129                 {
130                         []string{"-foo=" + oldfile},
131                         []string{"-foo=" + oldfile},
132                 },
133                 {
134                         []string{"-foo", "-config=" + oldfile},
135                         []string{"-foo", "-old-config=" + oldfile},
136                 },
137                 {
138                         []string{"-foo", "bar", "-config=" + oldfile},
139                         []string{"-foo", "bar", "-old-config=" + oldfile},
140                 },
141                 {
142                         []string{"-foo=bar", "baz", "-config=" + oldfile},
143                         []string{"-foo=bar", "baz", "-old-config=" + oldfile},
144                 },
145                 {
146                         []string{"-config=/dev/null"},
147                         []string{"-config=/dev/null"},
148                 },
149                 {
150                         []string{"-config=-"},
151                         []string{"-config=-"},
152                 },
153                 {
154                         []string{"-config="},
155                         []string{"-config="},
156                 },
157                 {
158                         []string{"-foo=bar", "baz", "-config"},
159                         []string{"-foo=bar", "baz", "-config"},
160                 },
161                 {
162                         []string{},
163                         nil,
164                 },
165         } {
166                 var logbuf bytes.Buffer
167                 logger := logrus.New()
168                 logger.Out = &logbuf
169
170                 var ldr Loader
171                 args := ldr.MungeLegacyConfigArgs(logger, trial.argsIn, "-old-config")
172                 c.Check(args, check.DeepEquals, trial.argsOut)
173                 if fmt.Sprintf("%v", trial.argsIn) != fmt.Sprintf("%v", trial.argsOut) {
174                         c.Check(logbuf.String(), check.Matches, `.*`+oldfile+` is not a cluster config file -- interpreting -config as -old-config.*\n`)
175                 }
176         }
177 }
178
179 func (s *LoadSuite) TestSampleKeys(c *check.C) {
180         for _, yaml := range []string{
181                 `{"Clusters":{"z1111":{}}}`,
182                 `{"Clusters":{"z1111":{"InstanceTypes":{"Foo":{"RAM": "12345M"}}}}}`,
183         } {
184                 cfg, err := testLoader(c, yaml, nil).Load()
185                 c.Assert(err, check.IsNil)
186                 cc, err := cfg.GetCluster("z1111")
187                 c.Assert(err, check.IsNil)
188                 _, hasSample := cc.InstanceTypes["SAMPLE"]
189                 c.Check(hasSample, check.Equals, false)
190                 if strings.Contains(yaml, "Foo") {
191                         c.Check(cc.InstanceTypes["Foo"].RAM, check.Equals, arvados.ByteSize(12345000000))
192                         c.Check(cc.InstanceTypes["Foo"].Price, check.Equals, 0.0)
193                 }
194         }
195 }
196
197 func (s *LoadSuite) TestMultipleClusters(c *check.C) {
198         ldr := testLoader(c, `{"Clusters":{"z1111":{},"z2222":{}}}`, nil)
199         ldr.SkipDeprecated = true
200         cfg, err := ldr.Load()
201         c.Assert(err, check.IsNil)
202         c1, err := cfg.GetCluster("z1111")
203         c.Assert(err, check.IsNil)
204         c.Check(c1.ClusterID, check.Equals, "z1111")
205         c2, err := cfg.GetCluster("z2222")
206         c.Assert(err, check.IsNil)
207         c.Check(c2.ClusterID, check.Equals, "z2222")
208 }
209
210 func (s *LoadSuite) TestDeprecatedOrUnknownWarning(c *check.C) {
211         var logbuf bytes.Buffer
212         _, err := testLoader(c, `
213 Clusters:
214   zzzzz:
215     ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
216     SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
217     Collections:
218      BlobSigningKey: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
219     PostgreSQL: {}
220     BadKey1: {}
221     Containers:
222       RunTimeEngine: abc
223     RemoteClusters:
224       z2222:
225         Host: z2222.arvadosapi.com
226         Proxy: true
227         BadKey2: badValue
228     Services:
229       KeepStore:
230         InternalURLs:
231           "http://host.example:12345": {}
232       Keepstore:
233         InternalURLs:
234           "http://host.example:12345":
235             RendezVous: x
236     ServiceS:
237       Keepstore:
238         InternalURLs:
239           "http://host.example:12345": {}
240     Volumes:
241       zzzzz-nyw5e-aaaaaaaaaaaaaaa: {Replication: 2}
242 `, &logbuf).Load()
243         c.Assert(err, check.IsNil)
244         c.Log(logbuf.String())
245         logs := strings.Split(strings.TrimSuffix(logbuf.String(), "\n"), "\n")
246         for _, log := range logs {
247                 c.Check(log, check.Matches, `.*deprecated or unknown config entry:.*(RunTimeEngine.*RuntimeEngine|BadKey1|BadKey2|KeepStore|ServiceS|RendezVous).*`)
248         }
249         c.Check(logs, check.HasLen, 6)
250 }
251
252 func (s *LoadSuite) checkSAMPLEKeys(c *check.C, path string, x interface{}) {
253         v := reflect.Indirect(reflect.ValueOf(x))
254         switch v.Kind() {
255         case reflect.Map:
256                 var stringKeys, sampleKey bool
257                 iter := v.MapRange()
258                 for iter.Next() {
259                         k := iter.Key()
260                         if k.Kind() == reflect.String {
261                                 stringKeys = true
262                                 if k.String() == "SAMPLE" || k.String() == "xxxxx" {
263                                         sampleKey = true
264                                         s.checkSAMPLEKeys(c, path+"."+k.String(), iter.Value().Interface())
265                                 }
266                         }
267                 }
268                 if stringKeys && !sampleKey {
269                         c.Errorf("%s is a map with string keys (type %T) but config.default.yml has no SAMPLE key", path, x)
270                 }
271                 return
272         case reflect.Struct:
273                 for i := 0; i < v.NumField(); i++ {
274                         val := v.Field(i)
275                         if val.CanInterface() {
276                                 s.checkSAMPLEKeys(c, path+"."+v.Type().Field(i).Name, val.Interface())
277                         }
278                 }
279         }
280 }
281
282 func (s *LoadSuite) TestDefaultConfigHasAllSAMPLEKeys(c *check.C) {
283         var logbuf bytes.Buffer
284         loader := testLoader(c, string(DefaultYAML), &logbuf)
285         cfg, err := loader.Load()
286         c.Assert(err, check.IsNil)
287         s.checkSAMPLEKeys(c, "", cfg)
288 }
289
290 func (s *LoadSuite) TestNoUnrecognizedKeysInDefaultConfig(c *check.C) {
291         var logbuf bytes.Buffer
292         var supplied map[string]interface{}
293         yaml.Unmarshal(DefaultYAML, &supplied)
294
295         loader := testLoader(c, string(DefaultYAML), &logbuf)
296         cfg, err := loader.Load()
297         c.Assert(err, check.IsNil)
298         var loaded map[string]interface{}
299         buf, err := yaml.Marshal(cfg)
300         c.Assert(err, check.IsNil)
301         err = yaml.Unmarshal(buf, &loaded)
302         c.Assert(err, check.IsNil)
303
304         c.Check(logbuf.String(), check.Matches, `(?ms).*SystemRootToken: secret token is not set.*`)
305         c.Check(logbuf.String(), check.Matches, `(?ms).*ManagementToken: secret token is not set.*`)
306         c.Check(logbuf.String(), check.Matches, `(?ms).*Collections.BlobSigningKey: secret token is not set.*`)
307         logbuf.Reset()
308         loader.logExtraKeys(loaded, supplied, "")
309         c.Check(logbuf.String(), check.Equals, "")
310 }
311
312 func (s *LoadSuite) TestNoWarningsForDumpedConfig(c *check.C) {
313         var logbuf bytes.Buffer
314         cfg, err := testLoader(c, `
315 Clusters:
316  zzzzz:
317   ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
318   SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
319   Collections:
320    BlobSigningKey: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
321   InstanceTypes:
322    abc:
323     IncludedScratch: 123456
324 `, &logbuf).Load()
325         c.Assert(err, check.IsNil)
326         yaml, err := yaml.Marshal(cfg)
327         c.Assert(err, check.IsNil)
328         // Well, *nearly* no warnings. SourceTimestamp and
329         // SourceSHA256 are included in a config-dump, but not
330         // expected in a real config file.
331         yaml = regexp.MustCompile(`(^|\n)(Source(Timestamp|SHA256): .*?\n)+`).ReplaceAll(yaml, []byte("$1"))
332         cfgDumped, err := testLoader(c, string(yaml), &logbuf).Load()
333         c.Assert(err, check.IsNil)
334         // SourceTimestamp and SourceSHA256 aren't expected to be
335         // preserved through dump+load
336         cfgDumped.SourceTimestamp = cfg.SourceTimestamp
337         cfgDumped.SourceSHA256 = cfg.SourceSHA256
338         c.Check(cfg, check.DeepEquals, cfgDumped)
339         c.Check(logbuf.String(), check.Equals, "")
340 }
341
342 func (s *LoadSuite) TestUnacceptableTokens(c *check.C) {
343         for _, trial := range []struct {
344                 short      bool
345                 configPath string
346                 example    string
347         }{
348                 {false, "SystemRootToken", "SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_b_c"},
349                 {false, "ManagementToken", "ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b c"},
350                 {false, "ManagementToken", "ManagementToken: \"$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\""},
351                 {false, "Collections.BlobSigningKey", "Collections: {BlobSigningKey: \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa⛵\"}"},
352                 {true, "SystemRootToken", "SystemRootToken: a_b_c"},
353                 {true, "ManagementToken", "ManagementToken: a b c"},
354                 {true, "ManagementToken", "ManagementToken: \"$abc\""},
355                 {true, "Collections.BlobSigningKey", "Collections: {BlobSigningKey: \"⛵\"}"},
356         } {
357                 c.Logf("trying bogus config: %s", trial.example)
358                 _, err := testLoader(c, "Clusters:\n zzzzz:\n  "+trial.example, nil).Load()
359                 c.Check(err, check.ErrorMatches, `Clusters.zzzzz.`+trial.configPath+`: unacceptable characters in token.*`)
360         }
361 }
362
363 func (s *LoadSuite) TestPostgreSQLKeyConflict(c *check.C) {
364         _, err := testLoader(c, `
365 Clusters:
366  zzzzz:
367   PostgreSQL:
368    Connection:
369      DBName: dbname
370      Host: host
371 `, nil).Load()
372         c.Check(err, check.ErrorMatches, `Clusters.zzzzz.PostgreSQL.Connection: multiple entries for "(dbname|host)".*`)
373 }
374
375 func (s *LoadSuite) TestBadClusterIDs(c *check.C) {
376         for _, data := range []string{`
377 Clusters:
378  123456:
379   ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
380   SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
381   Collections:
382    BlobSigningKey: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
383 `, `
384 Clusters:
385  12345:
386   ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
387   SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
388   Collections:
389    BlobSigningKey: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
390   RemoteClusters:
391    Zzzzz:
392     Host: Zzzzz.arvadosapi.com
393     Proxy: true
394 `, `
395 Clusters:
396  abcde:
397   ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
398   SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
399   Collections:
400    BlobSigningKey: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
401   Login:
402    LoginCluster: zz-zz
403 `,
404         } {
405                 c.Log(data)
406                 v, err := testLoader(c, data, nil).Load()
407                 if v != nil {
408                         c.Logf("%#v", v.Clusters)
409                 }
410                 c.Check(err, check.ErrorMatches, `.*cluster ID should be 5 lowercase alphanumeric characters.*`)
411         }
412 }
413
414 func (s *LoadSuite) TestBadType(c *check.C) {
415         for _, data := range []string{`
416 Clusters:
417  zzzzz:
418   PostgreSQL: true
419 `, `
420 Clusters:
421  zzzzz:
422   PostgreSQL:
423    ConnectionPool: true
424 `, `
425 Clusters:
426  zzzzz:
427   PostgreSQL:
428    ConnectionPool: "foo"
429 `, `
430 Clusters:
431  zzzzz:
432   PostgreSQL:
433    ConnectionPool: []
434 `, `
435 Clusters:
436  zzzzz:
437   PostgreSQL:
438    ConnectionPool: [] # {foo: bar} isn't caught here; we rely on config-check
439 `,
440         } {
441                 c.Log(data)
442                 v, err := testLoader(c, data, nil).Load()
443                 if v != nil {
444                         c.Logf("%#v", v.Clusters["zzzzz"].PostgreSQL.ConnectionPool)
445                 }
446                 c.Check(err, check.ErrorMatches, `.*cannot unmarshal .*PostgreSQL.*`)
447         }
448 }
449
450 func (s *LoadSuite) TestMovedKeys(c *check.C) {
451         checkEquivalent(c, `# config has old keys only
452 Clusters:
453  zzzzz:
454   RequestLimits:
455    MultiClusterRequestConcurrency: 3
456    MaxItemsPerResponse: 999
457 `, `
458 Clusters:
459  zzzzz:
460   API:
461    MaxRequestAmplification: 3
462    MaxItemsPerResponse: 999
463 `)
464         checkEquivalent(c, `# config has both old and new keys; old values win
465 Clusters:
466  zzzzz:
467   RequestLimits:
468    MultiClusterRequestConcurrency: 0
469    MaxItemsPerResponse: 555
470   API:
471    MaxRequestAmplification: 3
472    MaxItemsPerResponse: 999
473 `, `
474 Clusters:
475  zzzzz:
476   API:
477    MaxRequestAmplification: 0
478    MaxItemsPerResponse: 555
479 `)
480 }
481
482 func checkEquivalent(c *check.C, goty, expectedy string) string {
483         var logbuf bytes.Buffer
484         gotldr := testLoader(c, goty, &logbuf)
485         expectedldr := testLoader(c, expectedy, nil)
486         checkEquivalentLoaders(c, gotldr, expectedldr)
487         return logbuf.String()
488 }
489
490 func checkEqualYAML(c *check.C, got, expected interface{}) {
491         expectedyaml, err := yaml.Marshal(expected)
492         c.Assert(err, check.IsNil)
493         gotyaml, err := yaml.Marshal(got)
494         c.Assert(err, check.IsNil)
495         if !bytes.Equal(gotyaml, expectedyaml) {
496                 cmd := exec.Command("diff", "-u", "--label", "expected", "--label", "got", "/dev/fd/3", "/dev/fd/4")
497                 for _, y := range [][]byte{expectedyaml, gotyaml} {
498                         pr, pw, err := os.Pipe()
499                         c.Assert(err, check.IsNil)
500                         defer pr.Close()
501                         go func(data []byte) {
502                                 pw.Write(data)
503                                 pw.Close()
504                         }(y)
505                         cmd.ExtraFiles = append(cmd.ExtraFiles, pr)
506                 }
507                 diff, err := cmd.CombinedOutput()
508                 // diff should report differences and exit non-zero.
509                 c.Check(err, check.NotNil)
510                 c.Log(string(diff))
511                 c.Error("got != expected; see diff (-expected +got) above")
512         }
513 }
514
515 func checkEquivalentLoaders(c *check.C, gotldr, expectedldr *Loader) {
516         got, err := gotldr.Load()
517         c.Assert(err, check.IsNil)
518         expected, err := expectedldr.Load()
519         c.Assert(err, check.IsNil)
520         // The inputs generally aren't even files, so SourceTimestamp
521         // can't be expected to match.
522         got.SourceTimestamp = expected.SourceTimestamp
523         // Obviously the content isn't identical -- otherwise we
524         // wouldn't need to check that it's equivalent.
525         got.SourceSHA256 = expected.SourceSHA256
526         checkEqualYAML(c, got, expected)
527 }
528
529 func checkListKeys(path string, x interface{}) (err error) {
530         v := reflect.Indirect(reflect.ValueOf(x))
531         switch v.Kind() {
532         case reflect.Map:
533                 iter := v.MapRange()
534                 for iter.Next() {
535                         k := iter.Key()
536                         if k.Kind() == reflect.String {
537                                 if err = checkListKeys(path+"."+k.String(), iter.Value().Interface()); err != nil {
538                                         return
539                                 }
540                         }
541                 }
542                 return
543
544         case reflect.Struct:
545                 for i := 0; i < v.NumField(); i++ {
546                         val := v.Field(i)
547                         structField := v.Type().Field(i)
548                         fieldname := structField.Name
549                         endsWithList := strings.HasSuffix(fieldname, "List")
550                         isAnArray := structField.Type.Kind() == reflect.Slice
551                         if endsWithList != isAnArray {
552                                 if endsWithList {
553                                         err = fmt.Errorf("%s.%s ends with 'List' but field is not an array (type %v)", path, fieldname, val.Kind())
554                                         return
555                                 }
556                                 if isAnArray && structField.Type.Elem().Kind() != reflect.Uint8 {
557                                         err = fmt.Errorf("%s.%s is an array but field name does not end in 'List' (slice of %v)", path, fieldname, structField.Type.Elem().Kind())
558                                         return
559                                 }
560                         }
561                         if val.CanInterface() {
562                                 checkListKeys(path+"."+fieldname, val.Interface())
563                         }
564                 }
565         }
566         return
567 }
568
569 func (s *LoadSuite) TestListKeys(c *check.C) {
570         v1 := struct {
571                 EndInList []string
572         }{[]string{"a", "b"}}
573         var m1 = make(map[string]interface{})
574         m1["c"] = &v1
575         if err := checkListKeys("", m1); err != nil {
576                 c.Error(err)
577         }
578
579         v2 := struct {
580                 DoesNot []string
581         }{[]string{"a", "b"}}
582         var m2 = make(map[string]interface{})
583         m2["c"] = &v2
584         if err := checkListKeys("", m2); err == nil {
585                 c.Errorf("Should have produced an error")
586         }
587
588         v3 := struct {
589                 EndInList string
590         }{"a"}
591         var m3 = make(map[string]interface{})
592         m3["c"] = &v3
593         if err := checkListKeys("", m3); err == nil {
594                 c.Errorf("Should have produced an error")
595         }
596
597         loader := testLoader(c, string(DefaultYAML), nil)
598         cfg, err := loader.Load()
599         c.Assert(err, check.IsNil)
600         if err := checkListKeys("", cfg); err != nil {
601                 c.Error(err)
602         }
603 }
604
605 func (s *LoadSuite) TestLoopbackInstanceTypes(c *check.C) {
606         ldr := testLoader(c, `
607 Clusters:
608  z1111:
609   Containers:
610    CloudVMs:
611     Enable: true
612     Driver: loopback
613   InstanceTypes:
614    a: {}
615    b: {}
616 `, nil)
617         cfg, err := ldr.Load()
618         c.Check(err, check.ErrorMatches, `Clusters\.z1111\.InstanceTypes: cannot use multiple InstanceTypes with loopback driver`)
619
620         ldr = testLoader(c, `
621 Clusters:
622  z1111:
623   Containers:
624    CloudVMs:
625     Enable: true
626     Driver: loopback
627 `, nil)
628         cfg, err = ldr.Load()
629         c.Assert(err, check.IsNil)
630         cc, err := cfg.GetCluster("")
631         c.Assert(err, check.IsNil)
632         c.Check(cc.InstanceTypes, check.HasLen, 1)
633         c.Check(cc.InstanceTypes["localhost"].VCPUs, check.Equals, runtime.NumCPU())
634
635         ldr = testLoader(c, `
636 Clusters:
637  z1111:
638   Containers:
639    CloudVMs:
640     Enable: true
641     Driver: loopback
642   InstanceTypes:
643    a:
644     VCPUs: 9
645 `, nil)
646         cfg, err = ldr.Load()
647         c.Assert(err, check.IsNil)
648         cc, err = cfg.GetCluster("")
649         c.Assert(err, check.IsNil)
650         c.Check(cc.InstanceTypes, check.HasLen, 1)
651         c.Check(cc.InstanceTypes["a"].VCPUs, check.Equals, 9)
652 }
653
654 func (s *LoadSuite) TestWarnUnusedLocalKeep(c *check.C) {
655         var logbuf bytes.Buffer
656         _, err := testLoader(c, `
657 Clusters:
658  z1111:
659   Volumes:
660    z:
661     Replication: 1
662 `, &logbuf).Load()
663         c.Assert(err, check.IsNil)
664         c.Check(logbuf.String(), check.Matches, `(?ms).*LocalKeepBlobBuffersPerVCPU is 1 but will not be used because at least one volume \(z\) has lower replication than DefaultReplication \(1 < 2\) -- suggest changing to 0.*`)
665
666         logbuf.Reset()
667         _, err = testLoader(c, `
668 Clusters:
669  z1111:
670   Volumes:
671    z:
672     AccessViaHosts:
673      "http://0.0.0.0:12345": {}
674 `, &logbuf).Load()
675         c.Assert(err, check.IsNil)
676         c.Check(logbuf.String(), check.Matches, `(?ms).*LocalKeepBlobBuffersPerVCPU is 1 but will not be used because at least one volume \(z\) uses AccessViaHosts -- suggest changing to 0.*`)
677 }
678
679 func (s *LoadSuite) TestImplicitStorageClasses(c *check.C) {
680         // If StorageClasses and Volumes.*.StorageClasses are all
681         // empty, there is a default storage class named "default".
682         ldr := testLoader(c, `{"Clusters":{"z1111":{}}}`, nil)
683         cfg, err := ldr.Load()
684         c.Assert(err, check.IsNil)
685         cc, err := cfg.GetCluster("z1111")
686         c.Assert(err, check.IsNil)
687         c.Check(cc.StorageClasses, check.HasLen, 1)
688         c.Check(cc.StorageClasses["default"].Default, check.Equals, true)
689         c.Check(cc.StorageClasses["default"].Priority, check.Equals, 0)
690
691         // The implicit "default" storage class is used by all
692         // volumes.
693         ldr = testLoader(c, `
694 Clusters:
695  z1111:
696   Volumes:
697    z: {}`, nil)
698         cfg, err = ldr.Load()
699         c.Assert(err, check.IsNil)
700         cc, err = cfg.GetCluster("z1111")
701         c.Assert(err, check.IsNil)
702         c.Check(cc.StorageClasses, check.HasLen, 1)
703         c.Check(cc.StorageClasses["default"].Default, check.Equals, true)
704         c.Check(cc.StorageClasses["default"].Priority, check.Equals, 0)
705         c.Check(cc.Volumes["z"].StorageClasses["default"], check.Equals, true)
706
707         // The "default" storage class isn't implicit if any classes
708         // are configured explicitly.
709         ldr = testLoader(c, `
710 Clusters:
711  z1111:
712   StorageClasses:
713    local:
714     Default: true
715     Priority: 111
716   Volumes:
717    z:
718     StorageClasses:
719      local: true`, nil)
720         cfg, err = ldr.Load()
721         c.Assert(err, check.IsNil)
722         cc, err = cfg.GetCluster("z1111")
723         c.Assert(err, check.IsNil)
724         c.Check(cc.StorageClasses, check.HasLen, 1)
725         c.Check(cc.StorageClasses["local"].Default, check.Equals, true)
726         c.Check(cc.StorageClasses["local"].Priority, check.Equals, 111)
727
728         // It is an error for a volume to refer to a storage class
729         // that isn't listed in StorageClasses.
730         ldr = testLoader(c, `
731 Clusters:
732  z1111:
733   StorageClasses:
734    local:
735     Default: true
736     Priority: 111
737   Volumes:
738    z:
739     StorageClasses:
740      nx: true`, nil)
741         _, err = ldr.Load()
742         c.Assert(err, check.ErrorMatches, `z: volume refers to storage class "nx" that is not defined.*`)
743
744         // It is an error for a volume to refer to a storage class
745         // that isn't listed in StorageClasses ... even if it's
746         // "default", which would exist implicitly if it weren't
747         // referenced explicitly by a volume.
748         ldr = testLoader(c, `
749 Clusters:
750  z1111:
751   Volumes:
752    z:
753     StorageClasses:
754      default: true`, nil)
755         _, err = ldr.Load()
756         c.Assert(err, check.ErrorMatches, `z: volume refers to storage class "default" that is not defined.*`)
757
758         // If the "default" storage class is configured explicitly, it
759         // is not used implicitly by any volumes, even if it's the
760         // only storage class.
761         var logbuf bytes.Buffer
762         ldr = testLoader(c, `
763 Clusters:
764  z1111:
765   StorageClasses:
766    default:
767     Default: true
768     Priority: 111
769   Volumes:
770    z: {}`, &logbuf)
771         _, err = ldr.Load()
772         c.Assert(err, check.ErrorMatches, `z: volume has no StorageClasses listed`)
773
774         // If StorageClasses are configured explicitly, there must be
775         // at least one with Default: true. (Calling one "default" is
776         // not sufficient.)
777         ldr = testLoader(c, `
778 Clusters:
779  z1111:
780   StorageClasses:
781    default:
782     Priority: 111
783   Volumes:
784    z:
785     StorageClasses:
786      default: true`, nil)
787         _, err = ldr.Load()
788         c.Assert(err, check.ErrorMatches, `there is no default storage class.*`)
789 }
790
791 func (s *LoadSuite) TestPreemptiblePriceFactor(c *check.C) {
792         yaml := `
793 Clusters:
794  z1111:
795   InstanceTypes:
796    Type1:
797     RAM: 12345M
798     VCPUs: 8
799     Price: 1.23
800  z2222:
801   Containers:
802    PreemptiblePriceFactor: 0.5
803   InstanceTypes:
804    Type1:
805     RAM: 12345M
806     VCPUs: 8
807     Price: 1.23
808  z3333:
809   Containers:
810    PreemptiblePriceFactor: 0.5
811   InstanceTypes:
812    Type1:
813     RAM: 12345M
814     VCPUs: 8
815     Price: 1.23
816    Type1.preemptible: # higher price than the auto-added variant would use -- should generate warning
817     ProviderType: Type1
818     RAM: 12345M
819     VCPUs: 8
820     Price: 1.23
821     Preemptible: true
822    Type2:
823     RAM: 23456M
824     VCPUs: 16
825     Price: 2.46
826    Type2.preemptible: # identical to the auto-added variant -- so no warning
827     ProviderType: Type2
828     RAM: 23456M
829     VCPUs: 16
830     Price: 1.23
831     Preemptible: true
832 `
833         var logbuf bytes.Buffer
834         cfg, err := testLoader(c, yaml, &logbuf).Load()
835         c.Assert(err, check.IsNil)
836         cc, err := cfg.GetCluster("z1111")
837         c.Assert(err, check.IsNil)
838         c.Check(cc.InstanceTypes["Type1"].Price, check.Equals, 1.23)
839         c.Check(cc.InstanceTypes, check.HasLen, 1)
840
841         cc, err = cfg.GetCluster("z2222")
842         c.Assert(err, check.IsNil)
843         c.Check(cc.InstanceTypes["Type1"].Preemptible, check.Equals, false)
844         c.Check(cc.InstanceTypes["Type1"].Price, check.Equals, 1.23)
845         c.Check(cc.InstanceTypes["Type1.preemptible"].Preemptible, check.Equals, true)
846         c.Check(cc.InstanceTypes["Type1.preemptible"].Price, check.Equals, 1.23/2)
847         c.Check(cc.InstanceTypes["Type1.preemptible"].ProviderType, check.Equals, "Type1")
848         c.Check(cc.InstanceTypes, check.HasLen, 2)
849
850         cc, err = cfg.GetCluster("z3333")
851         c.Assert(err, check.IsNil)
852         // Don't overwrite the explicitly configured preemptible variant
853         c.Check(cc.InstanceTypes["Type1.preemptible"].Price, check.Equals, 1.23)
854         c.Check(cc.InstanceTypes, check.HasLen, 4)
855         c.Check(logbuf.String(), check.Matches, `(?ms).*Clusters\.z3333\.InstanceTypes\[Type1\.preemptible\]: already exists, so not automatically adding a preemptible variant of Type1.*`)
856         c.Check(logbuf.String(), check.Not(check.Matches), `(?ms).*Type2\.preemptible.*`)
857         c.Check(logbuf.String(), check.Not(check.Matches), `(?ms).*(z1111|z2222)[^\n]*InstanceTypes.*`)
858 }
859
860 func (s *LoadSuite) TestSourceTimestamp(c *check.C) {
861         conftime, err := time.Parse(time.RFC3339, "2022-03-04T05:06:07-08:00")
862         c.Assert(err, check.IsNil)
863         confdata := `Clusters: {zzzzz: {}}`
864         conffile := c.MkDir() + "/config.yml"
865         ioutil.WriteFile(conffile, []byte(confdata), 0777)
866         tv := unix.NsecToTimeval(conftime.UnixNano())
867         unix.Lutimes(conffile, []unix.Timeval{tv, tv})
868         for _, trial := range []struct {
869                 configarg  string
870                 expectTime time.Time
871         }{
872                 {"-", time.Now()},
873                 {conffile, conftime},
874         } {
875                 c.Logf("trial: %+v", trial)
876                 ldr := NewLoader(strings.NewReader(confdata), ctxlog.TestLogger(c))
877                 ldr.Path = trial.configarg
878                 cfg, err := ldr.Load()
879                 c.Assert(err, check.IsNil)
880                 c.Check(cfg.SourceTimestamp, check.Equals, cfg.SourceTimestamp.UTC())
881                 c.Check(cfg.SourceTimestamp, check.Equals, ldr.sourceTimestamp)
882                 c.Check(int(cfg.SourceTimestamp.Sub(trial.expectTime).Seconds()), check.Equals, 0)
883                 c.Check(int(ldr.loadTimestamp.Sub(time.Now()).Seconds()), check.Equals, 0)
884
885                 reg := prometheus.NewRegistry()
886                 ldr.RegisterMetrics(reg)
887                 metrics := arvadostest.GatherMetricsAsString(reg)
888                 c.Check(metrics, check.Matches, `# HELP .*
889 # TYPE .*
890 arvados_config_load_timestamp_seconds{sha256="83aea5d82eb1d53372cd65c936c60acc1c6ef946e61977bbca7cfea709d201a8"} \Q`+fmt.Sprintf("%g", float64(ldr.loadTimestamp.UnixNano())/1e9)+`\E
891 # HELP .*
892 # TYPE .*
893 arvados_config_source_timestamp_seconds{sha256="83aea5d82eb1d53372cd65c936c60acc1c6ef946e61977bbca7cfea709d201a8"} \Q`+fmt.Sprintf("%g", float64(cfg.SourceTimestamp.UnixNano())/1e9)+`\E
894 `)
895         }
896 }
897
898 func (s *LoadSuite) TestGetHostRAM(c *check.C) {
899         hostram, err := getHostRAM()
900         c.Check(err, check.IsNil)
901         c.Logf("getHostRAM() == %v", hostram)
902 }
903
904 func (s *LoadSuite) TestGetFilesystemSize(c *check.C) {
905         path := c.MkDir()
906         size, err := getFilesystemSize(path)
907         c.Check(err, check.IsNil)
908         c.Logf("getFilesystemSize(%q) == %v", path, size)
909 }
910
911 func (s *LoadSuite) TestLoadSSHKey(c *check.C) {
912         cwd, err := os.Getwd()
913         c.Assert(err, check.IsNil)
914         _, err = LoadSSHKey("file://" + cwd + "/../dispatchcloud/test/sshkey_dispatch")
915         c.Check(err, check.IsNil)
916 }