...

Source file src/k8s.io/kubectl/pkg/scale/scale.go

Documentation: k8s.io/kubectl/pkg/scale

     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 scale
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strconv"
    23  	"time"
    24  
    25  	autoscalingv1 "k8s.io/api/autoscaling/v1"
    26  	"k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	"k8s.io/apimachinery/pkg/util/json"
    31  	"k8s.io/apimachinery/pkg/util/wait"
    32  	scaleclient "k8s.io/client-go/scale"
    33  )
    34  
    35  // Scaler provides an interface for resources that can be scaled.
    36  type Scaler interface {
    37  	// Scale scales the named resource after checking preconditions. It optionally
    38  	// retries in the event of resource version mismatch (if retry is not nil),
    39  	// and optionally waits until the status of the resource matches newSize (if wait is not nil)
    40  	// TODO: Make the implementation of this watch-based (#56075) once #31345 is fixed.
    41  	Scale(namespace, name string, newSize uint, preconditions *ScalePrecondition, retry, wait *RetryParams, gvr schema.GroupVersionResource, dryRun bool) error
    42  	// ScaleSimple does a simple one-shot attempt at scaling - not useful on its own, but
    43  	// a necessary building block for Scale
    44  	ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, err error)
    45  }
    46  
    47  // NewScaler get a scaler for a given resource
    48  func NewScaler(scalesGetter scaleclient.ScalesGetter) Scaler {
    49  	return &genericScaler{scalesGetter}
    50  }
    51  
    52  // ScalePrecondition describes a condition that must be true for the scale to take place
    53  // If CurrentSize == -1, it is ignored.
    54  // If CurrentResourceVersion is the empty string, it is ignored.
    55  // Otherwise they must equal the values in the resource for it to be valid.
    56  type ScalePrecondition struct {
    57  	Size            int
    58  	ResourceVersion string
    59  }
    60  
    61  // A PreconditionError is returned when a resource fails to match
    62  // the scale preconditions passed to kubectl.
    63  type PreconditionError struct {
    64  	Precondition  string
    65  	ExpectedValue string
    66  	ActualValue   string
    67  }
    68  
    69  func (pe PreconditionError) Error() string {
    70  	return fmt.Sprintf("Expected %s to be %s, was %s", pe.Precondition, pe.ExpectedValue, pe.ActualValue)
    71  }
    72  
    73  // RetryParams encapsulates the retry parameters used by kubectl's scaler.
    74  type RetryParams struct {
    75  	Interval, Timeout time.Duration
    76  }
    77  
    78  func NewRetryParams(interval, timeout time.Duration) *RetryParams {
    79  	return &RetryParams{interval, timeout}
    80  }
    81  
    82  // ScaleCondition is a closure around Scale that facilitates retries via util.wait
    83  func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name string, count uint, updatedResourceVersion *string, gvr schema.GroupVersionResource, dryRun bool) wait.ConditionFunc {
    84  	return func() (bool, error) {
    85  		rv, err := r.ScaleSimple(namespace, name, precondition, count, gvr, dryRun)
    86  		if updatedResourceVersion != nil {
    87  			*updatedResourceVersion = rv
    88  		}
    89  		// Retry only on update conflicts.
    90  		if errors.IsConflict(err) {
    91  			return false, nil
    92  		}
    93  		if err != nil {
    94  			return false, err
    95  		}
    96  		return true, nil
    97  	}
    98  }
    99  
   100  // validateGeneric ensures that the preconditions match. Returns nil if they are valid, otherwise an error
   101  func (precondition *ScalePrecondition) validate(scale *autoscalingv1.Scale) error {
   102  	if precondition.Size != -1 && int(scale.Spec.Replicas) != precondition.Size {
   103  		return PreconditionError{"replicas", strconv.Itoa(precondition.Size), strconv.Itoa(int(scale.Spec.Replicas))}
   104  	}
   105  	if len(precondition.ResourceVersion) > 0 && scale.ResourceVersion != precondition.ResourceVersion {
   106  		return PreconditionError{"resource version", precondition.ResourceVersion, scale.ResourceVersion}
   107  	}
   108  	return nil
   109  }
   110  
   111  // genericScaler can update scales for resources in a particular namespace
   112  type genericScaler struct {
   113  	scaleNamespacer scaleclient.ScalesGetter
   114  }
   115  
   116  var _ Scaler = &genericScaler{}
   117  
   118  // ScaleSimple updates a scale of a given resource. It returns the resourceVersion of the scale if the update was successful.
   119  func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, err error) {
   120  	if preconditions != nil {
   121  		scale, err := s.scaleNamespacer.Scales(namespace).Get(context.TODO(), gvr.GroupResource(), name, metav1.GetOptions{})
   122  		if err != nil {
   123  			return "", err
   124  		}
   125  		if err = preconditions.validate(scale); err != nil {
   126  			return "", err
   127  		}
   128  		scale.Spec.Replicas = int32(newSize)
   129  		updateOptions := metav1.UpdateOptions{}
   130  		if dryRun {
   131  			updateOptions.DryRun = []string{metav1.DryRunAll}
   132  		}
   133  		updatedScale, err := s.scaleNamespacer.Scales(namespace).Update(context.TODO(), gvr.GroupResource(), scale, updateOptions)
   134  		if err != nil {
   135  			return "", err
   136  		}
   137  		return updatedScale.ResourceVersion, nil
   138  	}
   139  
   140  	// objectForReplicas is used for encoding scale patch
   141  	type objectForReplicas struct {
   142  		Replicas uint `json:"replicas"`
   143  	}
   144  	// objectForSpec is used for encoding scale patch
   145  	type objectForSpec struct {
   146  		Spec objectForReplicas `json:"spec"`
   147  	}
   148  	spec := objectForSpec{
   149  		Spec: objectForReplicas{Replicas: newSize},
   150  	}
   151  	patch, err := json.Marshal(&spec)
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	patchOptions := metav1.PatchOptions{}
   156  	if dryRun {
   157  		patchOptions.DryRun = []string{metav1.DryRunAll}
   158  	}
   159  	updatedScale, err := s.scaleNamespacer.Scales(namespace).Patch(context.TODO(), gvr, name, types.MergePatchType, patch, patchOptions)
   160  	if err != nil {
   161  		return "", err
   162  	}
   163  	return updatedScale.ResourceVersion, nil
   164  }
   165  
   166  // Scale updates a scale of a given resource to a new size, with optional precondition check (if preconditions is not nil),
   167  // optional retries (if retry is not nil), and then optionally waits for the status to reach desired count.
   168  func (s *genericScaler) Scale(namespace, resourceName string, newSize uint, preconditions *ScalePrecondition, retry, waitForReplicas *RetryParams, gvr schema.GroupVersionResource, dryRun bool) error {
   169  	if retry == nil {
   170  		// make it try only once, immediately
   171  		retry = &RetryParams{Interval: time.Millisecond, Timeout: time.Millisecond}
   172  	}
   173  	cond := ScaleCondition(s, preconditions, namespace, resourceName, newSize, nil, gvr, dryRun)
   174  	if err := wait.PollImmediate(retry.Interval, retry.Timeout, cond); err != nil {
   175  		return err
   176  	}
   177  	if waitForReplicas != nil {
   178  		return WaitForScaleHasDesiredReplicas(s.scaleNamespacer, gvr.GroupResource(), resourceName, namespace, newSize, waitForReplicas)
   179  	}
   180  	return nil
   181  }
   182  
   183  // scaleHasDesiredReplicas returns a condition that will be true if and only if the desired replica
   184  // count for a scale (Spec) equals its updated replicas count (Status)
   185  func scaleHasDesiredReplicas(sClient scaleclient.ScalesGetter, gr schema.GroupResource, resourceName string, namespace string, desiredReplicas int32) wait.ConditionFunc {
   186  	return func() (bool, error) {
   187  		actualScale, err := sClient.Scales(namespace).Get(context.TODO(), gr, resourceName, metav1.GetOptions{})
   188  		if err != nil {
   189  			return false, err
   190  		}
   191  		// this means the desired scale target has been reset by something else
   192  		if actualScale.Spec.Replicas != desiredReplicas {
   193  			return true, nil
   194  		}
   195  		return actualScale.Spec.Replicas == actualScale.Status.Replicas &&
   196  			desiredReplicas == actualScale.Status.Replicas, nil
   197  	}
   198  }
   199  
   200  // WaitForScaleHasDesiredReplicas waits until condition scaleHasDesiredReplicas is satisfied
   201  // or returns error when timeout happens
   202  func WaitForScaleHasDesiredReplicas(sClient scaleclient.ScalesGetter, gr schema.GroupResource, resourceName string, namespace string, newSize uint, waitForReplicas *RetryParams) error {
   203  	if waitForReplicas == nil {
   204  		return fmt.Errorf("waitForReplicas parameter cannot be nil")
   205  	}
   206  	err := wait.PollImmediate(
   207  		waitForReplicas.Interval,
   208  		waitForReplicas.Timeout,
   209  		scaleHasDesiredReplicas(sClient, gr, resourceName, namespace, int32(newSize)))
   210  	if err == wait.ErrWaitTimeout {
   211  		return fmt.Errorf("timed out waiting for %q to be synced", resourceName)
   212  	}
   213  	return err
   214  }
   215  

View as plain text