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