github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/snap/info_snap_yaml.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2016 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package snap 21 22 import ( 23 "fmt" 24 "os" 25 "sort" 26 "strconv" 27 "strings" 28 29 "gopkg.in/yaml.v2" 30 31 "github.com/snapcore/snapd/metautil" 32 "github.com/snapcore/snapd/strutil" 33 "github.com/snapcore/snapd/timeout" 34 ) 35 36 type snapYaml struct { 37 Name string `yaml:"name"` 38 Version string `yaml:"version"` 39 Type Type `yaml:"type"` 40 Architectures []string `yaml:"architectures,omitempty"` 41 Assumes []string `yaml:"assumes"` 42 Title string `yaml:"title"` 43 Description string `yaml:"description"` 44 Summary string `yaml:"summary"` 45 License string `yaml:"license,omitempty"` 46 Epoch Epoch `yaml:"epoch,omitempty"` 47 Base string `yaml:"base,omitempty"` 48 Confinement ConfinementType `yaml:"confinement,omitempty"` 49 Environment strutil.OrderedMap `yaml:"environment,omitempty"` 50 Plugs map[string]interface{} `yaml:"plugs,omitempty"` 51 Slots map[string]interface{} `yaml:"slots,omitempty"` 52 Apps map[string]appYaml `yaml:"apps,omitempty"` 53 Hooks map[string]hookYaml `yaml:"hooks,omitempty"` 54 Layout map[string]layoutYaml `yaml:"layout,omitempty"` 55 SystemUsernames map[string]interface{} `yaml:"system-usernames,omitempty"` 56 57 // TypoLayouts is used to detect the use of the incorrect plural form of "layout" 58 TypoLayouts typoDetector `yaml:"layouts,omitempty"` 59 } 60 61 type typoDetector struct { 62 Hint string 63 } 64 65 func (td *typoDetector) UnmarshalYAML(func(interface{}) error) error { 66 return fmt.Errorf("typo detected: %s", td.Hint) 67 } 68 69 type appYaml struct { 70 Aliases []string `yaml:"aliases,omitempty"` 71 72 Command string `yaml:"command"` 73 CommandChain []string `yaml:"command-chain,omitempty"` 74 75 Daemon string `yaml:"daemon"` 76 DaemonScope DaemonScope `yaml:"daemon-scope"` 77 78 StopCommand string `yaml:"stop-command,omitempty"` 79 ReloadCommand string `yaml:"reload-command,omitempty"` 80 PostStopCommand string `yaml:"post-stop-command,omitempty"` 81 StopTimeout timeout.Timeout `yaml:"stop-timeout,omitempty"` 82 StartTimeout timeout.Timeout `yaml:"start-timeout,omitempty"` 83 WatchdogTimeout timeout.Timeout `yaml:"watchdog-timeout,omitempty"` 84 Completer string `yaml:"completer,omitempty"` 85 RefreshMode string `yaml:"refresh-mode,omitempty"` 86 StopMode StopModeType `yaml:"stop-mode,omitempty"` 87 88 RestartCond RestartCondition `yaml:"restart-condition,omitempty"` 89 RestartDelay timeout.Timeout `yaml:"restart-delay,omitempty"` 90 SlotNames []string `yaml:"slots,omitempty"` 91 PlugNames []string `yaml:"plugs,omitempty"` 92 93 BusName string `yaml:"bus-name,omitempty"` 94 ActivatesOn []string `yaml:"activates-on,omitempty"` 95 CommonID string `yaml:"common-id,omitempty"` 96 97 Environment strutil.OrderedMap `yaml:"environment,omitempty"` 98 99 Sockets map[string]socketsYaml `yaml:"sockets,omitempty"` 100 101 After []string `yaml:"after,omitempty"` 102 Before []string `yaml:"before,omitempty"` 103 104 Timer string `yaml:"timer,omitempty"` 105 106 Autostart string `yaml:"autostart,omitempty"` 107 } 108 109 type hookYaml struct { 110 PlugNames []string `yaml:"plugs,omitempty"` 111 SlotNames []string `yaml:"slots,omitempty"` 112 Environment strutil.OrderedMap `yaml:"environment,omitempty"` 113 CommandChain []string `yaml:"command-chain,omitempty"` 114 } 115 116 type layoutYaml struct { 117 Bind string `yaml:"bind,omitempty"` 118 BindFile string `yaml:"bind-file,omitempty"` 119 Type string `yaml:"type,omitempty"` 120 User string `yaml:"user,omitempty"` 121 Group string `yaml:"group,omitempty"` 122 Mode string `yaml:"mode,omitempty"` 123 Symlink string `yaml:"symlink,omitempty"` 124 } 125 126 type socketsYaml struct { 127 ListenStream string `yaml:"listen-stream,omitempty"` 128 SocketMode os.FileMode `yaml:"socket-mode,omitempty"` 129 } 130 131 // InfoFromSnapYaml creates a new info based on the given snap.yaml data 132 func InfoFromSnapYaml(yamlData []byte) (*Info, error) { 133 return infoFromSnapYaml(yamlData, new(scopedTracker)) 134 } 135 136 // scopedTracker helps keeping track of which slots/plugs are scoped 137 // to apps and hooks. 138 type scopedTracker struct { 139 plugs map[*PlugInfo]bool 140 slots map[*SlotInfo]bool 141 } 142 143 func (strk *scopedTracker) init(sizeGuess int) { 144 strk.plugs = make(map[*PlugInfo]bool, sizeGuess) 145 strk.slots = make(map[*SlotInfo]bool, sizeGuess) 146 } 147 148 func (strk *scopedTracker) markPlug(plug *PlugInfo) { 149 strk.plugs[plug] = true 150 } 151 152 func (strk *scopedTracker) markSlot(slot *SlotInfo) { 153 strk.slots[slot] = true 154 } 155 156 func (strk *scopedTracker) plug(plug *PlugInfo) bool { 157 return strk.plugs[plug] 158 } 159 160 func (strk *scopedTracker) slot(slot *SlotInfo) bool { 161 return strk.slots[slot] 162 } 163 164 func infoFromSnapYaml(yamlData []byte, strk *scopedTracker) (*Info, error) { 165 var y snapYaml 166 // Customize hints for the typo detector. 167 y.TypoLayouts.Hint = `use singular "layout" instead of plural "layouts"` 168 err := yaml.Unmarshal(yamlData, &y) 169 if err != nil { 170 return nil, fmt.Errorf("cannot parse snap.yaml: %s", err) 171 } 172 173 snap := infoSkeletonFromSnapYaml(y) 174 175 // Collect top-level definitions of plugs and slots 176 if err := setPlugsFromSnapYaml(y, snap); err != nil { 177 return nil, err 178 } 179 if err := setSlotsFromSnapYaml(y, snap); err != nil { 180 return nil, err 181 } 182 183 strk.init(len(y.Apps) + len(y.Hooks)) 184 185 // Collect all apps, their aliases and hooks 186 if err := setAppsFromSnapYaml(y, snap, strk); err != nil { 187 return nil, err 188 } 189 setHooksFromSnapYaml(y, snap, strk) 190 191 // Bind plugs and slots that are not scoped to all known apps and hooks. 192 bindUnscopedPlugs(snap, strk) 193 bindUnscopedSlots(snap, strk) 194 195 // Collect layout elements. 196 if y.Layout != nil { 197 snap.Layout = make(map[string]*Layout, len(y.Layout)) 198 for path, l := range y.Layout { 199 var mode os.FileMode = 0755 200 if l.Mode != "" { 201 m, err := strconv.ParseUint(l.Mode, 8, 32) 202 if err != nil { 203 return nil, err 204 } 205 mode = os.FileMode(m) 206 } 207 user := "root" 208 if l.User != "" { 209 user = l.User 210 } 211 group := "root" 212 if l.Group != "" { 213 group = l.Group 214 } 215 snap.Layout[path] = &Layout{ 216 Snap: snap, Path: path, 217 Bind: l.Bind, Type: l.Type, Symlink: l.Symlink, BindFile: l.BindFile, 218 User: user, Group: group, Mode: mode, 219 } 220 } 221 } 222 223 // Rename specific plugs on the core snap. 224 snap.renameClashingCorePlugs() 225 226 snap.BadInterfaces = make(map[string]string) 227 SanitizePlugsSlots(snap) 228 229 // Collect system usernames 230 if err := setSystemUsernamesFromSnapYaml(y, snap); err != nil { 231 return nil, err 232 } 233 234 // FIXME: validation of the fields 235 return snap, nil 236 } 237 238 // infoSkeletonFromSnapYaml initializes an Info without apps, hook, plugs, or 239 // slots 240 func infoSkeletonFromSnapYaml(y snapYaml) *Info { 241 // Prepare defaults 242 architectures := []string{"all"} 243 if len(y.Architectures) != 0 { 244 architectures = y.Architectures 245 } 246 247 typ := TypeApp 248 if y.Type != "" { 249 typ = y.Type 250 } 251 // TODO: once we have epochs transition to the snapd type for real 252 if y.Name == "snapd" { 253 typ = TypeSnapd 254 } 255 256 if len(y.Epoch.Read) == 0 { 257 // normalize 258 y.Epoch.Read = []uint32{0} 259 y.Epoch.Write = []uint32{0} 260 } 261 262 confinement := StrictConfinement 263 if y.Confinement != "" { 264 confinement = y.Confinement 265 } 266 267 // Construct snap skeleton without apps, hooks, plugs, or slots 268 snap := &Info{ 269 SuggestedName: y.Name, 270 Version: y.Version, 271 SnapType: typ, 272 Architectures: architectures, 273 Assumes: y.Assumes, 274 OriginalTitle: y.Title, 275 OriginalDescription: y.Description, 276 OriginalSummary: y.Summary, 277 License: y.License, 278 Epoch: y.Epoch, 279 Confinement: confinement, 280 Base: y.Base, 281 Apps: make(map[string]*AppInfo), 282 LegacyAliases: make(map[string]*AppInfo), 283 Hooks: make(map[string]*HookInfo), 284 Plugs: make(map[string]*PlugInfo), 285 Slots: make(map[string]*SlotInfo), 286 Environment: y.Environment, 287 SystemUsernames: make(map[string]*SystemUsernameInfo), 288 } 289 290 sort.Strings(snap.Assumes) 291 292 return snap 293 } 294 295 func setPlugsFromSnapYaml(y snapYaml, snap *Info) error { 296 for name, data := range y.Plugs { 297 iface, label, attrs, err := convertToSlotOrPlugData("plug", name, data) 298 if err != nil { 299 return err 300 } 301 snap.Plugs[name] = &PlugInfo{ 302 Snap: snap, 303 Name: name, 304 Interface: iface, 305 Attrs: attrs, 306 Label: label, 307 } 308 if len(y.Apps) > 0 { 309 snap.Plugs[name].Apps = make(map[string]*AppInfo) 310 } 311 if len(y.Hooks) > 0 { 312 snap.Plugs[name].Hooks = make(map[string]*HookInfo) 313 } 314 } 315 316 return nil 317 } 318 319 func setSlotsFromSnapYaml(y snapYaml, snap *Info) error { 320 for name, data := range y.Slots { 321 iface, label, attrs, err := convertToSlotOrPlugData("slot", name, data) 322 if err != nil { 323 return err 324 } 325 snap.Slots[name] = &SlotInfo{ 326 Snap: snap, 327 Name: name, 328 Interface: iface, 329 Attrs: attrs, 330 Label: label, 331 } 332 if len(y.Apps) > 0 { 333 snap.Slots[name].Apps = make(map[string]*AppInfo) 334 } 335 if len(y.Hooks) > 0 { 336 snap.Slots[name].Hooks = make(map[string]*HookInfo) 337 } 338 } 339 340 return nil 341 } 342 343 func setAppsFromSnapYaml(y snapYaml, snap *Info, strk *scopedTracker) error { 344 for appName, yApp := range y.Apps { 345 // Collect all apps 346 app := &AppInfo{ 347 Snap: snap, 348 Name: appName, 349 LegacyAliases: yApp.Aliases, 350 Command: yApp.Command, 351 CommandChain: yApp.CommandChain, 352 StartTimeout: yApp.StartTimeout, 353 Daemon: yApp.Daemon, 354 DaemonScope: yApp.DaemonScope, 355 StopTimeout: yApp.StopTimeout, 356 StopCommand: yApp.StopCommand, 357 ReloadCommand: yApp.ReloadCommand, 358 PostStopCommand: yApp.PostStopCommand, 359 RestartCond: yApp.RestartCond, 360 RestartDelay: yApp.RestartDelay, 361 BusName: yApp.BusName, 362 CommonID: yApp.CommonID, 363 Environment: yApp.Environment, 364 Completer: yApp.Completer, 365 StopMode: yApp.StopMode, 366 RefreshMode: yApp.RefreshMode, 367 Before: yApp.Before, 368 After: yApp.After, 369 Autostart: yApp.Autostart, 370 WatchdogTimeout: yApp.WatchdogTimeout, 371 } 372 if len(y.Plugs) > 0 || len(yApp.PlugNames) > 0 { 373 app.Plugs = make(map[string]*PlugInfo) 374 } 375 if len(y.Slots) > 0 || len(yApp.SlotNames) > 0 { 376 app.Slots = make(map[string]*SlotInfo) 377 } 378 if len(yApp.Sockets) > 0 { 379 app.Sockets = make(map[string]*SocketInfo, len(yApp.Sockets)) 380 } 381 if len(yApp.ActivatesOn) > 0 { 382 app.ActivatesOn = make([]*SlotInfo, 0, len(yApp.ActivatesOn)) 383 } 384 // Daemons default to being system daemons 385 if app.Daemon != "" && app.DaemonScope == "" { 386 app.DaemonScope = SystemDaemon 387 } 388 389 snap.Apps[appName] = app 390 for _, alias := range app.LegacyAliases { 391 if snap.LegacyAliases[alias] != nil { 392 return fmt.Errorf("cannot set %q as alias for both %q and %q", alias, snap.LegacyAliases[alias].Name, appName) 393 } 394 snap.LegacyAliases[alias] = app 395 } 396 // Bind all plugs/slots listed in this app 397 for _, plugName := range yApp.PlugNames { 398 plug, ok := snap.Plugs[plugName] 399 if !ok { 400 // Create implicit plug definitions if required 401 plug = &PlugInfo{ 402 Snap: snap, 403 Name: plugName, 404 Interface: plugName, 405 Apps: make(map[string]*AppInfo), 406 } 407 snap.Plugs[plugName] = plug 408 } 409 // Mark the plug as scoped. 410 strk.markPlug(plug) 411 app.Plugs[plugName] = plug 412 plug.Apps[appName] = app 413 } 414 for _, slotName := range yApp.SlotNames { 415 slot, ok := snap.Slots[slotName] 416 if !ok { 417 slot = &SlotInfo{ 418 Snap: snap, 419 Name: slotName, 420 Interface: slotName, 421 Apps: make(map[string]*AppInfo), 422 } 423 snap.Slots[slotName] = slot 424 } 425 // Mark the slot as scoped. 426 strk.markSlot(slot) 427 app.Slots[slotName] = slot 428 slot.Apps[appName] = app 429 } 430 for _, slotName := range yApp.ActivatesOn { 431 slot, ok := snap.Slots[slotName] 432 if !ok { 433 return fmt.Errorf("invalid activates-on value %q on app %q: slot not found", slotName, appName) 434 } 435 app.ActivatesOn = append(app.ActivatesOn, slot) 436 // Implicitly add the slot to the app 437 strk.markSlot(slot) 438 app.Slots[slotName] = slot 439 slot.Apps[appName] = app 440 } 441 for name, data := range yApp.Sockets { 442 app.Sockets[name] = &SocketInfo{ 443 App: app, 444 Name: name, 445 ListenStream: data.ListenStream, 446 SocketMode: data.SocketMode, 447 } 448 } 449 if yApp.Timer != "" { 450 app.Timer = &TimerInfo{ 451 App: app, 452 Timer: yApp.Timer, 453 } 454 } 455 // collect all common IDs 456 if app.CommonID != "" { 457 snap.CommonIDs = append(snap.CommonIDs, app.CommonID) 458 } 459 } 460 return nil 461 } 462 463 func setHooksFromSnapYaml(y snapYaml, snap *Info, strk *scopedTracker) { 464 for hookName, yHook := range y.Hooks { 465 if !IsHookSupported(hookName) { 466 continue 467 } 468 469 // Collect all hooks 470 hook := &HookInfo{ 471 Snap: snap, 472 Name: hookName, 473 Environment: yHook.Environment, 474 CommandChain: yHook.CommandChain, 475 Explicit: true, 476 } 477 if len(y.Plugs) > 0 || len(yHook.PlugNames) > 0 { 478 hook.Plugs = make(map[string]*PlugInfo) 479 } 480 if len(y.Slots) > 0 || len(yHook.SlotNames) > 0 { 481 hook.Slots = make(map[string]*SlotInfo) 482 } 483 484 snap.Hooks[hookName] = hook 485 // Bind all plugs/slots listed in this hook 486 for _, plugName := range yHook.PlugNames { 487 plug, ok := snap.Plugs[plugName] 488 if !ok { 489 // Create implicit plug definitions if required 490 plug = &PlugInfo{ 491 Snap: snap, 492 Name: plugName, 493 Interface: plugName, 494 Hooks: make(map[string]*HookInfo), 495 } 496 snap.Plugs[plugName] = plug 497 } 498 // Mark the plug as scoped. 499 strk.markPlug(plug) 500 if plug.Hooks == nil { 501 plug.Hooks = make(map[string]*HookInfo) 502 } 503 hook.Plugs[plugName] = plug 504 plug.Hooks[hookName] = hook 505 } 506 for _, slotName := range yHook.SlotNames { 507 slot, ok := snap.Slots[slotName] 508 if !ok { 509 // Create implicit slot definitions if required 510 slot = &SlotInfo{ 511 Snap: snap, 512 Name: slotName, 513 Interface: slotName, 514 Hooks: make(map[string]*HookInfo), 515 } 516 snap.Slots[slotName] = slot 517 } 518 // Mark the slot as scoped. 519 strk.markSlot(slot) 520 if slot.Hooks == nil { 521 slot.Hooks = make(map[string]*HookInfo) 522 } 523 hook.Slots[slotName] = slot 524 slot.Hooks[hookName] = hook 525 } 526 } 527 } 528 529 func setSystemUsernamesFromSnapYaml(y snapYaml, snap *Info) error { 530 for user, data := range y.SystemUsernames { 531 if user == "" { 532 return fmt.Errorf("system username cannot be empty") 533 } 534 scope, attrs, err := convertToUsernamesData(user, data) 535 if err != nil { 536 return err 537 } 538 if scope == "" { 539 return fmt.Errorf("system username %q does not specify a scope", user) 540 } 541 snap.SystemUsernames[user] = &SystemUsernameInfo{ 542 Name: user, 543 Scope: scope, 544 Attrs: attrs, 545 } 546 } 547 548 return nil 549 } 550 551 func bindUnscopedPlugs(snap *Info, strk *scopedTracker) { 552 for plugName, plug := range snap.Plugs { 553 if strk.plug(plug) { 554 continue 555 } 556 for appName, app := range snap.Apps { 557 app.Plugs[plugName] = plug 558 plug.Apps[appName] = app 559 } 560 561 for hookName, hook := range snap.Hooks { 562 hook.Plugs[plugName] = plug 563 plug.Hooks[hookName] = hook 564 } 565 } 566 } 567 568 func bindUnscopedSlots(snap *Info, strk *scopedTracker) { 569 for slotName, slot := range snap.Slots { 570 if strk.slot(slot) { 571 continue 572 } 573 for appName, app := range snap.Apps { 574 app.Slots[slotName] = slot 575 slot.Apps[appName] = app 576 } 577 for hookName, hook := range snap.Hooks { 578 hook.Slots[slotName] = slot 579 slot.Hooks[hookName] = hook 580 } 581 } 582 } 583 584 // bindImplicitHooks binds all global plugs and slots to implicit hooks 585 func bindImplicitHooks(snap *Info, strk *scopedTracker) { 586 for hookName, hook := range snap.Hooks { 587 if hook.Explicit { 588 continue 589 } 590 for _, plug := range snap.Plugs { 591 if strk.plug(plug) { 592 continue 593 } 594 if hook.Plugs == nil { 595 hook.Plugs = make(map[string]*PlugInfo) 596 } 597 hook.Plugs[plug.Name] = plug 598 if plug.Hooks == nil { 599 plug.Hooks = make(map[string]*HookInfo) 600 } 601 plug.Hooks[hookName] = hook 602 } 603 for _, slot := range snap.Slots { 604 if strk.slot(slot) { 605 continue 606 } 607 if hook.Slots == nil { 608 hook.Slots = make(map[string]*SlotInfo) 609 } 610 hook.Slots[slot.Name] = slot 611 if slot.Hooks == nil { 612 slot.Hooks = make(map[string]*HookInfo) 613 } 614 slot.Hooks[hookName] = hook 615 } 616 } 617 } 618 619 func convertToSlotOrPlugData(plugOrSlot, name string, data interface{}) (iface, label string, attrs map[string]interface{}, err error) { 620 iface = name 621 switch data.(type) { 622 case string: 623 return data.(string), "", nil, nil 624 case nil: 625 return name, "", nil, nil 626 case map[interface{}]interface{}: 627 for keyData, valueData := range data.(map[interface{}]interface{}) { 628 key, ok := keyData.(string) 629 if !ok { 630 err := fmt.Errorf("%s %q has attribute key that is not a string (found %T)", 631 plugOrSlot, name, keyData) 632 return "", "", nil, err 633 } 634 if strings.HasPrefix(key, "$") { 635 err := fmt.Errorf("%s %q uses reserved attribute %q", plugOrSlot, name, key) 636 return "", "", nil, err 637 } 638 switch key { 639 case "": 640 return "", "", nil, fmt.Errorf("%s %q has an empty attribute key", plugOrSlot, name) 641 case "interface": 642 value, ok := valueData.(string) 643 if !ok { 644 err := fmt.Errorf("interface name on %s %q is not a string (found %T)", 645 plugOrSlot, name, valueData) 646 return "", "", nil, err 647 } 648 iface = value 649 case "label": 650 value, ok := valueData.(string) 651 if !ok { 652 err := fmt.Errorf("label of %s %q is not a string (found %T)", 653 plugOrSlot, name, valueData) 654 return "", "", nil, err 655 } 656 label = value 657 default: 658 if attrs == nil { 659 attrs = make(map[string]interface{}) 660 } 661 value, err := metautil.NormalizeValue(valueData) 662 if err != nil { 663 return "", "", nil, fmt.Errorf("attribute %q of %s %q: %v", key, plugOrSlot, name, err) 664 } 665 attrs[key] = value 666 } 667 } 668 return iface, label, attrs, nil 669 default: 670 err := fmt.Errorf("%s %q has malformed definition (found %T)", plugOrSlot, name, data) 671 return "", "", nil, err 672 } 673 } 674 675 // Short form: 676 // system-usernames: 677 // snap_daemon: shared # 'scope' is 'shared' 678 // lxd: external # currently unsupported 679 // foo: private # currently unsupported 680 // Attributes form: 681 // system-usernames: 682 // snap_daemon: 683 // scope: shared 684 // attrib1: ... 685 // attrib2: ... 686 func convertToUsernamesData(user string, data interface{}) (scope string, attrs map[string]interface{}, err error) { 687 switch data.(type) { 688 case string: 689 return data.(string), nil, nil 690 case nil: 691 return "", nil, nil 692 case map[interface{}]interface{}: 693 for keyData, valueData := range data.(map[interface{}]interface{}) { 694 key, ok := keyData.(string) 695 if !ok { 696 err := fmt.Errorf("system username %q has attribute key that is not a string (found %T)", user, keyData) 697 return "", nil, err 698 } 699 switch key { 700 case "scope": 701 value, ok := valueData.(string) 702 if !ok { 703 err := fmt.Errorf("scope on system username %q is not a string (found %T)", user, valueData) 704 return "", nil, err 705 } 706 scope = value 707 case "": 708 return "", nil, fmt.Errorf("system username %q has an empty attribute key", user) 709 default: 710 if attrs == nil { 711 attrs = make(map[string]interface{}) 712 } 713 value, err := metautil.NormalizeValue(valueData) 714 if err != nil { 715 return "", nil, fmt.Errorf("attribute %q of system username %q: %v", key, user, err) 716 } 717 attrs[key] = value 718 } 719 } 720 return scope, attrs, nil 721 default: 722 err := fmt.Errorf("system username %q has malformed definition (found %T)", user, data) 723 return "", nil, err 724 } 725 }