github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/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  }