feb05cb9515bb03be84352d550c24bf82fe80320
[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: {Replication: 2}
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) TestWarnUnusedLocalKeep(c *check.C) {
605         var logbuf bytes.Buffer
606         _, err := testLoader(c, `
607 Clusters:
608  z1111:
609   Volumes:
610    z:
611     Replication: 1
612 `, &logbuf).Load()
613         c.Assert(err, check.IsNil)
614         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.*`)
615
616         logbuf.Reset()
617         _, err = testLoader(c, `
618 Clusters:
619  z1111:
620   Volumes:
621    z:
622     AccessViaHosts:
623      "http://0.0.0.0:12345": {}
624 `, &logbuf).Load()
625         c.Assert(err, check.IsNil)
626         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.*`)
627 }
628
629 func (s *LoadSuite) TestImplicitStorageClasses(c *check.C) {
630         // If StorageClasses and Volumes.*.StorageClasses are all
631         // empty, there is a default storage class named "default".
632         ldr := testLoader(c, `{"Clusters":{"z1111":{}}}`, nil)
633         cfg, err := ldr.Load()
634         c.Assert(err, check.IsNil)
635         cc, err := cfg.GetCluster("z1111")
636         c.Assert(err, check.IsNil)
637         c.Check(cc.StorageClasses, check.HasLen, 1)
638         c.Check(cc.StorageClasses["default"].Default, check.Equals, true)
639         c.Check(cc.StorageClasses["default"].Priority, check.Equals, 0)
640
641         // The implicit "default" storage class is used by all
642         // volumes.
643         ldr = testLoader(c, `
644 Clusters:
645  z1111:
646   Volumes:
647    z: {}`, nil)
648         cfg, err = ldr.Load()
649         c.Assert(err, check.IsNil)
650         cc, err = cfg.GetCluster("z1111")
651         c.Assert(err, check.IsNil)
652         c.Check(cc.StorageClasses, check.HasLen, 1)
653         c.Check(cc.StorageClasses["default"].Default, check.Equals, true)
654         c.Check(cc.StorageClasses["default"].Priority, check.Equals, 0)
655         c.Check(cc.Volumes["z"].StorageClasses["default"], check.Equals, true)
656
657         // The "default" storage class isn't implicit if any classes
658         // are configured explicitly.
659         ldr = testLoader(c, `
660 Clusters:
661  z1111:
662   StorageClasses:
663    local:
664     Default: true
665     Priority: 111
666   Volumes:
667    z:
668     StorageClasses:
669      local: true`, nil)
670         cfg, err = ldr.Load()
671         c.Assert(err, check.IsNil)
672         cc, err = cfg.GetCluster("z1111")
673         c.Assert(err, check.IsNil)
674         c.Check(cc.StorageClasses, check.HasLen, 1)
675         c.Check(cc.StorageClasses["local"].Default, check.Equals, true)
676         c.Check(cc.StorageClasses["local"].Priority, check.Equals, 111)
677
678         // It is an error for a volume to refer to a storage class
679         // that isn't listed in StorageClasses.
680         ldr = testLoader(c, `
681 Clusters:
682  z1111:
683   StorageClasses:
684    local:
685     Default: true
686     Priority: 111
687   Volumes:
688    z:
689     StorageClasses:
690      nx: true`, nil)
691         _, err = ldr.Load()
692         c.Assert(err, check.ErrorMatches, `z: volume refers to storage class "nx" that is not defined.*`)
693
694         // It is an error for a volume to refer to a storage class
695         // that isn't listed in StorageClasses ... even if it's
696         // "default", which would exist implicitly if it weren't
697         // referenced explicitly by a volume.
698         ldr = testLoader(c, `
699 Clusters:
700  z1111:
701   Volumes:
702    z:
703     StorageClasses:
704      default: true`, nil)
705         _, err = ldr.Load()
706         c.Assert(err, check.ErrorMatches, `z: volume refers to storage class "default" that is not defined.*`)
707
708         // If the "default" storage class is configured explicitly, it
709         // is not used implicitly by any volumes, even if it's the
710         // only storage class.
711         var logbuf bytes.Buffer
712         ldr = testLoader(c, `
713 Clusters:
714  z1111:
715   StorageClasses:
716    default:
717     Default: true
718     Priority: 111
719   Volumes:
720    z: {}`, &logbuf)
721         _, err = ldr.Load()
722         c.Assert(err, check.ErrorMatches, `z: volume has no StorageClasses listed`)
723
724         // If StorageClasses are configured explicitly, there must be
725         // at least one with Default: true. (Calling one "default" is
726         // not sufficient.)
727         ldr = testLoader(c, `
728 Clusters:
729  z1111:
730   StorageClasses:
731    default:
732     Priority: 111
733   Volumes:
734    z:
735     StorageClasses:
736      default: true`, nil)
737         _, err = ldr.Load()
738         c.Assert(err, check.ErrorMatches, `there is no default storage class.*`)
739 }
740
741 func (s *LoadSuite) TestPreemptiblePriceFactor(c *check.C) {
742         yaml := `
743 Clusters:
744  z1111:
745   InstanceTypes:
746    Type1:
747     RAM: 12345M
748     VCPUs: 8
749     Price: 1.23
750  z2222:
751   Containers:
752    PreemptiblePriceFactor: 0.5
753   InstanceTypes:
754    Type1:
755     RAM: 12345M
756     VCPUs: 8
757     Price: 1.23
758  z3333:
759   Containers:
760    PreemptiblePriceFactor: 0.5
761   InstanceTypes:
762    Type1:
763     RAM: 12345M
764     VCPUs: 8
765     Price: 1.23
766    Type1.preemptible: # higher price than the auto-added variant would use -- should generate warning
767     ProviderType: Type1
768     RAM: 12345M
769     VCPUs: 8
770     Price: 1.23
771     Preemptible: true
772    Type2:
773     RAM: 23456M
774     VCPUs: 16
775     Price: 2.46
776    Type2.preemptible: # identical to the auto-added variant -- so no warning
777     ProviderType: Type2
778     RAM: 23456M
779     VCPUs: 16
780     Price: 1.23
781     Preemptible: true
782 `
783         var logbuf bytes.Buffer
784         cfg, err := testLoader(c, yaml, &logbuf).Load()
785         c.Assert(err, check.IsNil)
786         cc, err := cfg.GetCluster("z1111")
787         c.Assert(err, check.IsNil)
788         c.Check(cc.InstanceTypes["Type1"].Price, check.Equals, 1.23)
789         c.Check(cc.InstanceTypes, check.HasLen, 1)
790
791         cc, err = cfg.GetCluster("z2222")
792         c.Assert(err, check.IsNil)
793         c.Check(cc.InstanceTypes["Type1"].Preemptible, check.Equals, false)
794         c.Check(cc.InstanceTypes["Type1"].Price, check.Equals, 1.23)
795         c.Check(cc.InstanceTypes["Type1.preemptible"].Preemptible, check.Equals, true)
796         c.Check(cc.InstanceTypes["Type1.preemptible"].Price, check.Equals, 1.23/2)
797         c.Check(cc.InstanceTypes["Type1.preemptible"].ProviderType, check.Equals, "Type1")
798         c.Check(cc.InstanceTypes, check.HasLen, 2)
799
800         cc, err = cfg.GetCluster("z3333")
801         c.Assert(err, check.IsNil)
802         // Don't overwrite the explicitly configured preemptible variant
803         c.Check(cc.InstanceTypes["Type1.preemptible"].Price, check.Equals, 1.23)
804         c.Check(cc.InstanceTypes, check.HasLen, 4)
805         c.Check(logbuf.String(), check.Matches, `(?ms).*Clusters\.z3333\.InstanceTypes\[Type1\.preemptible\]: already exists, so not automatically adding a preemptible variant of Type1.*`)
806         c.Check(logbuf.String(), check.Not(check.Matches), `(?ms).*Type2\.preemptible.*`)
807         c.Check(logbuf.String(), check.Not(check.Matches), `(?ms).*(z1111|z2222)[^\n]*InstanceTypes.*`)
808 }
809
810 func (s *LoadSuite) TestSourceTimestamp(c *check.C) {
811         conftime, err := time.Parse(time.RFC3339, "2022-03-04T05:06:07-08:00")
812         c.Assert(err, check.IsNil)
813         confdata := `Clusters: {zzzzz: {}}`
814         conffile := c.MkDir() + "/config.yml"
815         ioutil.WriteFile(conffile, []byte(confdata), 0777)
816         tv := unix.NsecToTimeval(conftime.UnixNano())
817         unix.Lutimes(conffile, []unix.Timeval{tv, tv})
818         for _, trial := range []struct {
819                 configarg  string
820                 expectTime time.Time
821         }{
822                 {"-", time.Now()},
823                 {conffile, conftime},
824         } {
825                 c.Logf("trial: %+v", trial)
826                 ldr := NewLoader(strings.NewReader(confdata), ctxlog.TestLogger(c))
827                 ldr.Path = trial.configarg
828                 cfg, err := ldr.Load()
829                 c.Assert(err, check.IsNil)
830                 c.Check(cfg.SourceTimestamp, check.Equals, cfg.SourceTimestamp.UTC())
831                 c.Check(cfg.SourceTimestamp, check.Equals, ldr.sourceTimestamp)
832                 c.Check(int(cfg.SourceTimestamp.Sub(trial.expectTime).Seconds()), check.Equals, 0)
833                 c.Check(int(ldr.loadTimestamp.Sub(time.Now()).Seconds()), check.Equals, 0)
834
835                 var buf bytes.Buffer
836                 reg := prometheus.NewRegistry()
837                 ldr.RegisterMetrics(reg)
838                 enc := expfmt.NewEncoder(&buf, expfmt.FmtText)
839                 got, _ := reg.Gather()
840                 for _, mf := range got {
841                         enc.Encode(mf)
842                 }
843                 c.Check(buf.String(), check.Matches, `# HELP .*
844 # TYPE .*
845 arvados_config_load_timestamp_seconds{sha256="83aea5d82eb1d53372cd65c936c60acc1c6ef946e61977bbca7cfea709d201a8"} \Q`+fmt.Sprintf("%g", float64(ldr.loadTimestamp.UnixNano())/1e9)+`\E
846 # HELP .*
847 # TYPE .*
848 arvados_config_source_timestamp_seconds{sha256="83aea5d82eb1d53372cd65c936c60acc1c6ef946e61977bbca7cfea709d201a8"} \Q`+fmt.Sprintf("%g", float64(cfg.SourceTimestamp.UnixNano())/1e9)+`\E
849 `)
850         }
851 }