github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/hookstate/ctlcmd/health.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 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 ctlcmd 21 22 import ( 23 "fmt" 24 "regexp" 25 "time" 26 27 "github.com/snapcore/snapd/i18n" 28 "github.com/snapcore/snapd/overlord/healthstate" 29 "github.com/snapcore/snapd/overlord/state" 30 ) 31 32 var ( 33 shortHealthHelp = i18n.G("Report the health status of a snap") 34 longHealthHelp = i18n.G(` 35 The set-health command is called from within a snap to inform the system of the 36 snap's overall health. 37 38 It can be called from any hook, and even from the apps themselves. A snap can 39 optionally provide a 'check-health' hook to better manage these calls, which is 40 then called periodically and with increased frequency while the snap is 41 "unhealthy". Any health regression will issue a warning to the user. 42 43 Note: the health is of the snap only, not of the apps it contains; it’s up to 44 the snap developer to determine how the health of the individual apps is 45 reflected in the overall health of the snap. 46 47 status can be one of: 48 49 - okay: the snap is healthy. This status takes no message and no code. 50 51 - waiting: a resource needed by the snap (e.g. a device, network, or service) is 52 not ready and the user will need to wait. The message must explain what 53 resource is being waited for. 54 55 - blocked: something needs doing to unblock the snap (e.g. a service needs to be 56 configured); the message must be sufficient to point the user in the right 57 direction. 58 59 - error: something is broken; the message must explain what. 60 `) 61 ) 62 63 func init() { 64 addCommand("set-health", shortHealthHelp, longHealthHelp, func() command { return &healthCommand{} }) 65 } 66 67 type healthPositional struct { 68 Status string `positional-arg-name:"<status>" required:"yes" description:"a valid health status; required."` 69 Message string `positional-arg-name:"<message>" description:"a short human-readable explanation of the status (when not okay). Must be longer than 7 characters, and will be truncated if over 70. Message cannot be provided if status is okay, but is required otherwise."` 70 } 71 72 type healthCommand struct { 73 baseCommand 74 healthPositional `positional-args:"yes"` 75 Code string `long:"code" value-name:"<code>" description:"optional tool-friendly value representing the problem that makes the snap unhealthy. Not a number, but a word with 3-30 characters matching [a-z](-?[a-z0-9])+"` 76 } 77 78 var ( 79 validCode = regexp.MustCompile(`^[a-z](?:-?[a-z0-9])+$`).MatchString 80 ) 81 82 func (c *healthCommand) Execute([]string) error { 83 if c.Status == "okay" && (len(c.Message) > 0 || len(c.Code) > 0) { 84 return fmt.Errorf(`when status is "okay", message and code must be empty`) 85 } 86 87 status, err := healthstate.StatusLookup(c.Status) 88 if err != nil { 89 return err 90 } 91 if status == healthstate.UnknownStatus { 92 return fmt.Errorf(`status cannot be manually set to "unknown"`) 93 } 94 95 if len(c.Code) > 0 { 96 if len(c.Code) < 3 || len(c.Code) > 30 { 97 return fmt.Errorf("code must have between 3 and 30 characters, got %d", len(c.Code)) 98 } 99 if !validCode(c.Code) { 100 return fmt.Errorf("invalid code %q (code must start with lowercase ASCII letters, and contain only ASCII letters and numbers, optionally separated by single dashes)", c.Code) // technically not dashes but hyphen-minuses 101 } 102 } 103 104 if status != healthstate.OkayStatus { 105 if len(c.Message) == 0 { 106 return fmt.Errorf(`when status is not "okay", message is required`) 107 } 108 109 rmsg := []rune(c.Message) 110 if len(rmsg) < 7 { 111 return fmt.Errorf(`message must be at least 7 characters long (got %d)`, len(rmsg)) 112 } 113 if len(rmsg) > 70 { 114 c.Message = string(rmsg[:69]) + "…" 115 } 116 } 117 118 ctx := c.context() 119 if ctx == nil { 120 // reuses the i18n'ed error message from service ctl 121 return fmt.Errorf(i18n.G("cannot %s without a context"), "set-health") 122 } 123 ctx.Lock() 124 defer ctx.Unlock() 125 126 var v struct{} 127 128 // if 'health' is there we've either already added an OnDone (and the 129 // following Set("health"), or we're in the set-health hook itself 130 // (which sets it to a dummy entry for this purpose). 131 if err := ctx.Get("health", &v); err == state.ErrNoState { 132 ctx.OnDone(func() error { 133 return healthstate.SetFromHookContext(ctx) 134 }) 135 } 136 137 health := &healthstate.HealthState{ 138 Revision: ctx.SnapRevision(), // will be "unset" for unasserted installs, and trys 139 Timestamp: time.Now(), 140 Status: status, 141 Message: c.Message, 142 Code: c.Code, 143 } 144 145 ctx.Set("health", health) 146 147 return nil 148 }