k8s.io/kubernetes@v1.29.3/pkg/kubelet/lifecycle/handlers.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package lifecycle
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net"
    25  	"net/http"
    26  	"net/url"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	v1 "k8s.io/api/core/v1"
    32  	"k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/apimachinery/pkg/util/intstr"
    34  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    35  	"k8s.io/client-go/tools/record"
    36  	"k8s.io/klog/v2"
    37  	"k8s.io/kubernetes/pkg/features"
    38  	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
    39  	"k8s.io/kubernetes/pkg/kubelet/metrics"
    40  	kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
    41  	"k8s.io/kubernetes/pkg/kubelet/util/format"
    42  	httpprobe "k8s.io/kubernetes/pkg/probe/http"
    43  	"k8s.io/kubernetes/pkg/security/apparmor"
    44  )
    45  
    46  const (
    47  	maxRespBodyLength = 10 * 1 << 10 // 10KB
    48  )
    49  
    50  type handlerRunner struct {
    51  	httpDoer         kubetypes.HTTPDoer
    52  	commandRunner    kubecontainer.CommandRunner
    53  	containerManager podStatusProvider
    54  	eventRecorder    record.EventRecorder
    55  }
    56  
    57  type podStatusProvider interface {
    58  	GetPodStatus(ctx context.Context, uid types.UID, name, namespace string) (*kubecontainer.PodStatus, error)
    59  }
    60  
    61  // NewHandlerRunner returns a configured lifecycle handler for a container.
    62  func NewHandlerRunner(httpDoer kubetypes.HTTPDoer, commandRunner kubecontainer.CommandRunner, containerManager podStatusProvider, eventRecorder record.EventRecorder) kubecontainer.HandlerRunner {
    63  	return &handlerRunner{
    64  		httpDoer:         httpDoer,
    65  		commandRunner:    commandRunner,
    66  		containerManager: containerManager,
    67  		eventRecorder:    eventRecorder,
    68  	}
    69  }
    70  
    71  func (hr *handlerRunner) Run(ctx context.Context, containerID kubecontainer.ContainerID, pod *v1.Pod, container *v1.Container, handler *v1.LifecycleHandler) (string, error) {
    72  	switch {
    73  	case handler.Exec != nil:
    74  		var msg string
    75  		// TODO(tallclair): Pass a proper timeout value.
    76  		output, err := hr.commandRunner.RunInContainer(ctx, containerID, handler.Exec.Command, 0)
    77  		if err != nil {
    78  			msg = fmt.Sprintf("Exec lifecycle hook (%v) for Container %q in Pod %q failed - error: %v, message: %q", handler.Exec.Command, container.Name, format.Pod(pod), err, string(output))
    79  			klog.V(1).ErrorS(err, "Exec lifecycle hook for Container in Pod failed", "execCommand", handler.Exec.Command, "containerName", container.Name, "pod", klog.KObj(pod), "message", string(output))
    80  		}
    81  		return msg, err
    82  	case handler.HTTPGet != nil:
    83  		err := hr.runHTTPHandler(ctx, pod, container, handler, hr.eventRecorder)
    84  		var msg string
    85  		if err != nil {
    86  			msg = fmt.Sprintf("HTTP lifecycle hook (%s) for Container %q in Pod %q failed - error: %v", handler.HTTPGet.Path, container.Name, format.Pod(pod), err)
    87  			klog.V(1).ErrorS(err, "HTTP lifecycle hook for Container in Pod failed", "path", handler.HTTPGet.Path, "containerName", container.Name, "pod", klog.KObj(pod))
    88  		}
    89  		return msg, err
    90  	case handler.Sleep != nil:
    91  		err := hr.runSleepHandler(ctx, handler.Sleep.Seconds)
    92  		var msg string
    93  		if err != nil {
    94  			msg = fmt.Sprintf("Sleep lifecycle hook (%d) for Container %q in Pod %q failed - error: %v", handler.Sleep.Seconds, container.Name, format.Pod(pod), err)
    95  			klog.V(1).ErrorS(err, "Sleep lifecycle hook for Container in Pod failed", "sleepSeconds", handler.Sleep.Seconds, "containerName", container.Name, "pod", klog.KObj(pod))
    96  		}
    97  		return msg, err
    98  	default:
    99  		err := fmt.Errorf("invalid handler: %v", handler)
   100  		msg := fmt.Sprintf("Cannot run handler: %v", err)
   101  		klog.ErrorS(err, "Cannot run handler")
   102  		return msg, err
   103  	}
   104  }
   105  
   106  // resolvePort attempts to turn an IntOrString port reference into a concrete port number.
   107  // If portReference has an int value, it is treated as a literal, and simply returns that value.
   108  // If portReference is a string, an attempt is first made to parse it as an integer.  If that fails,
   109  // an attempt is made to find a port with the same name in the container spec.
   110  // If a port with the same name is found, it's ContainerPort value is returned.  If no matching
   111  // port is found, an error is returned.
   112  func resolvePort(portReference intstr.IntOrString, container *v1.Container) (int, error) {
   113  	if portReference.Type == intstr.Int {
   114  		return portReference.IntValue(), nil
   115  	}
   116  	portName := portReference.StrVal
   117  	port, err := strconv.Atoi(portName)
   118  	if err == nil {
   119  		return port, nil
   120  	}
   121  	for _, portSpec := range container.Ports {
   122  		if portSpec.Name == portName {
   123  			return int(portSpec.ContainerPort), nil
   124  		}
   125  	}
   126  	return -1, fmt.Errorf("couldn't find port: %v in %v", portReference, container)
   127  }
   128  
   129  func (hr *handlerRunner) runSleepHandler(ctx context.Context, seconds int64) error {
   130  	if !utilfeature.DefaultFeatureGate.Enabled(features.PodLifecycleSleepAction) {
   131  		return nil
   132  	}
   133  	c := time.After(time.Duration(seconds) * time.Second)
   134  	select {
   135  	case <-ctx.Done():
   136  		// unexpected termination
   137  		return fmt.Errorf("container terminated before sleep hook finished")
   138  	case <-c:
   139  		return nil
   140  	}
   141  }
   142  
   143  func (hr *handlerRunner) runHTTPHandler(ctx context.Context, pod *v1.Pod, container *v1.Container, handler *v1.LifecycleHandler, eventRecorder record.EventRecorder) error {
   144  	host := handler.HTTPGet.Host
   145  	podIP := host
   146  	if len(host) == 0 {
   147  		status, err := hr.containerManager.GetPodStatus(ctx, pod.UID, pod.Name, pod.Namespace)
   148  		if err != nil {
   149  			klog.ErrorS(err, "Unable to get pod info, event handlers may be invalid.", "pod", klog.KObj(pod))
   150  			return err
   151  		}
   152  		if len(status.IPs) == 0 {
   153  			return fmt.Errorf("failed to find networking container: %v", status)
   154  		}
   155  		host = status.IPs[0]
   156  		podIP = host
   157  	}
   158  
   159  	if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentHTTPGetHandlers) {
   160  		req, err := httpprobe.NewRequestForHTTPGetAction(handler.HTTPGet, container, podIP, "lifecycle")
   161  		if err != nil {
   162  			return err
   163  		}
   164  		resp, err := hr.httpDoer.Do(req)
   165  		discardHTTPRespBody(resp)
   166  
   167  		if isHTTPResponseError(err) {
   168  			klog.V(1).ErrorS(err, "HTTPS request to lifecycle hook got HTTP response, retrying with HTTP.", "pod", klog.KObj(pod), "host", req.URL.Host)
   169  
   170  			req := req.Clone(context.Background())
   171  			req.URL.Scheme = "http"
   172  			req.Header.Del("Authorization")
   173  			resp, httpErr := hr.httpDoer.Do(req)
   174  
   175  			// clear err since the fallback succeeded
   176  			if httpErr == nil {
   177  				metrics.LifecycleHandlerHTTPFallbacks.Inc()
   178  				if eventRecorder != nil {
   179  					// report the fallback with an event
   180  					eventRecorder.Event(pod, v1.EventTypeWarning, "LifecycleHTTPFallback", fmt.Sprintf("request to HTTPS lifecycle hook %s got HTTP response, retry with HTTP succeeded", req.URL.Host))
   181  				}
   182  				err = nil
   183  			}
   184  			discardHTTPRespBody(resp)
   185  		}
   186  		return err
   187  	}
   188  
   189  	// Deprecated code path.
   190  	var port int
   191  	if handler.HTTPGet.Port.Type == intstr.String && len(handler.HTTPGet.Port.StrVal) == 0 {
   192  		port = 80
   193  	} else {
   194  		var err error
   195  		port, err = resolvePort(handler.HTTPGet.Port, container)
   196  		if err != nil {
   197  			return err
   198  		}
   199  	}
   200  
   201  	url := fmt.Sprintf("http://%s/%s", net.JoinHostPort(host, strconv.Itoa(port)), handler.HTTPGet.Path)
   202  	req, err := http.NewRequest(http.MethodGet, url, nil)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	resp, err := hr.httpDoer.Do(req)
   207  
   208  	discardHTTPRespBody(resp)
   209  	return err
   210  }
   211  
   212  func discardHTTPRespBody(resp *http.Response) {
   213  	if resp == nil {
   214  		return
   215  	}
   216  
   217  	// Ensure the response body is fully read and closed
   218  	// before we reconnect, so that we reuse the same TCP
   219  	// connection.
   220  	defer resp.Body.Close()
   221  
   222  	if resp.ContentLength <= maxRespBodyLength {
   223  		io.Copy(io.Discard, &io.LimitedReader{R: resp.Body, N: maxRespBodyLength})
   224  	}
   225  }
   226  
   227  // NewAppArmorAdmitHandler returns a PodAdmitHandler which is used to evaluate
   228  // if a pod can be admitted from the perspective of AppArmor.
   229  func NewAppArmorAdmitHandler(validator apparmor.Validator) PodAdmitHandler {
   230  	return &appArmorAdmitHandler{
   231  		Validator: validator,
   232  	}
   233  }
   234  
   235  type appArmorAdmitHandler struct {
   236  	apparmor.Validator
   237  }
   238  
   239  func (a *appArmorAdmitHandler) Admit(attrs *PodAdmitAttributes) PodAdmitResult {
   240  	// If the pod is already running or terminated, no need to recheck AppArmor.
   241  	if attrs.Pod.Status.Phase != v1.PodPending {
   242  		return PodAdmitResult{Admit: true}
   243  	}
   244  
   245  	err := a.Validate(attrs.Pod)
   246  	if err == nil {
   247  		return PodAdmitResult{Admit: true}
   248  	}
   249  	return PodAdmitResult{
   250  		Admit:   false,
   251  		Reason:  "AppArmor",
   252  		Message: fmt.Sprintf("Cannot enforce AppArmor: %v", err),
   253  	}
   254  }
   255  
   256  func isHTTPResponseError(err error) bool {
   257  	if err == nil {
   258  		return false
   259  	}
   260  	urlErr := &url.Error{}
   261  	if !errors.As(err, &urlErr) {
   262  		return false
   263  	}
   264  	return strings.Contains(urlErr.Err.Error(), "server gave HTTP response to HTTPS client")
   265  }