github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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  }