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