github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controllerutil/pod_utils.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package controllerutil
    21  
    22  import (
    23  	"fmt"
    24  	"regexp"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	appsv1 "k8s.io/api/apps/v1"
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	metautil "k8s.io/apimachinery/pkg/util/intstr"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  
    35  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    36  	"github.com/1aal/kubeblocks/pkg/constant"
    37  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    38  )
    39  
    40  // statefulPodRegex is a regular expression that extracts the parent StatefulSet and ordinal from the Name of a Pod
    41  var statefulPodRegex = regexp.MustCompile("(.*)-([0-9]+)$")
    42  
    43  // GetParentNameAndOrdinal gets the name of pod's parent StatefulSet and pod's ordinal as extracted from its Name. If
    44  // the Pod was not created by a StatefulSet, its parent is considered to be empty string, and its ordinal is considered
    45  // to be -1.
    46  func GetParentNameAndOrdinal(pod *corev1.Pod) (string, int) {
    47  	parent := ""
    48  	ordinal := -1
    49  	subMatches := statefulPodRegex.FindStringSubmatch(pod.Name)
    50  	if len(subMatches) < 3 {
    51  		return parent, ordinal
    52  	}
    53  	parent = subMatches[1]
    54  	if i, err := strconv.ParseInt(subMatches[2], 10, 32); err == nil {
    55  		ordinal = int(i)
    56  	}
    57  	return parent, ordinal
    58  }
    59  
    60  // GetContainerByConfigSpec searches for container using the configmap of config from the pod
    61  //
    62  //	e.g.:
    63  //	ClusterDefinition.configTemplateRef:
    64  //		 - Name: "mysql-8.0"
    65  //		   VolumeName: "mysql_config"
    66  //
    67  //
    68  //	PodTemplate.containers[*].volumeMounts:
    69  //		 - mountPath: /data/config
    70  //		   name: mysql_config
    71  //		 - mountPath: /data
    72  //		   name: data
    73  //		 - mountPath: /log
    74  //		   name: log
    75  func GetContainerByConfigSpec(podSpec *corev1.PodSpec, configs []appsv1alpha1.ComponentConfigSpec) *corev1.Container {
    76  	containers := podSpec.Containers
    77  	initContainers := podSpec.InitContainers
    78  	if container := getContainerWithTplList(containers, configs); container != nil {
    79  		return container
    80  	}
    81  	if container := getContainerWithTplList(initContainers, configs); container != nil {
    82  		return container
    83  	}
    84  	return nil
    85  }
    86  
    87  // GetPodContainerWithVolumeMount searches for containers mounting the volume
    88  func GetPodContainerWithVolumeMount(podSpec *corev1.PodSpec, volumeName string) []*corev1.Container {
    89  	containers := podSpec.Containers
    90  	if len(containers) == 0 || volumeName == "" {
    91  		return nil
    92  	}
    93  	return getContainerWithVolumeMount(containers, volumeName)
    94  }
    95  
    96  // GetVolumeMountName finds the volume with mount name
    97  func GetVolumeMountName(volumes []corev1.Volume, resourceName string) *corev1.Volume {
    98  	for i := range volumes {
    99  		if volumes[i].ConfigMap != nil && volumes[i].ConfigMap.Name == resourceName {
   100  			return &volumes[i]
   101  		}
   102  		if volumes[i].Projected == nil {
   103  			continue
   104  		}
   105  		for j := range volumes[i].Projected.Sources {
   106  			if volumes[i].Projected.Sources[j].ConfigMap != nil && volumes[i].Projected.Sources[j].ConfigMap.Name == resourceName {
   107  				return &volumes[i]
   108  			}
   109  		}
   110  	}
   111  	return nil
   112  }
   113  
   114  type containerNameFilter func(containerName string) bool
   115  
   116  func GetContainersByConfigmap(containers []corev1.Container, volumeName string, cmName string, filters ...containerNameFilter) []string {
   117  	containerFilter := func(c corev1.Container) bool {
   118  		for _, f := range filters {
   119  			if (len(c.VolumeMounts) == 0 && len(c.EnvFrom) == 0) ||
   120  				f(c.Name) {
   121  				return true
   122  			}
   123  		}
   124  		return false
   125  	}
   126  
   127  	tmpList := make([]string, 0, len(containers))
   128  	for _, c := range containers {
   129  		if containerFilter(c) {
   130  			continue
   131  		}
   132  		for _, vm := range c.VolumeMounts {
   133  			if vm.Name == volumeName {
   134  				tmpList = append(tmpList, c.Name)
   135  				goto breakHere
   136  			}
   137  		}
   138  		if cmName == "" {
   139  			continue
   140  		}
   141  		for _, source := range c.EnvFrom {
   142  			if source.ConfigMapRef != nil && source.ConfigMapRef.Name == cmName {
   143  				tmpList = append(tmpList, c.Name)
   144  				break
   145  			}
   146  		}
   147  	breakHere:
   148  	}
   149  	return tmpList
   150  }
   151  
   152  func getContainerWithTplList(containers []corev1.Container, configs []appsv1alpha1.ComponentConfigSpec) *corev1.Container {
   153  	if len(containers) == 0 {
   154  		return nil
   155  	}
   156  	for i, c := range containers {
   157  		volumeMounts := c.VolumeMounts
   158  		if len(volumeMounts) > 0 && checkContainerWithVolumeMount(volumeMounts, configs) {
   159  			return &containers[i]
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  func checkContainerWithVolumeMount(volumeMounts []corev1.VolumeMount, configs []appsv1alpha1.ComponentConfigSpec) bool {
   166  	volumes := make(map[string]int)
   167  	for _, c := range configs {
   168  		for j, vm := range volumeMounts {
   169  			if vm.Name == c.VolumeName {
   170  				volumes[vm.Name] = j
   171  				break
   172  			}
   173  		}
   174  	}
   175  	return len(configs) == len(volumes)
   176  }
   177  
   178  func getContainerWithVolumeMount(containers []corev1.Container, volumeName string) []*corev1.Container {
   179  	mountContainers := make([]*corev1.Container, 0, len(containers))
   180  	for i, c := range containers {
   181  		volumeMounts := c.VolumeMounts
   182  		for _, vm := range volumeMounts {
   183  			if vm.Name == volumeName {
   184  				mountContainers = append(mountContainers, &containers[i])
   185  				break
   186  			}
   187  		}
   188  	}
   189  	return mountContainers
   190  }
   191  
   192  func GetVolumeMountByVolume(container *corev1.Container, volumeName string) *corev1.VolumeMount {
   193  	for _, volume := range container.VolumeMounts {
   194  		if volume.Name == volumeName {
   195  			return &volume
   196  		}
   197  	}
   198  
   199  	return nil
   200  }
   201  
   202  // GetCoreNum gets content of Resources.Limits.cpu
   203  func GetCoreNum(container corev1.Container) int64 {
   204  	limits := container.Resources.Limits
   205  	if val, ok := (limits)[corev1.ResourceCPU]; ok {
   206  		return val.Value()
   207  	}
   208  	return 0
   209  }
   210  
   211  // GetMemorySize gets content of Resources.Limits.memory
   212  func GetMemorySize(container corev1.Container) int64 {
   213  	limits := container.Resources.Limits
   214  	if val, ok := (limits)[corev1.ResourceMemory]; ok {
   215  		return val.Value()
   216  	}
   217  	return 0
   218  }
   219  
   220  // GetRequestMemorySize gets content of Resources.Limits.memory
   221  func GetRequestMemorySize(container corev1.Container) int64 {
   222  	requests := container.Resources.Requests
   223  	if val, ok := (requests)[corev1.ResourceMemory]; ok {
   224  		return val.Value()
   225  	}
   226  	return 0
   227  }
   228  
   229  // GetStorageSizeFromPersistentVolume gets content of Resources.Requests.storage
   230  func GetStorageSizeFromPersistentVolume(pvc corev1.PersistentVolumeClaimTemplate) int64 {
   231  	requests := pvc.Spec.Resources.Requests
   232  	if val, ok := (requests)[corev1.ResourceStorage]; ok {
   233  		return val.Value()
   234  	}
   235  	return -1
   236  }
   237  
   238  // PodIsReady checks if pod is ready
   239  func PodIsReady(pod *corev1.Pod) bool {
   240  	if pod.Status.Conditions == nil {
   241  		return false
   242  	}
   243  
   244  	if pod.DeletionTimestamp != nil {
   245  		return false
   246  	}
   247  
   248  	for _, condition := range pod.Status.Conditions {
   249  		if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue {
   250  			return true
   251  		}
   252  	}
   253  	return false
   254  }
   255  
   256  // GetContainerID gets the containerID from pod by name
   257  func GetContainerID(pod *corev1.Pod, containerName string) string {
   258  	const containerSep = "//"
   259  
   260  	// container id is present in the form of <runtime>://<container-id>
   261  	// e.g: containerID: docker://27d1586d53ef9a6af5bd983831d13b6a38128119fadcdc22894d7b2397758eb5
   262  	for _, container := range pod.Status.ContainerStatuses {
   263  		if container.Name == containerName {
   264  			return strings.Split(container.ContainerID, containerSep)[1]
   265  		}
   266  	}
   267  	return ""
   268  }
   269  
   270  func isRunning(pod *corev1.Pod) bool {
   271  	return pod.Status.Phase == corev1.PodRunning && pod.DeletionTimestamp == nil
   272  }
   273  
   274  func IsAvailable(pod *corev1.Pod, minReadySeconds int32) bool {
   275  	if !isRunning(pod) {
   276  		return false
   277  	}
   278  
   279  	condition := GetPodCondition(&pod.Status, corev1.PodReady)
   280  	if condition == nil || condition.Status != corev1.ConditionTrue {
   281  		return false
   282  	}
   283  	if minReadySeconds == 0 {
   284  		return true
   285  	}
   286  
   287  	var (
   288  		now                = metav1.Now()
   289  		minDuration        = time.Duration(minReadySeconds) * time.Second
   290  		lastTransitionTime = condition.LastTransitionTime
   291  	)
   292  
   293  	return !lastTransitionTime.IsZero() && lastTransitionTime.Add(minDuration).Before(now.Time)
   294  }
   295  
   296  func GetPodCondition(status *corev1.PodStatus, conditionType corev1.PodConditionType) *corev1.PodCondition {
   297  	if len(status.Conditions) == 0 {
   298  		return nil
   299  	}
   300  
   301  	for i, condition := range status.Conditions {
   302  		if condition.Type == conditionType {
   303  			return &status.Conditions[i]
   304  		}
   305  	}
   306  	return nil
   307  }
   308  
   309  func IsMatchConfigVersion(obj client.Object, labelKey string, version string) bool {
   310  	labels := obj.GetLabels()
   311  	if len(labels) == 0 {
   312  		return false
   313  	}
   314  	if lastVersion, ok := labels[labelKey]; ok && lastVersion == version {
   315  		return true
   316  	}
   317  	return false
   318  }
   319  
   320  func GetIntOrPercentValue(intOrStr *metautil.IntOrString) (int, bool, error) {
   321  	if intOrStr.Type == metautil.Int {
   322  		return intOrStr.IntValue(), false, nil
   323  	}
   324  
   325  	// parse string
   326  	s := intOrStr.StrVal
   327  	if strings.HasSuffix(s, "%") {
   328  		s = strings.TrimSuffix(intOrStr.StrVal, "%")
   329  	} else {
   330  		return 0, false, fmt.Errorf("failed to parse percentage. [%s]", intOrStr.StrVal)
   331  	}
   332  	v, err := strconv.Atoi(s)
   333  	if err != nil {
   334  		return 0, false, fmt.Errorf("failed to atoi [%s], error: %v", intOrStr.StrVal, err)
   335  	}
   336  	return v, true, nil
   337  }
   338  
   339  // GetPortByPortName gets the Port from pod by name
   340  func GetPortByPortName(pod *corev1.Pod, portName string) (int32, error) {
   341  	for _, container := range pod.Spec.Containers {
   342  		for _, port := range container.Ports {
   343  			if port.Name == portName {
   344  				return port.ContainerPort, nil
   345  			}
   346  		}
   347  	}
   348  	return 0, fmt.Errorf("port %s not found", portName)
   349  }
   350  
   351  func GetLorryGRPCPort(pod *corev1.Pod) (int32, error) {
   352  	return GetPortByPortName(pod, constant.LorryGRPCPortName)
   353  }
   354  
   355  func GetLorryHTTPPort(pod *corev1.Pod) (int32, error) {
   356  	return GetPortByPortName(pod, constant.LorryHTTPPortName)
   357  }
   358  
   359  // GuessLorryHTTPPort guesses lorry container and serving port.
   360  // TODO(xuriwuyun): should provide a deterministic way to find the lorry serving port.
   361  func GuessLorryHTTPPort(pod *corev1.Pod) (int32, error) {
   362  	lorryImage := viper.GetString(constant.KBToolsImage)
   363  	for _, container := range pod.Spec.Containers {
   364  		if container.Image != lorryImage {
   365  			continue
   366  		}
   367  		if len(container.Ports) > 0 {
   368  			return container.Ports[0].ContainerPort, nil
   369  		}
   370  	}
   371  	return 0, fmt.Errorf("lorry port not found")
   372  }
   373  
   374  // GetLorryContainerName gets the probe container from pod
   375  func GetLorryContainerName(pod *corev1.Pod) (string, error) {
   376  	for _, container := range pod.Spec.Containers {
   377  		if len(container.Command) > 0 && strings.Contains(container.Command[0], "lorry") {
   378  			return container.Name, nil
   379  		}
   380  	}
   381  	return "", fmt.Errorf("lorry container not found")
   382  }
   383  
   384  // PodIsReadyWithLabel checks if pod is ready for ConsensusSet/ReplicationSet component,
   385  // it will be available when the pod is ready and labeled with role.
   386  func PodIsReadyWithLabel(pod corev1.Pod) bool {
   387  	if _, ok := pod.Labels[constant.RoleLabelKey]; !ok {
   388  		return false
   389  	}
   390  
   391  	return PodIsReady(&pod)
   392  }
   393  
   394  // PodIsControlledByLatestRevision checks if the pod is controlled by latest controller revision.
   395  func PodIsControlledByLatestRevision(pod *corev1.Pod, sts *appsv1.StatefulSet) bool {
   396  	return GetPodRevision(pod) == sts.Status.UpdateRevision && sts.Status.ObservedGeneration == sts.Generation
   397  }
   398  
   399  // GetPodRevision gets the revision of Pod by inspecting the StatefulSetRevisionLabel. If pod has no revision empty
   400  // string is returned.
   401  func GetPodRevision(pod *corev1.Pod) string {
   402  	if pod.Labels == nil {
   403  		return ""
   404  	}
   405  	return pod.Labels[appsv1.StatefulSetRevisionLabel]
   406  }
   407  
   408  // ByPodName sorts a list of jobs by pod name
   409  type ByPodName []corev1.Pod
   410  
   411  // Len returns the length of byPodName for sort.Sort
   412  func (c ByPodName) Len() int {
   413  	return len(c)
   414  }
   415  
   416  // Swap swaps the items for sort.Sort
   417  func (c ByPodName) Swap(i, j int) {
   418  	c[i], c[j] = c[j], c[i]
   419  }
   420  
   421  // Less defines compare method for sort.Sort
   422  func (c ByPodName) Less(i, j int) bool {
   423  	return c[i].Name < c[j].Name
   424  }
   425  
   426  // BuildPodHostDNS builds the host dns of pod.
   427  // ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
   428  func BuildPodHostDNS(pod *corev1.Pod) string {
   429  	if pod == nil {
   430  		return ""
   431  	}
   432  	// build pod dns string
   433  	// ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
   434  	if pod.Spec.Subdomain != "" {
   435  		hostDNS := []string{pod.Name}
   436  		if pod.Spec.Hostname != "" {
   437  			hostDNS[0] = pod.Spec.Hostname
   438  		}
   439  		hostDNS = append(hostDNS, pod.Spec.Subdomain)
   440  		return strings.Join(hostDNS, ".")
   441  	}
   442  	return pod.Status.PodIP
   443  }