github.com/engineyard/workflow-cli@v2.21.6+incompatible/parser/healthchecks.go (about)

     1  package parser
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  
     8  	"github.com/teamhephy/workflow-cli/cmd"
     9  
    10  	"github.com/teamhephy/controller-sdk-go/api"
    11  	docopt "github.com/docopt/docopt-go"
    12  )
    13  
    14  // TODO: This is for supporting backward compatibility and should be removed
    15  // in future when next major version will be released.
    16  const (
    17  	defaultProcType string = "web/cmd"
    18  )
    19  
    20  // Healthchecks routes ealthcheck commands to their specific function
    21  func Healthchecks(argv []string, cmdr cmd.Commander) error {
    22  	usage := `
    23  Valid commands for healthchecks:
    24  
    25  healthchecks:list        list healthchecks for an app
    26  healthchecks:set         set healthchecks for an app
    27  healthchecks:unset       unset healthchecks for an app
    28  
    29  Use 'deis help [command]' to learn more.
    30  `
    31  
    32  	switch argv[0] {
    33  	case "healthchecks:list":
    34  		return healthchecksList(argv, cmdr)
    35  	case "healthchecks:set":
    36  		return healthchecksSet(argv, cmdr)
    37  	case "healthchecks:unset":
    38  		return healthchecksUnset(argv, cmdr)
    39  	default:
    40  		if printHelp(argv, usage) {
    41  			return nil
    42  		}
    43  
    44  		if argv[0] == "healthchecks" {
    45  			argv[0] = "healthchecks:list"
    46  			return healthchecksList(argv, cmdr)
    47  		}
    48  
    49  		PrintUsage(cmdr)
    50  		return nil
    51  	}
    52  }
    53  
    54  func healthchecksList(argv []string, cmdr cmd.Commander) error {
    55  	usage := `
    56  Lists healthchecks for an application.
    57  
    58  Usage: deis healthchecks:list [options]
    59  
    60  Options:
    61    -a --app=<app>
    62      the uniquely identifiable name of the application.
    63    --type=<type>
    64      the procType for which the health check needs to be listed.
    65  `
    66  
    67  	args, err := docopt.Parse(usage, argv, true, "", false, true)
    68  
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	app := safeGetValue(args, "--app")
    74  	procType := safeGetValue(args, "--type")
    75  
    76  	return cmdr.HealthchecksList(app, procType)
    77  }
    78  
    79  func healthchecksSet(argv []string, cmdr cmd.Commander) error {
    80  	usage := `
    81  Sets healthchecks for an application.
    82  
    83  By default, Workflow only checks that the application starts in their Container. A health
    84  check may be added by configuring a health check probe for the application. The health
    85  checks are implemented as Kubernetes Container Probes. A 'liveness' and a 'readiness'
    86  probe can be configured, and each probe can be of type 'httpGet', 'exec' or 'tcpSocket'
    87  depending on the type of probe the Container requires.
    88  
    89  A 'liveness' probe is useful for applications running for long periods of time, eventually
    90  transitioning to broken states and cannot recover except by restarting them.
    91  
    92  Other times, a 'readiness' probe is useful when the Container is only temporarily unable
    93  to serve, and will recover on its own. In this case, if a Container fails its 'readiness'
    94  probe, the Container will not be shut down, but rather the Container will stop receiving
    95  incoming requests.
    96  
    97  'httpGet' probes are just as it sounds: it performs a HTTP GET operation on the Container.
    98  A response code inside the 200-399 range is considered a pass. 'httpGet' probes accept a
    99  port number to perform the HTTP GET operation on the Container.
   100  
   101  'exec' probes run a command inside the Container to determine its health. An exit code of
   102  zero is considered a pass, while a non-zero status code is considered a fail. 'exec'
   103  probes accept a string of arguments to be run inside the Container.
   104  
   105  'tcpSocket' probes attempt to open a socket in the Container. The Container is only
   106  considered healthy if the check can establish a connection. 'tcpSocket' probes accept a
   107  port number to perform the socket connection on the Container.
   108  
   109  Usage: deis healthchecks:set <health-type> <probe-type> [options] [--] <args>...
   110  
   111  Arguments:
   112    <health-type>
   113      the healthcheck type, such as 'liveness' or 'readiness'.
   114    <probe-type>
   115      the healthcheck probe type, such as 'httpGet', 'exec' or 'tcpSocket'.
   116    <args>
   117      The arguments required for the healthcheck probe. 'exec', accepts a list of arguments;
   118      'httpGet' and 'tcpSocket' accept a port number.
   119  
   120  Options:
   121    -a --app=<app>
   122      the uniquely identifiable name for the application.
   123    -p --path=<path>
   124      the relative URL path for 'httpGet' probes. [default: /]
   125    --type=<type>
   126      the procType for which the health check needs to be applied.
   127    --headers=<headers>...
   128      the HTTP headers to send for 'httpGet' probes, separated by commas.
   129    --initial-delay-timeout=<initial-delay-timeout>
   130      the initial delay timeout for the probe [default: 50]
   131    --timeout-seconds=<timeout-seconds>
   132      the number of seconds after which the probe times out [default: 50]
   133    --period-seconds=<period-seconds>
   134      how often (in seconds) to perform the probe [default: 10]
   135    --success-threshold=<success-threshold>
   136      minimum consecutive successes for the probe to be considered successful after having failed [default: 1]
   137    --failure-threshold=<failure-threshold>
   138      minimum consecutive successes for the probe to be considered failed after having succeeded [default: 3]
   139  `
   140  
   141  	args, err := docopt.Parse(usage, argv, true, "", false, true)
   142  
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	app := safeGetValue(args, "--app")
   148  	path := safeGetValue(args, "--path")
   149  	procType := safeGetValue(args, "--type")
   150  	initialDelayTimeout := safeGetInt(args, "--initial-delay-timeout")
   151  	timeoutSeconds := safeGetInt(args, "--timeout-seconds")
   152  	periodSeconds := safeGetInt(args, "--period-seconds")
   153  	successThreshold := safeGetInt(args, "--success-threshold")
   154  	failureThreshold := safeGetInt(args, "--failure-threshold")
   155  	headers := []string{}
   156  	if args["--headers"] != nil {
   157  		headers = strings.Split(args["--headers"].(string), ",")
   158  	}
   159  	if procType == "" {
   160  		procType = defaultProcType
   161  	}
   162  
   163  	healthcheckType := args["<health-type>"].(string)
   164  	probeType := args["<probe-type>"].(string)
   165  	probeArgs := args["<args>"].([]string)
   166  
   167  	if err := checkProbeType(healthcheckType); err != nil {
   168  		return err
   169  	}
   170  
   171  	// NOTE(bacongobbler): k8s healthchecks use the term "livenessProbe" and "readinessProbe", so let's
   172  	// add that to the end of the healthcheck type so the controller sees the right probe type
   173  	healthcheckType += "Probe"
   174  
   175  	probe := &api.Healthcheck{
   176  		InitialDelaySeconds: initialDelayTimeout,
   177  		TimeoutSeconds:      timeoutSeconds,
   178  		PeriodSeconds:       periodSeconds,
   179  		SuccessThreshold:    successThreshold,
   180  		FailureThreshold:    failureThreshold,
   181  	}
   182  
   183  	switch probeType {
   184  	case "httpGet":
   185  		parsedHeaders, err := parseHeaders(headers)
   186  		if err != nil {
   187  			return fmt.Errorf("could not parse headers: %s", err)
   188  		}
   189  		port, err := strconv.Atoi(probeArgs[0])
   190  		if err != nil {
   191  			return fmt.Errorf("could not parse port: %s", err)
   192  		}
   193  		probe.HTTPGet = &api.HTTPGetProbe{
   194  			Path:        path,
   195  			Port:        port,
   196  			HTTPHeaders: parsedHeaders,
   197  		}
   198  	case "exec":
   199  		probe.Exec = &api.ExecProbe{
   200  			Command: probeArgs,
   201  		}
   202  	case "tcpSocket":
   203  		port, err := strconv.Atoi(probeArgs[0])
   204  		if err != nil {
   205  			return fmt.Errorf("could not parse port: %s", err)
   206  		}
   207  		probe.TCPSocket = &api.TCPSocketProbe{
   208  			Port: port,
   209  		}
   210  	default:
   211  		return fmt.Errorf("Invalid probe type. Must be one of: \"httpGet\", \"exec\"")
   212  	}
   213  
   214  	return cmdr.HealthchecksSet(app, healthcheckType, procType, probe)
   215  }
   216  
   217  func healthchecksUnset(argv []string, cmdr cmd.Commander) error {
   218  	usage := `
   219  Unsets healthchecks for an application.
   220  
   221  Usage: deis healthchecks:unset [options] <health-type>...
   222  
   223  Arguments:
   224    <health-type>
   225      the healthcheck type, such as 'liveness' or 'readiness'.
   226  
   227  Options:
   228    -a --app=<app>
   229      the uniquely identifiable name for the application.
   230    --type=<type>
   231      the procType for which the health check needs to be removed.
   232  `
   233  
   234  	args, err := docopt.Parse(usage, argv, true, "", false, true)
   235  
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	app := safeGetValue(args, "--app")
   241  	healthchecks := args["<health-type>"].([]string)
   242  	procType := safeGetValue(args, "--type")
   243  	if procType == "" {
   244  		procType = defaultProcType
   245  	}
   246  
   247  	// NOTE(bacongobbler): k8s healthchecks use the term "livenessProbe" and "readinessProbe", so let's
   248  	// add that to the end of the healthcheck type so the controller sees the right probe type
   249  	for healthcheck := range healthchecks {
   250  		if err := checkProbeType(healthchecks[healthcheck]); err != nil {
   251  			return err
   252  		}
   253  		healthchecks[healthcheck] += "Probe"
   254  	}
   255  
   256  	return cmdr.HealthchecksUnset(app, procType, healthchecks)
   257  }
   258  
   259  func parseHeaders(headers []string) ([]*api.KVPair, error) {
   260  	var parsedHeaders []*api.KVPair
   261  	for _, header := range headers {
   262  		parsedHeader, err := parseHeader(header)
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  		parsedHeaders = append(parsedHeaders, parsedHeader)
   267  	}
   268  	return parsedHeaders, nil
   269  }
   270  
   271  func parseHeader(header string) (*api.KVPair, error) {
   272  	headerParts := strings.SplitN(header, ":", 2)
   273  	if len(headerParts) != 2 {
   274  		return nil, fmt.Errorf("could not find separator in header (%s)", header)
   275  	}
   276  	return &api.KVPair{
   277  		Name:  strings.TrimSpace(headerParts[0]),
   278  		Value: strings.TrimSpace(headerParts[1]),
   279  	}, nil
   280  }
   281  
   282  func checkProbeType(probe string) error {
   283  	var found bool
   284  	probeTypes := []string{
   285  		"liveness",
   286  		"readiness",
   287  	}
   288  	for _, ptype := range probeTypes {
   289  		if probe == ptype {
   290  			found = true
   291  		}
   292  	}
   293  	if !found {
   294  		return fmt.Errorf("probe type %s is invalid. Must be one of %s", probe, probeTypes)
   295  	}
   296  	return nil
   297  }