github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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) (bool, error) {
   149  	return false, 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  }