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