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