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 }