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 }