pkg/cmd/cli/cmd/set/probe.go
b2de2910
 package set
 
 import (
 	"fmt"
 	"io"
fe7e661b
 	"net"
b2de2910
 	"net/url"
 	"os"
 	"strconv"
fe7e661b
 	"strings"
b2de2910
 
 	"github.com/spf13/cobra"
 	kapi "k8s.io/kubernetes/pkg/api"
 	"k8s.io/kubernetes/pkg/api/meta"
5a556e67
 	"k8s.io/kubernetes/pkg/api/unversioned"
b2de2910
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
 	"k8s.io/kubernetes/pkg/kubectl/resource"
 	"k8s.io/kubernetes/pkg/runtime"
fe7e661b
 	"k8s.io/kubernetes/pkg/util/intstr"
b2de2910
 
6267dded
 	"github.com/openshift/origin/pkg/cmd/templates"
b2de2910
 	cmdutil "github.com/openshift/origin/pkg/cmd/util"
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
 )
 
6267dded
 var (
 	probeLong = templates.LongDesc(`
 		Set or remove a liveness or readiness probe from a pod or pod template
b2de2910
 
6267dded
 		Each container in a pod may define one or more probes that are used for general health
 		checking. A liveness probe is checked periodically to ensure the container is still healthy:
 		if the probe fails, the container is restarted. Readiness probes set or clear the ready
 		flag for each container, which controls whether the container's ports are included in the list
 		of endpoints for a service and whether a deployment can proceed. A readiness check should
 		indicate when your container is ready to accept incoming traffic or begin handling work.
 		Setting both liveness and readiness probes for each container is highly recommended.
b2de2910
 
6267dded
 		The three probe types are:
b2de2910
 
6267dded
 		1. Open a TCP socket on the pod IP
 		2. Perform an HTTP GET against a URL on a container that must return 200 OK
 		3. Run a command in the container that must return exit code 0
b2de2910
 
6267dded
 		Containers that take a variable amount of time to start should set generous
 		initial-delay-seconds values, otherwise as your application evolves you may suddenly begin
 		to fail.`)
b2de2910
 
6267dded
 	probeExample = templates.Examples(`
 		# Clear both readiness and liveness probes off all containers
 	  %[1]s probe dc/registry --remove --readiness --liveness
b2de2910
 
6267dded
 	  # Set an exec action as a liveness probe to run 'echo ok'
 	  %[1]s probe dc/registry --liveness -- echo ok
b2de2910
 
6267dded
 	  # Set a readiness probe to try to open a TCP socket on 3306
 	  %[1]s probe rc/mysql --readiness --open-tcp=3306
b2de2910
 
6267dded
 	  # Set an HTTP readiness probe for port 8080 and path /healthz over HTTP on the pod IP
 	  %[1]s probe dc/webapp --readiness --get-url=http://:8080/healthz
b2de2910
 
6267dded
 	  # Set an HTTP readiness probe over HTTPS on 127.0.0.1 for a hostNetwork pod
 	  %[1]s probe dc/router --readiness --get-url=https://127.0.0.1:1936/stats
b2de2910
 
6267dded
 	  # Set only the initial-delay-seconds field on all deployments
 	  %[1]s probe dc --all --readiness --initial-delay-seconds=30`)
b2de2910
 )
 
 type ProbeOptions struct {
 	Out io.Writer
 	Err io.Writer
 
 	Filenames         []string
 	ContainerSelector string
 	Selector          string
 	All               bool
 
 	Builder *resource.Builder
 	Infos   []*resource.Info
 
 	Encoder runtime.Encoder
 
5a556e67
 	ShortOutput   bool
 	Mapper        meta.RESTMapper
 	OutputVersion unversioned.GroupVersion
b2de2910
 
0650f99d
 	PrintObject            func([]*resource.Info) error
b2de2910
 	UpdatePodSpecForObject func(runtime.Object, func(spec *kapi.PodSpec) error) (bool, error)
 
 	Readiness bool
 	Liveness  bool
 	Remove    bool
 
 	OpenTCPSocket string
 	HTTPGet       string
 	Command       []string
 
 	FlagSet       func(string) bool
 	HTTPGetAction *kapi.HTTPGetAction
 
 	// Length of time before health checking is activated.  In seconds.
 	InitialDelaySeconds *int
 	// Length of time before health checking times out.  In seconds.
 	TimeoutSeconds *int
 	// How often (in seconds) to perform the probe.
 	PeriodSeconds *int
 	// Minimum consecutive successes for the probe to be considered successful after having failed.
 	// Must be 1 for liveness.
 	SuccessThreshold *int
 	// Minimum consecutive failures for the probe to be considered failed after having succeeded.
 	FailureThreshold *int
 }
 
 // NewCmdProbe implements the set probe command
 func NewCmdProbe(fullName string, f *clientcmd.Factory, out, errOut io.Writer) *cobra.Command {
 	options := &ProbeOptions{
 		Out: out,
 		Err: errOut,
 
 		ContainerSelector: "*",
 	}
 	cmd := &cobra.Command{
fdcb9dc6
 		Use:     "probe RESOURCE/NAME --readiness|--liveness [options] (--get-url=URL|--open-tcp=PORT|-- CMD)",
b2de2910
 		Short:   "Update a probe on a pod template",
 		Long:    probeLong,
 		Example: fmt.Sprintf(probeExample, fullName),
 		Run: func(cmd *cobra.Command, args []string) {
 			kcmdutil.CheckErr(options.Complete(f, cmd, args))
 			kcmdutil.CheckErr(options.Validate())
 			if err := options.Run(); err != nil {
d7da290f
 				// TODO: move me to kcmdutil
b2de2910
 				if err == cmdutil.ErrExit {
 					os.Exit(1)
 				}
 				kcmdutil.CheckErr(err)
 			}
 		},
 	}
 
 	kcmdutil.AddPrinterFlags(cmd)
 	cmd.Flags().StringVarP(&options.ContainerSelector, "containers", "c", options.ContainerSelector, "The names of containers in the selected pod templates to change - may use wildcards")
 	cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter on")
9c93da29
 	cmd.Flags().BoolVar(&options.All, "all", options.All, "If true, select all resources in the namespace of the specified resource types")
b2de2910
 	cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename, directory, or URL to file to use to edit the resource.")
 
 	cmd.Flags().BoolVar(&options.Remove, "remove", options.Remove, "If true, remove the specified probe(s).")
 	cmd.Flags().BoolVar(&options.Readiness, "readiness", options.Readiness, "Set or remove a readiness probe to indicate when this container should receive traffic")
 	cmd.Flags().BoolVar(&options.Liveness, "liveness", options.Liveness, "Set or remove a liveness probe to verify this container is running")
 
 	cmd.Flags().StringVar(&options.OpenTCPSocket, "open-tcp", options.OpenTCPSocket, "A port number or port name to attempt to open via TCP.")
 	cmd.Flags().StringVar(&options.HTTPGet, "get-url", options.HTTPGet, "A URL to perform an HTTP GET on (you can omit the host, have a string port, or omit the scheme.")
 	options.InitialDelaySeconds = cmd.Flags().Int("initial-delay-seconds", 0, "The time in seconds to wait before the probe begins checking")
 	options.SuccessThreshold = cmd.Flags().Int("success-threshold", 0, "The number of successes required before the probe is considered successful")
 	options.FailureThreshold = cmd.Flags().Int("failure-threshold", 0, "The number of failures before the probe is considered to have failed")
 	options.PeriodSeconds = cmd.Flags().Int("period-seconds", 0, "The time in seconds between attempts")
 	options.TimeoutSeconds = cmd.Flags().Int("timeout-seconds", 0, "The time in seconds to wait before considering the probe to have failed")
 
 	cmd.MarkFlagFilename("filename", "yaml", "yml", "json")
 
 	return cmd
 }
 
 func (o *ProbeOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error {
 	resources := args
 	if i := cmd.ArgsLenAtDash(); i != -1 {
 		resources = args[:i]
 		o.Command = args[i:]
 	}
 	if len(o.Filenames) == 0 && len(args) < 1 {
 		return kcmdutil.UsageError(cmd, "one or more resources must be specified as <resource> <name> or <resource>/<name>")
 	}
 
 	cmdNamespace, explicit, err := f.DefaultNamespace()
 	if err != nil {
 		return err
 	}
 
5a556e67
 	clientConfig, err := f.ClientConfig()
 	if err != nil {
 		return err
 	}
 
 	o.OutputVersion, err = kcmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
 	if err != nil {
 		return err
 	}
 
59e6f9d2
 	mapper, typer := f.Object(false)
b2de2910
 	o.Builder = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder()).
 		ContinueOnError().
 		NamespaceParam(cmdNamespace).DefaultNamespace().
c5dc7f15
 		FilenameParam(explicit, false, o.Filenames...).
b2de2910
 		SelectorParam(o.Selector).
 		ResourceTypeOrNameArgs(o.All, resources...).
 		Flatten()
 
 	output := kcmdutil.GetFlagString(cmd, "output")
0650f99d
 	if len(output) > 0 {
 		o.PrintObject = func(infos []*resource.Info) error {
 			return f.PrintResourceInfos(cmd, infos, o.Out)
 		}
b2de2910
 	}
 
 	o.Encoder = f.JSONEncoder()
 	o.UpdatePodSpecForObject = f.UpdatePodSpecForObject
 	o.ShortOutput = kcmdutil.GetFlagString(cmd, "output") == "name"
 	o.Mapper = mapper
 
 	if !cmd.Flags().Lookup("initial-delay-seconds").Changed {
 		o.InitialDelaySeconds = nil
 	}
 	if !cmd.Flags().Lookup("timeout-seconds").Changed {
 		o.TimeoutSeconds = nil
 	}
 	if !cmd.Flags().Lookup("period-seconds").Changed {
 		o.PeriodSeconds = nil
 	}
 	if !cmd.Flags().Lookup("success-threshold").Changed {
 		o.SuccessThreshold = nil
 	}
 	if !cmd.Flags().Lookup("failure-threshold").Changed {
 		o.FailureThreshold = nil
 	}
 
 	if len(o.HTTPGet) > 0 {
 		url, err := url.Parse(o.HTTPGet)
 		if err != nil {
 			return fmt.Errorf("--get-url could not be parsed as a valid URL: %v", err)
 		}
 		var host, port string
 		if strings.Contains(url.Host, ":") {
 			if host, port, err = net.SplitHostPort(url.Host); err != nil {
 				return fmt.Errorf("--get-url did not have a valid port specification: %v", err)
 			}
 		}
 		if host == "localhost" {
 			host = ""
 		}
 		o.HTTPGetAction = &kapi.HTTPGetAction{
 			Scheme: kapi.URIScheme(strings.ToUpper(url.Scheme)),
 			Host:   host,
 			Port:   intOrString(port),
 			Path:   url.Path,
 		}
 	}
 
 	return nil
 }
 
 func (o *ProbeOptions) Validate() error {
 	if !o.Readiness && !o.Liveness {
 		return fmt.Errorf("you must specify one of --readiness or --liveness or both")
 	}
 	count := 0
 	if o.Command != nil {
 		count++
 	}
 	if len(o.OpenTCPSocket) > 0 {
 		count++
 	}
 	if len(o.HTTPGet) > 0 {
 		count++
 	}
 
 	switch {
 	case o.Remove && count != 0:
 		return fmt.Errorf("--remove may not be used with any flag except --readiness or --liveness")
 	case count > 1:
 		return fmt.Errorf("you may only set one of --get-url, --open-tcp, or command")
 	case len(o.OpenTCPSocket) > 0 && intOrString(o.OpenTCPSocket).IntVal > 65535:
 		return fmt.Errorf("--open-tcp must be a port number between 1 and 65535 or an IANA port name")
 	}
 	if o.FailureThreshold != nil && *o.FailureThreshold < 1 {
 		return fmt.Errorf("--failure-threshold may not be less than one")
 	}
 	if o.SuccessThreshold != nil && *o.SuccessThreshold < 1 {
 		return fmt.Errorf("--success-threshold may not be less than one")
 	}
 	if o.InitialDelaySeconds != nil && *o.InitialDelaySeconds < 0 {
 		return fmt.Errorf("--initial-delay-seconds may not be negative")
 	}
 	if o.TimeoutSeconds != nil && *o.TimeoutSeconds < 0 {
 		return fmt.Errorf("--timeout-seconds may not be negative")
 	}
 	if o.PeriodSeconds != nil && *o.PeriodSeconds < 0 {
 		return fmt.Errorf("--period-seconds may not be negative")
 	}
 	return nil
 }
 
 func (o *ProbeOptions) Run() error {
 	infos := o.Infos
 	singular := len(o.Infos) <= 1
 	if o.Builder != nil {
 		loaded, err := o.Builder.Do().IntoSingular(&singular).Infos()
 		if err != nil {
 			return err
 		}
 		infos = loaded
 	}
 
 	patches := CalculatePatches(infos, o.Encoder, func(info *resource.Info) (bool, error) {
 		transformed := false
 		_, err := o.UpdatePodSpecForObject(info.Object, func(spec *kapi.PodSpec) error {
 			containers, _ := selectContainers(spec.Containers, o.ContainerSelector)
 			if len(containers) == 0 {
 				fmt.Fprintf(o.Err, "warning: %s/%s does not have any containers matching %q\n", info.Mapping.Resource, info.Name, o.ContainerSelector)
 				return nil
 			}
 			// perform updates
 			transformed = true
 			for _, container := range containers {
 				o.updateContainer(container)
 			}
 			return nil
 		})
 		return transformed, err
 	})
 	if singular && len(patches) == 0 {
 		return fmt.Errorf("%s/%s is not a pod or does not have a pod template", infos[0].Mapping.Resource, infos[0].Name)
 	}
 
 	if o.PrintObject != nil {
0650f99d
 		return o.PrintObject(infos)
b2de2910
 	}
 
 	failed := false
 	for _, patch := range patches {
 		info := patch.Info
 		if patch.Err != nil {
 			fmt.Fprintf(o.Err, "error: %s/%s %v\n", info.Mapping.Resource, info.Name, patch.Err)
 			continue
 		}
 
 		if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
 			fmt.Fprintf(o.Err, "info: %s %q was not changed\n", info.Mapping.Resource, info.Name)
 			continue
 		}
 
 		obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, kapi.StrategicMergePatchType, patch.Patch)
 		if err != nil {
 			handlePodUpdateError(o.Err, err, "probes")
 			failed = true
 			continue
 		}
 
 		info.Refresh(obj, true)
7a2339f4
 		kcmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, false, "updated")
b2de2910
 	}
 	if failed {
 		return cmdutil.ErrExit
 	}
 	return nil
 }
 
 func (o *ProbeOptions) updateContainer(container *kapi.Container) {
 	if o.Remove {
 		if o.Readiness {
 			container.ReadinessProbe = nil
 		}
 		if o.Liveness {
 			container.LivenessProbe = nil
 		}
 		return
 	}
 	if o.Readiness {
 		if container.ReadinessProbe == nil {
 			container.ReadinessProbe = &kapi.Probe{}
 		}
 		o.updateProbe(container.ReadinessProbe)
 	}
 	if o.Liveness {
 		if container.LivenessProbe == nil {
 			container.LivenessProbe = &kapi.Probe{}
 		}
 		o.updateProbe(container.LivenessProbe)
 	}
 }
 
 // updateProbe updates only those fields with flags set by the user
 func (o *ProbeOptions) updateProbe(probe *kapi.Probe) {
 	switch {
 	case o.Command != nil:
 		probe.Handler = kapi.Handler{Exec: &kapi.ExecAction{Command: o.Command}}
 	case o.HTTPGetAction != nil:
 		probe.Handler = kapi.Handler{HTTPGet: o.HTTPGetAction}
 	case len(o.OpenTCPSocket) > 0:
 		probe.Handler = kapi.Handler{TCPSocket: &kapi.TCPSocketAction{Port: intOrString(o.OpenTCPSocket)}}
 	}
 	if o.InitialDelaySeconds != nil {
37b6c5f1
 		probe.InitialDelaySeconds = int32(*o.InitialDelaySeconds)
b2de2910
 	}
 	if o.SuccessThreshold != nil {
37b6c5f1
 		probe.SuccessThreshold = int32(*o.SuccessThreshold)
b2de2910
 	}
 	if o.FailureThreshold != nil {
37b6c5f1
 		probe.FailureThreshold = int32(*o.FailureThreshold)
b2de2910
 	}
 	if o.TimeoutSeconds != nil {
37b6c5f1
 		probe.TimeoutSeconds = int32(*o.TimeoutSeconds)
b2de2910
 	}
 	if o.PeriodSeconds != nil {
37b6c5f1
 		probe.PeriodSeconds = int32(*o.PeriodSeconds)
b2de2910
 	}
 }
 
 func intOrString(s string) intstr.IntOrString {
 	if i, err := strconv.Atoi(s); err == nil {
 		return intstr.FromInt(i)
 	}
 	return intstr.FromString(s)
 }