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