19126: Merge branch 'main' into 19126-nginx-proxy-settings-change
[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
320   InstanceTypes:
321    abc:
322     IncludedScratch: 123456
323 `, &logbuf).Load()
324         c.Assert(err, check.IsNil)
325         yaml, err := yaml.Marshal(cfg)
326         c.Assert(err, check.IsNil)
327         // Well, *nearly* no warnings. SourceTimestamp and
328         // SourceSHA256 are included in a config-dump, but not
329         // expected in a real config file.
330         yaml = regexp.MustCompile(`(^|\n)(Source(Timestamp|SHA256): .*?\n)+`).ReplaceAll(yaml, []byte("$1"))
331         cfgDumped, err := testLoader(c, string(yaml), &logbuf).Load()
332         c.Assert(err, check.IsNil)
333         // SourceTimestamp and SourceSHA256 aren't expected to be
334         // preserved through dump+load
335         cfgDumped.SourceTimestamp = cfg.SourceTimestamp
336         cfgDumped.SourceSHA256 = cfg.SourceSHA256
337         c.Check(cfg, check.DeepEquals, cfgDumped)
338         c.Check(logbuf.String(), check.Equals, "")
339 }
340
341 func (s *LoadSuite) TestUnacceptableTokens(c *check.C) {
342         for _, trial := range []struct {
343                 short      bool
344                 configPath string
345                 example    string
346         }{
347                 {false, "SystemRootToken", "SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_b_c"},
348                 {false, "ManagementToken", "ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa b c"},
349                 {false, "ManagementToken", "ManagementToken: \"$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabc\""},
350                 {false, "Collections.BlobSigningKey", "Collections: {BlobSigningKey: \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa⛵\"}"},
351                 {true, "SystemRootToken", "SystemRootToken: a_b_c"},
352                 {true, "ManagementToken", "ManagementToken: a b c"},
353                 {true, "ManagementToken", "ManagementToken: \"$abc\""},
354                 {true, "Collections.BlobSigningKey", "Collections: {BlobSigningKey: \"⛵\"}"},
355         } {
356                 c.Logf("trying bogus config: %s", trial.example)
357                 _, err := testLoader(c, "Clusters:\n zzzzz:\n  "+trial.example, nil).Load()
358                 c.Check(err, check.ErrorMatches, `Clusters.zzzzz.`+trial.configPath+`: unacceptable characters in token.*`)
359         }
360 }
361
362 func (s *LoadSuite) TestPostgreSQLKeyConflict(c *check.C) {
363         _, err := testLoader(c, `
364 Clusters:
365  zzzzz:
366   PostgreSQL:
367    Connection:
368      DBName: dbname
369      Host: host
370 `, nil).Load()
371         c.Check(err, check.ErrorMatches, `Clusters.zzzzz.PostgreSQL.Connection: multiple entries for "(dbname|host)".*`)
372 }
373
374 func (s *LoadSuite) TestBadClusterIDs(c *check.C) {
375         for _, data := range []string{`
376 Clusters:
377  123456:
378   ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
379   SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
380   Collections:
381    BlobSigningKey: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
382 `, `
383 Clusters:
384  12345:
385   ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
386   SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
387   Collections:
388    BlobSigningKey: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
389   RemoteClusters:
390    Zzzzz:
391     Host: Zzzzz.arvadosapi.com
392     Proxy: true
393 `, `
394 Clusters:
395  abcde:
396   ManagementToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
397   SystemRootToken: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
398   Collections:
399    BlobSigningKey: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
400   Login:
401    LoginCluster: zz-zz
402 `,
403         } {
404                 c.Log(data)
405                 v, err := testLoader(c, data, nil).Load()
406                 if v != nil {
407                         c.Logf("%#v", v.Clusters)
408                 }
409                 c.Check(err, check.ErrorMatches, `.*cluster ID should be 5 alphanumeric characters.*`)
410         }
411 }
412
413 func (s *LoadSuite) TestBadType(c *check.C) {
414         for _, data := range []string{`
415 Clusters:
416  zzzzz:
417   PostgreSQL: true
418 `, `
419 Clusters:
420  zzzzz:
421   PostgreSQL:
422    ConnectionPool: true
423 `, `
424 Clusters:
425  zzzzz:
426   PostgreSQL:
427    ConnectionPool: "foo"
428 `, `
429 Clusters:
430  zzzzz:
431   PostgreSQL:
432    ConnectionPool: []
433 `, `
434 Clusters:
435  zzzzz:
436   PostgreSQL:
437    ConnectionPool: [] # {foo: bar} isn't caught here; we rely on config-check
438 `,
439         } {
440                 c.Log(data)
441                 v, err := testLoader(c, data, nil).Load()
442                 if v != nil {
443                         c.Logf("%#v", v.Clusters["zzzzz"].PostgreSQL.ConnectionPool)
444                 }
445                 c.Check(err, check.ErrorMatches, `.*cannot unmarshal .*PostgreSQL.*`)
446         }
447 }
448
449 func (s *LoadSuite) TestMovedKeys(c *check.C) {
450         checkEquivalent(c, `# config has old keys only
451 Clusters:
452  zzzzz:
453   RequestLimits:
454    MultiClusterRequestConcurrency: 3
455    MaxItemsPerResponse: 999
456 `, `
457 Clusters:
458  zzzzz:
459   API:
460    MaxRequestAmplification: 3
461    MaxItemsPerResponse: 999
462 `)
463         checkEquivalent(c, `# config has both old and new keys; old values win
464 Clusters:
465  zzzzz:
466   RequestLimits:
467    MultiClusterRequestConcurrency: 0
468    MaxItemsPerResponse: 555
469   API:
470    MaxRequestAmplification: 3
471    MaxItemsPerResponse: 999
472 `, `
473 Clusters:
474  zzzzz:
475   API:
476    MaxRequestAmplification: 0
477    MaxItemsPerResponse: 555
478 `)
479 }
480
481 func checkEquivalent(c *check.C, goty, expectedy string) string {
482         var logbuf bytes.Buffer
483         gotldr := testLoader(c, goty, &logbuf)
484         expectedldr := testLoader(c, expectedy, nil)
485         checkEquivalentLoaders(c, gotldr, expectedldr)
486         return logbuf.String()
487 }
488
489 func checkEqualYAML(c *check.C, got, expected interface{}) {
490         expectedyaml, err := yaml.Marshal(expected)
491         c.Assert(err, check.IsNil)
492         gotyaml, err := yaml.Marshal(got)
493         c.Assert(err, check.IsNil)
494         if !bytes.Equal(gotyaml, expectedyaml) {
495                 cmd := exec.Command("diff", "-u", "--label", "expected", "--label", "got", "/dev/fd/3", "/dev/fd/4")
496                 for _, y := range [][]byte{expectedyaml, gotyaml} {
497                         pr, pw, err := os.Pipe()
498                         c.Assert(err, check.IsNil)
499                         defer pr.Close()
500                         go func(data []byte) {
501                                 pw.Write(data)
502                                 pw.Close()
503                         }(y)
504                         cmd.ExtraFiles = append(cmd.ExtraFiles, pr)
505                 }
506                 diff, err := cmd.CombinedOutput()
507                 // diff should report differences and exit non-zero.
508                 c.Check(err, check.NotNil)
509                 c.Log(string(diff))
510                 c.Error("got != expected; see diff (-expected +got) above")
511         }
512 }
513
514 func checkEquivalentLoaders(c *check.C, gotldr, expectedldr *Loader) {
515         got, err := gotldr.Load()
516         c.Assert(err, check.IsNil)
517         expected, err := expectedldr.Load()
518         c.Assert(err, check.IsNil)
519         // The inputs generally aren't even files, so SourceTimestamp
520         // can't be expected to match.
521         got.SourceTimestamp = expected.SourceTimestamp
522         // Obviously the content isn't identical -- otherwise we
523         // wouldn't need to check that it's equivalent.
524         got.SourceSHA256 = expected.SourceSHA256
525         checkEqualYAML(c, got, expected)
526 }
527
528 func checkListKeys(path string, x interface{}) (err error) {
529         v := reflect.Indirect(reflect.ValueOf(x))
530         switch v.Kind() {
531         case reflect.Map:
532                 iter := v.MapRange()
533                 for iter.Next() {
534                         k := iter.Key()
535                         if k.Kind() == reflect.String {
536                                 if err = checkListKeys(path+"."+k.String(), iter.Value().Interface()); err != nil {
537                                         return
538                                 }
539                         }
540                 }
541                 return
542
543         case reflect.Struct:
544                 for i := 0; i < v.NumField(); i++ {
545                         val := v.Field(i)
546                         structField := v.Type().Field(i)
547                         fieldname := structField.Name
548                         endsWithList := strings.HasSuffix(fieldname, "List")
549                         isAnArray := structField.Type.Kind() == reflect.Slice
550                         if endsWithList != isAnArray {
551                                 if endsWithList {
552                                         err = fmt.Errorf("%s.%s ends with 'List' but field is not an array (type %v)", path, fieldname, val.Kind())
553                                         return
554                                 }
555                                 if isAnArray && structField.Type.Elem().Kind() != reflect.Uint8 {
556                                         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())
557                                         return
558                                 }
559                         }
560                         if val.CanInterface() {
561                                 checkListKeys(path+"."+fieldname, val.Interface())
562                         }
563                 }
564         }
565         return
566 }
567
568 func (s *LoadSuite) TestListKeys(c *check.C) {
569         v1 := struct {
570                 EndInList []string
571         }{[]string{"a", "b"}}
572         var m1 = make(map[string]interface{})
573         m1["c"] = &v1
574         if err := checkListKeys("", m1); err != nil {
575                 c.Error(err)
576         }
577
578         v2 := struct {
579                 DoesNot []string
580         }{[]string{"a", "b"}}
581         var m2 = make(map[string]interface{})
582         m2["c"] = &v2
583         if err := checkListKeys("", m2); err == nil {
584                 c.Errorf("Should have produced an error")
585         }
586
587         v3 := struct {
588                 EndInList string
589         }{"a"}
590         var m3 = make(map[string]interface{})
591         m3["c"] = &v3
592         if err := checkListKeys("", m3); err == nil {
593                 c.Errorf("Should have produced an error")
594         }
595
596         loader := testLoader(c, string(DefaultYAML), nil)
597         cfg, err := loader.Load()
598         c.Assert(err, check.IsNil)
599         if err := checkListKeys("", cfg); err != nil {
600                 c.Error(err)
601         }
602 }
603
604 func (s *LoadSuite) TestImplicitStorageClasses(c *check.C) {
605         // If StorageClasses and Volumes.*.StorageClasses are all
606         // empty, there is a default storage class named "default".
607         ldr := testLoader(c, `{"Clusters":{"z1111":{}}}`, nil)
608         cfg, err := ldr.Load()
609         c.Assert(err, check.IsNil)
610         cc, err := cfg.GetCluster("z1111")
611         c.Assert(err, check.IsNil)
612         c.Check(cc.StorageClasses, check.HasLen, 1)
613         c.Check(cc.StorageClasses["default"].Default, check.Equals, true)
614         c.Check(cc.StorageClasses["default"].Priority, check.Equals, 0)
615
616         // The implicit "default" storage class is used by all
617         // volumes.
618         ldr = testLoader(c, `
619 Clusters:
620  z1111:
621   Volumes:
622    z: {}`, nil)
623         cfg, err = ldr.Load()
624         c.Assert(err, check.IsNil)
625         cc, err = cfg.GetCluster("z1111")
626         c.Assert(err, check.IsNil)
627         c.Check(cc.StorageClasses, check.HasLen, 1)
628         c.Check(cc.StorageClasses["default"].Default, check.Equals, true)
629         c.Check(cc.StorageClasses["default"].Priority, check.Equals, 0)
630         c.Check(cc.Volumes["z"].StorageClasses["default"], check.Equals, true)
631
632         // The "default" storage class isn't implicit if any classes
633         // are configured explicitly.
634         ldr = testLoader(c, `
635 Clusters:
636  z1111:
637   StorageClasses:
638    local:
639     Default: true
640     Priority: 111
641   Volumes:
642    z:
643     StorageClasses:
644      local: true`, nil)
645         cfg, err = ldr.Load()
646         c.Assert(err, check.IsNil)
647         cc, err = cfg.GetCluster("z1111")
648         c.Assert(err, check.IsNil)
649         c.Check(cc.StorageClasses, check.HasLen, 1)
650         c.Check(cc.StorageClasses["local"].Default, check.Equals, true)
651         c.Check(cc.StorageClasses["local"].Priority, check.Equals, 111)
652
653         // It is an error for a volume to refer to a storage class
654         // that isn't listed in StorageClasses.
655         ldr = testLoader(c, `
656 Clusters:
657  z1111:
658   StorageClasses:
659    local:
660     Default: true
661     Priority: 111
662   Volumes:
663    z:
664     StorageClasses:
665      nx: true`, nil)
666         _, err = ldr.Load()
667         c.Assert(err, check.ErrorMatches, `z: volume refers to storage class "nx" that is not defined.*`)
668
669         // It is an error for a volume to refer to a storage class
670         // that isn't listed in StorageClasses ... even if it's
671         // "default", which would exist implicitly if it weren't
672         // referenced explicitly by a volume.
673         ldr = testLoader(c, `
674 Clusters:
675  z1111:
676   Volumes:
677    z:
678     StorageClasses:
679      default: true`, nil)
680         _, err = ldr.Load()
681         c.Assert(err, check.ErrorMatches, `z: volume refers to storage class "default" that is not defined.*`)
682
683         // If the "default" storage class is configured explicitly, it
684         // is not used implicitly by any volumes, even if it's the
685         // only storage class.
686         var logbuf bytes.Buffer
687         ldr = testLoader(c, `
688 Clusters:
689  z1111:
690   StorageClasses:
691    default:
692     Default: true
693     Priority: 111
694   Volumes:
695    z: {}`, &logbuf)
696         _, err = ldr.Load()
697         c.Assert(err, check.ErrorMatches, `z: volume has no StorageClasses listed`)
698
699         // If StorageClasses are configured explicitly, there must be
700         // at least one with Default: true. (Calling one "default" is
701         // not sufficient.)
702         ldr = testLoader(c, `
703 Clusters:
704  z1111:
705   StorageClasses:
706    default:
707     Priority: 111
708   Volumes:
709    z:
710     StorageClasses:
711      default: true`, nil)
712         _, err = ldr.Load()
713         c.Assert(err, check.ErrorMatches, `there is no default storage class.*`)
714 }
715
716 func (s *LoadSuite) TestPreemptiblePriceFactor(c *check.C) {
717         yaml := `
718 Clusters:
719  z1111:
720   InstanceTypes:
721    Type1:
722     RAM: 12345M
723     VCPUs: 8
724     Price: 1.23
725  z2222:
726   Containers:
727    PreemptiblePriceFactor: 0.5
728   InstanceTypes:
729    Type1:
730     RAM: 12345M
731     VCPUs: 8
732     Price: 1.23
733  z3333:
734   Containers:
735    PreemptiblePriceFactor: 0.5
736   InstanceTypes:
737    Type1:
738     RAM: 12345M
739     VCPUs: 8
740     Price: 1.23
741    Type1.preemptible: # higher price than the auto-added variant would use -- should generate warning
742     ProviderType: Type1
743     RAM: 12345M
744     VCPUs: 8
745     Price: 1.23
746     Preemptible: true
747    Type2:
748     RAM: 23456M
749     VCPUs: 16
750     Price: 2.46
751    Type2.preemptible: # identical to the auto-added variant -- so no warning
752     ProviderType: Type2
753     RAM: 23456M
754     VCPUs: 16
755     Price: 1.23
756     Preemptible: true
757 `
758         var logbuf bytes.Buffer
759         cfg, err := testLoader(c, yaml, &logbuf).Load()
760         c.Assert(err, check.IsNil)
761         cc, err := cfg.GetCluster("z1111")
762         c.Assert(err, check.IsNil)
763         c.Check(cc.InstanceTypes["Type1"].Price, check.Equals, 1.23)
764         c.Check(cc.InstanceTypes, check.HasLen, 1)
765
766         cc, err = cfg.GetCluster("z2222")
767         c.Assert(err, check.IsNil)
768         c.Check(cc.InstanceTypes["Type1"].Preemptible, check.Equals, false)
769         c.Check(cc.InstanceTypes["Type1"].Price, check.Equals, 1.23)
770         c.Check(cc.InstanceTypes["Type1.preemptible"].Preemptible, check.Equals, true)
771         c.Check(cc.InstanceTypes["Type1.preemptible"].Price, check.Equals, 1.23/2)
772         c.Check(cc.InstanceTypes["Type1.preemptible"].ProviderType, check.Equals, "Type1")
773         c.Check(cc.InstanceTypes, check.HasLen, 2)
774
775         cc, err = cfg.GetCluster("z3333")
776         c.Assert(err, check.IsNil)
777         // Don't overwrite the explicitly configured preemptible variant
778         c.Check(cc.InstanceTypes["Type1.preemptible"].Price, check.Equals, 1.23)
779         c.Check(cc.InstanceTypes, check.HasLen, 4)
780         c.Check(logbuf.String(), check.Matches, `(?ms).*Clusters\.z3333\.InstanceTypes\[Type1\.preemptible\]: already exists, so not automatically adding a preemptible variant of Type1.*`)
781         c.Check(logbuf.String(), check.Not(check.Matches), `(?ms).*Type2\.preemptible.*`)
782         c.Check(logbuf.String(), check.Not(check.Matches), `(?ms).*(z1111|z2222)[^\n]*InstanceTypes.*`)
783 }
784
785 func (s *LoadSuite) TestSourceTimestamp(c *check.C) {
786         conftime, err := time.Parse(time.RFC3339, "2022-03-04T05:06:07-08:00")
787         c.Assert(err, check.IsNil)
788         confdata := `Clusters: {zzzzz: {}}`
789         conffile := c.MkDir() + "/config.yml"
790         ioutil.WriteFile(conffile, []byte(confdata), 0777)
791         tv := unix.NsecToTimeval(conftime.UnixNano())
792         unix.Lutimes(conffile, []unix.Timeval{tv, tv})
793         for _, trial := range []struct {
794                 configarg  string
795                 expectTime time.Time
796         }{
797                 {"-", time.Now()},
798                 {conffile, conftime},
799         } {
800                 c.Logf("trial: %+v", trial)
801                 ldr := NewLoader(strings.NewReader(confdata), ctxlog.TestLogger(c))
802                 ldr.Path = trial.configarg
803                 cfg, err := ldr.Load()
804                 c.Assert(err, check.IsNil)
805                 c.Check(cfg.SourceTimestamp, check.Equals, cfg.SourceTimestamp.UTC())
806                 c.Check(cfg.SourceTimestamp, check.Equals, ldr.sourceTimestamp)
807                 c.Check(int(cfg.SourceTimestamp.Sub(trial.expectTime).Seconds()), check.Equals, 0)
808                 c.Check(int(ldr.loadTimestamp.Sub(time.Now()).Seconds()), check.Equals, 0)
809
810                 var buf bytes.Buffer
811                 reg := prometheus.NewRegistry()
812                 ldr.RegisterMetrics(reg)
813                 enc := expfmt.NewEncoder(&buf, expfmt.FmtText)
814                 got, _ := reg.Gather()
815                 for _, mf := range got {
816                         enc.Encode(mf)
817                 }
818                 c.Check(buf.String(), check.Matches, `# HELP .*
819 # TYPE .*
820 arvados_config_load_timestamp_seconds{sha256="83aea5d82eb1d53372cd65c936c60acc1c6ef946e61977bbca7cfea709d201a8"} \Q`+fmt.Sprintf("%g", float64(ldr.loadTimestamp.UnixNano())/1e9)+`\E
821 # HELP .*
822 # TYPE .*
823 arvados_config_source_timestamp_seconds{sha256="83aea5d82eb1d53372cd65c936c60acc1c6ef946e61977bbca7cfea709d201a8"} \Q`+fmt.Sprintf("%g", float64(cfg.SourceTimestamp.UnixNano())/1e9)+`\E
824 `)
825         }
826 }