github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/overlord/healthstate/healthstate.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 healthstate 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "os" 26 "regexp" 27 "time" 28 29 "github.com/snapcore/snapd/logger" 30 "github.com/snapcore/snapd/overlord/hookstate" 31 "github.com/snapcore/snapd/overlord/snapstate" 32 "github.com/snapcore/snapd/overlord/state" 33 "github.com/snapcore/snapd/snap" 34 "github.com/snapcore/snapd/strutil" 35 ) 36 37 var checkTimeout = 30 * time.Second 38 39 func init() { 40 if s, ok := os.LookupEnv("SNAPD_CHECK_HEALTH_HOOK_TIMEOUT"); ok { 41 if to, err := time.ParseDuration(s); err == nil { 42 checkTimeout = to 43 } else { 44 logger.Debugf("cannot override check-health timeout: %v", err) 45 } 46 } 47 48 snapstate.CheckHealthHook = Hook 49 } 50 51 func Hook(st *state.State, snapName string, snapRev snap.Revision) *state.Task { 52 summary := fmt.Sprintf("Run health check of %q snap", snapName) 53 hooksup := &hookstate.HookSetup{ 54 Snap: snapName, 55 Revision: snapRev, 56 Hook: "check-health", 57 Optional: true, 58 Timeout: checkTimeout, 59 } 60 61 return hookstate.HookTask(st, summary, hooksup, nil) 62 } 63 64 type HealthStatus int 65 66 const ( 67 UnknownStatus = HealthStatus(iota) 68 OkayStatus 69 WaitingStatus 70 BlockedStatus 71 ErrorStatus 72 ) 73 74 var knownStatuses = []string{"unknown", "okay", "waiting", "blocked", "error"} 75 76 func StatusLookup(str string) (HealthStatus, error) { 77 for i, k := range knownStatuses { 78 if k == str { 79 return HealthStatus(i), nil 80 } 81 } 82 return -1, fmt.Errorf("invalid status %q, must be one of %s", str, strutil.Quoted(knownStatuses)) 83 } 84 85 func (s HealthStatus) String() string { 86 if s < 0 || s >= HealthStatus(len(knownStatuses)) { 87 return fmt.Sprintf("invalid (%d)", s) 88 } 89 return knownStatuses[s] 90 } 91 92 type HealthState struct { 93 Revision snap.Revision `json:"revision"` 94 Timestamp time.Time `json:"timestamp"` 95 Status HealthStatus `json:"status"` 96 Message string `json:"message,omitempty"` 97 Code string `json:"code,omitempty"` 98 } 99 100 func Init(hookManager *hookstate.HookManager) { 101 hookManager.Register(regexp.MustCompile("^check-health$"), newHealthHandler) 102 } 103 104 func newHealthHandler(ctx *hookstate.Context) hookstate.Handler { 105 return &healthHandler{context: ctx} 106 } 107 108 type healthHandler struct { 109 context *hookstate.Context 110 } 111 112 // Before is called just before the hook runs -- nothing to do beyond setting a marker 113 func (h *healthHandler) Before() error { 114 // we use the 'health' entry as a marker to not add OnDone to 115 // the snapctl set-health execution 116 h.context.Lock() 117 h.context.Set("health", struct{}{}) 118 h.context.Unlock() 119 return nil 120 } 121 122 func (h *healthHandler) Done() error { 123 var health HealthState 124 125 h.context.Lock() 126 err := h.context.Get("health", &health) 127 h.context.Unlock() 128 129 if err != nil && err != state.ErrNoState { 130 // note it can't actually be state.ErrNoState because Before sets it 131 // (but if it were, health.Timestamp would still be zero) 132 return err 133 } 134 if health.Timestamp.IsZero() { 135 // health was actually the marker (or err == state.ErrNoState) 136 health = HealthState{ 137 Revision: h.context.SnapRevision(), 138 Timestamp: time.Now(), 139 Status: UnknownStatus, 140 Code: "snapd-hook-no-health-set", 141 Message: "hook did not call set-health", 142 } 143 } 144 145 return h.appendHealth(&health) 146 } 147 148 func (h *healthHandler) Error(err error) error { 149 return h.appendHealth(&HealthState{ 150 Revision: h.context.SnapRevision(), 151 Timestamp: time.Now(), 152 Status: UnknownStatus, 153 Code: "snapd-hook-failed", 154 Message: "hook failed", 155 }) 156 } 157 158 func (h *healthHandler) appendHealth(health *HealthState) error { 159 st := h.context.State() 160 st.Lock() 161 defer st.Unlock() 162 163 return appendHealth(h.context, health) 164 } 165 166 func appendHealth(ctx *hookstate.Context, health *HealthState) error { 167 st := ctx.State() 168 169 var hs map[string]*HealthState 170 if err := st.Get("health", &hs); err != nil { 171 if err != state.ErrNoState { 172 return err 173 } 174 hs = map[string]*HealthState{} 175 } 176 hs[ctx.InstanceName()] = health 177 st.Set("health", hs) 178 179 return nil 180 } 181 182 // SetFromHookContext extracts the health of a snap from a hook 183 // context, and saves it in snapd's state. 184 // Must be called with the context lock held. 185 func SetFromHookContext(ctx *hookstate.Context) error { 186 var health HealthState 187 err := ctx.Get("health", &health) 188 189 if err != nil { 190 if err == state.ErrNoState { 191 return nil 192 } 193 return err 194 } 195 return appendHealth(ctx, &health) 196 } 197 198 func All(st *state.State) (map[string]*HealthState, error) { 199 var hs map[string]*HealthState 200 if err := st.Get("health", &hs); err != nil && err != state.ErrNoState { 201 return nil, err 202 } 203 return hs, nil 204 } 205 206 func Get(st *state.State, snap string) (*HealthState, error) { 207 var hs map[string]json.RawMessage 208 if err := st.Get("health", &hs); err != nil { 209 if err != state.ErrNoState { 210 return nil, err 211 } 212 return nil, nil 213 } 214 215 buf := hs[snap] 216 if len(buf) == 0 { 217 return nil, nil 218 } 219 220 var health HealthState 221 if err := json.Unmarshal(buf, &health); err != nil { 222 return nil, err 223 } 224 225 return &health, nil 226 }