mirror of https://github.com/k3s-io/k3s
469 lines
16 KiB
Go
469 lines
16 KiB
Go
/*
|
|
Copyright 2014 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package rollingupdate
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"k8s.io/klog"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
|
"k8s.io/cli-runtime/pkg/printers"
|
|
"k8s.io/cli-runtime/pkg/resource"
|
|
"k8s.io/client-go/kubernetes"
|
|
scaleclient "k8s.io/client-go/scale"
|
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
|
"k8s.io/kubectl/pkg/scheme"
|
|
"k8s.io/kubectl/pkg/util"
|
|
"k8s.io/kubectl/pkg/util/i18n"
|
|
"k8s.io/kubectl/pkg/util/templates"
|
|
"k8s.io/kubectl/pkg/validation"
|
|
)
|
|
|
|
var (
|
|
rollingUpdateLong = templates.LongDesc(i18n.T(`
|
|
Perform a rolling update of the given ReplicationController.
|
|
|
|
Replaces the specified replication controller with a new replication controller by updating one pod at a time to use the
|
|
new PodTemplate. The new-controller.json must specify the same namespace as the
|
|
existing replication controller and overwrite at least one (common) label in its replicaSelector.
|
|
|
|
![Workflow](http://kubernetes.io/images/docs/kubectl_rollingupdate.svg)`))
|
|
|
|
rollingUpdateExample = templates.Examples(i18n.T(`
|
|
# Update pods of frontend-v1 using new replication controller data in frontend-v2.json.
|
|
kubectl rolling-update frontend-v1 -f frontend-v2.json
|
|
|
|
# Update pods of frontend-v1 using JSON data passed into stdin.
|
|
cat frontend-v2.json | kubectl rolling-update frontend-v1 -f -
|
|
|
|
# Update the pods of frontend-v1 to frontend-v2 by just changing the image, and switching the
|
|
# name of the replication controller.
|
|
kubectl rolling-update frontend-v1 frontend-v2 --image=image:v2
|
|
|
|
# Update the pods of frontend by just changing the image, and keeping the old name.
|
|
kubectl rolling-update frontend --image=image:v2
|
|
|
|
# Abort and reverse an existing rollout in progress (from frontend-v1 to frontend-v2).
|
|
kubectl rolling-update frontend-v1 frontend-v2 --rollback`))
|
|
)
|
|
|
|
const (
|
|
updatePeriod = 1 * time.Minute
|
|
timeout = 5 * time.Minute
|
|
pollInterval = 3 * time.Second
|
|
)
|
|
|
|
type RollingUpdateOptions struct {
|
|
FilenameOptions *resource.FilenameOptions
|
|
|
|
OldName string
|
|
KeepOldName bool
|
|
|
|
DeploymentKey string
|
|
Image string
|
|
Container string
|
|
PullPolicy string
|
|
Rollback bool
|
|
Period time.Duration
|
|
Timeout time.Duration
|
|
Interval time.Duration
|
|
DryRun bool
|
|
OutputFormat string
|
|
Namespace string
|
|
EnforceNamespace bool
|
|
|
|
ScaleClient scaleclient.ScalesGetter
|
|
ClientSet kubernetes.Interface
|
|
Builder *resource.Builder
|
|
|
|
ShouldValidate bool
|
|
Validator func(bool) (validation.Schema, error)
|
|
|
|
FindNewName func(*corev1.ReplicationController) string
|
|
|
|
PrintFlags *genericclioptions.PrintFlags
|
|
ToPrinter func(string) (printers.ResourcePrinter, error)
|
|
|
|
genericclioptions.IOStreams
|
|
}
|
|
|
|
func NewRollingUpdateOptions(streams genericclioptions.IOStreams) *RollingUpdateOptions {
|
|
return &RollingUpdateOptions{
|
|
PrintFlags: genericclioptions.NewPrintFlags("rolling updated").WithTypeSetter(scheme.Scheme),
|
|
FilenameOptions: &resource.FilenameOptions{},
|
|
DeploymentKey: "deployment",
|
|
Timeout: timeout,
|
|
Interval: pollInterval,
|
|
Period: updatePeriod,
|
|
|
|
IOStreams: streams,
|
|
}
|
|
}
|
|
|
|
func NewCmdRollingUpdate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
|
o := NewRollingUpdateOptions(ioStreams)
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "rolling-update OLD_CONTROLLER_NAME ([NEW_CONTROLLER_NAME] --image=NEW_CONTAINER_IMAGE | -f NEW_CONTROLLER_SPEC)",
|
|
DisableFlagsInUseLine: true,
|
|
Short: "Perform a rolling update. This command is deprecated, use rollout instead.",
|
|
Long: rollingUpdateLong,
|
|
Example: rollingUpdateExample,
|
|
Deprecated: `use "rollout" instead`,
|
|
Hidden: true,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
|
cmdutil.CheckErr(o.Validate(cmd, args))
|
|
cmdutil.CheckErr(o.Run())
|
|
},
|
|
}
|
|
|
|
o.PrintFlags.AddFlags(cmd)
|
|
|
|
cmd.Flags().DurationVar(&o.Period, "update-period", o.Period, `Time to wait between updating pods. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`)
|
|
cmd.Flags().DurationVar(&o.Interval, "poll-interval", o.Interval, `Time delay between polling for replication controller status after the update. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`)
|
|
cmd.Flags().DurationVar(&o.Timeout, "timeout", o.Timeout, `Max time to wait for a replication controller to update before giving up. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`)
|
|
usage := "Filename or URL to file to use to create the new replication controller."
|
|
cmdutil.AddJsonFilenameFlag(cmd.Flags(), &o.FilenameOptions.Filenames, usage)
|
|
cmd.Flags().StringVar(&o.Image, "image", o.Image, i18n.T("Image to use for upgrading the replication controller. Must be distinct from the existing image (either new image or new image tag). Can not be used with --filename/-f"))
|
|
cmd.Flags().StringVar(&o.DeploymentKey, "deployment-label-key", o.DeploymentKey, i18n.T("The key to use to differentiate between two different controllers, default 'deployment'. Only relevant when --image is specified, ignored otherwise"))
|
|
cmd.Flags().StringVar(&o.Container, "container", o.Container, i18n.T("Container name which will have its image upgraded. Only relevant when --image is specified, ignored otherwise. Required when using --image on a multi-container pod"))
|
|
cmd.Flags().StringVar(&o.PullPolicy, "image-pull-policy", o.PullPolicy, i18n.T("Explicit policy for when to pull container images. Required when --image is same as existing image, ignored otherwise."))
|
|
cmd.Flags().BoolVar(&o.Rollback, "rollback", o.Rollback, "If true, this is a request to abort an existing rollout that is partially rolled out. It effectively reverses current and next and runs a rollout")
|
|
cmdutil.AddDryRunFlag(cmd)
|
|
cmdutil.AddValidateFlags(cmd)
|
|
|
|
return cmd
|
|
}
|
|
|
|
func validateArguments(cmd *cobra.Command, filenames, args []string) error {
|
|
deploymentKey := cmdutil.GetFlagString(cmd, "deployment-label-key")
|
|
image := cmdutil.GetFlagString(cmd, "image")
|
|
rollback := cmdutil.GetFlagBool(cmd, "rollback")
|
|
|
|
errors := []error{}
|
|
if len(deploymentKey) == 0 {
|
|
errors = append(errors, cmdutil.UsageErrorf(cmd, "--deployment-label-key can not be empty"))
|
|
}
|
|
if len(filenames) > 1 {
|
|
errors = append(errors, cmdutil.UsageErrorf(cmd, "May only specify a single filename for new controller"))
|
|
}
|
|
|
|
if !rollback {
|
|
if len(filenames) == 0 && len(image) == 0 {
|
|
errors = append(errors, cmdutil.UsageErrorf(cmd, "Must specify --filename or --image for new controller"))
|
|
} else if len(filenames) != 0 && len(image) != 0 {
|
|
errors = append(errors, cmdutil.UsageErrorf(cmd, "--filename and --image can not both be specified"))
|
|
}
|
|
} else {
|
|
if len(filenames) != 0 || len(image) != 0 {
|
|
errors = append(errors, cmdutil.UsageErrorf(cmd, "Don't specify --filename or --image on rollback"))
|
|
}
|
|
}
|
|
|
|
if len(args) < 1 {
|
|
errors = append(errors, cmdutil.UsageErrorf(cmd, "Must specify the controller to update"))
|
|
}
|
|
|
|
return utilerrors.NewAggregate(errors)
|
|
}
|
|
|
|
func (o *RollingUpdateOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
|
if len(args) > 0 {
|
|
o.OldName = args[0]
|
|
}
|
|
o.DryRun = cmdutil.GetDryRunFlag(cmd)
|
|
o.OutputFormat = cmdutil.GetFlagString(cmd, "output")
|
|
o.KeepOldName = len(args) == 1
|
|
o.ShouldValidate = cmdutil.GetFlagBool(cmd, "validate")
|
|
|
|
o.Validator = f.Validator
|
|
o.FindNewName = func(obj *corev1.ReplicationController) string {
|
|
return findNewName(args, obj)
|
|
}
|
|
|
|
var err error
|
|
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
o.ScaleClient, err = cmdutil.ScaleClientFn(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
o.ClientSet, err = f.KubernetesClientSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
o.Builder = f.NewBuilder()
|
|
|
|
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
|
o.PrintFlags.NamePrintFlags.Operation = operation
|
|
if o.DryRun {
|
|
o.PrintFlags.Complete("%s (dry run)")
|
|
}
|
|
|
|
return o.PrintFlags.ToPrinter()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *RollingUpdateOptions) Validate(cmd *cobra.Command, args []string) error {
|
|
return validateArguments(cmd, o.FilenameOptions.Filenames, args)
|
|
}
|
|
|
|
func (o *RollingUpdateOptions) Run() error {
|
|
filename := ""
|
|
if len(o.FilenameOptions.Filenames) > 0 {
|
|
filename = o.FilenameOptions.Filenames[0]
|
|
}
|
|
|
|
coreClient := o.ClientSet.CoreV1()
|
|
|
|
var newRc *corev1.ReplicationController
|
|
// fetch rc
|
|
oldRc, err := coreClient.ReplicationControllers(o.Namespace).Get(o.OldName, metav1.GetOptions{})
|
|
if err != nil {
|
|
if !errors.IsNotFound(err) || len(o.Image) == 0 || !o.KeepOldName {
|
|
return err
|
|
}
|
|
// We're in the middle of a rename, look for an RC with a source annotation of oldName
|
|
newRc, err := FindSourceController(coreClient, o.Namespace, o.OldName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return Rename(coreClient, newRc, o.OldName)
|
|
}
|
|
|
|
var replicasDefaulted bool
|
|
|
|
if len(filename) != 0 {
|
|
schema, err := o.Validator(o.ShouldValidate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
request := o.Builder.
|
|
Unstructured().
|
|
Schema(schema).
|
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
|
FilenameParam(o.EnforceNamespace, &resource.FilenameOptions{Recursive: false, Filenames: []string{filename}}).
|
|
Flatten().
|
|
Do()
|
|
infos, err := request.Infos()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Handle filename input from stdin.
|
|
if len(infos) > 1 {
|
|
return fmt.Errorf("%s specifies multiple items", filename)
|
|
}
|
|
if len(infos) == 0 {
|
|
return fmt.Errorf("please make sure %s exists and is not empty", filename)
|
|
}
|
|
|
|
uncastVersionedObj, err := scheme.Scheme.ConvertToVersion(infos[0].Object, corev1.SchemeGroupVersion)
|
|
if err != nil {
|
|
klog.V(4).Infof("Object %T is not a ReplicationController", infos[0].Object)
|
|
return fmt.Errorf("%s contains a %v not a ReplicationController", filename, infos[0].Object.GetObjectKind().GroupVersionKind())
|
|
}
|
|
switch t := uncastVersionedObj.(type) {
|
|
case *corev1.ReplicationController:
|
|
replicasDefaulted = t.Spec.Replicas == nil
|
|
newRc = t
|
|
}
|
|
if newRc == nil {
|
|
klog.V(4).Infof("Object %T is not a ReplicationController", infos[0].Object)
|
|
return fmt.Errorf("%s contains a %v not a ReplicationController", filename, infos[0].Object.GetObjectKind().GroupVersionKind())
|
|
}
|
|
}
|
|
|
|
// If the --image option is specified, we need to create a new rc with at least one different selector
|
|
// than the old rc. This selector is the hash of the rc, with a suffix to provide uniqueness for
|
|
// same-image updates.
|
|
if len(o.Image) != 0 {
|
|
codec := scheme.Codecs.LegacyCodec(corev1.SchemeGroupVersion)
|
|
newName := o.FindNewName(oldRc)
|
|
if newRc, err = LoadExistingNextReplicationController(coreClient, o.Namespace, newName); err != nil {
|
|
return err
|
|
}
|
|
if newRc != nil {
|
|
if inProgressImage := newRc.Spec.Template.Spec.Containers[0].Image; inProgressImage != o.Image {
|
|
return fmt.Errorf("Found existing in-progress update to image (%s).\nEither continue in-progress update with --image=%s or rollback with --rollback", inProgressImage, inProgressImage)
|
|
}
|
|
fmt.Fprintf(o.Out, "Found existing update in progress (%s), resuming.\n", newRc.Name)
|
|
} else {
|
|
config := &NewControllerConfig{
|
|
Namespace: o.Namespace,
|
|
OldName: o.OldName,
|
|
NewName: newName,
|
|
Image: o.Image,
|
|
Container: o.Container,
|
|
DeploymentKey: o.DeploymentKey,
|
|
}
|
|
if oldRc.Spec.Template.Spec.Containers[0].Image == o.Image {
|
|
if len(o.PullPolicy) == 0 {
|
|
return fmt.Errorf("--image-pull-policy (Always|Never|IfNotPresent) must be provided when --image is the same as existing container image")
|
|
}
|
|
config.PullPolicy = corev1.PullPolicy(o.PullPolicy)
|
|
}
|
|
newRc, err = CreateNewControllerFromCurrentController(coreClient, codec, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Update the existing replication controller with pointers to the 'next' controller
|
|
// and adding the <deploymentKey> label if necessary to distinguish it from the 'next' controller.
|
|
oldHash, err := util.HashObject(oldRc, codec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// If new image is same as old, the hash may not be distinct, so add a suffix.
|
|
oldHash += "-orig"
|
|
oldRc, err = UpdateExistingReplicationController(coreClient, coreClient, oldRc, o.Namespace, newRc.Name, o.DeploymentKey, oldHash, o.Out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if o.Rollback {
|
|
newName := o.FindNewName(oldRc)
|
|
if newRc, err = LoadExistingNextReplicationController(coreClient, o.Namespace, newName); err != nil {
|
|
return err
|
|
}
|
|
|
|
if newRc == nil {
|
|
return fmt.Errorf("Could not find %s to rollback.\n", newName)
|
|
}
|
|
}
|
|
|
|
if o.OldName == newRc.Name {
|
|
return fmt.Errorf("%s cannot have the same name as the existing ReplicationController %s",
|
|
filename, o.OldName)
|
|
}
|
|
|
|
updater := NewRollingUpdater(newRc.Namespace, coreClient, coreClient, o.ScaleClient)
|
|
|
|
// To successfully pull off a rolling update the new and old rc have to differ
|
|
// by at least one selector. Every new pod should have the selector and every
|
|
// old pod should not have the selector.
|
|
var hasLabel bool
|
|
for key, oldValue := range oldRc.Spec.Selector {
|
|
if newValue, ok := newRc.Spec.Selector[key]; ok && newValue != oldValue {
|
|
hasLabel = true
|
|
break
|
|
}
|
|
}
|
|
if !hasLabel {
|
|
return fmt.Errorf("%s must specify a matching key with non-equal value in Selector for %s",
|
|
filename, o.OldName)
|
|
}
|
|
// TODO: handle scales during rolling update
|
|
if replicasDefaulted {
|
|
t := *oldRc.Spec.Replicas
|
|
newRc.Spec.Replicas = &t
|
|
}
|
|
|
|
if o.DryRun {
|
|
oldRcData := &bytes.Buffer{}
|
|
newRcData := &bytes.Buffer{}
|
|
if o.OutputFormat == "" {
|
|
oldRcData.WriteString(oldRc.Name)
|
|
newRcData.WriteString(newRc.Name)
|
|
} else {
|
|
printer, err := o.ToPrinter("rolling updated")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := printer.PrintObj(oldRc, oldRcData); err != nil {
|
|
return err
|
|
}
|
|
if err := printer.PrintObj(newRc, newRcData); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
fmt.Fprintf(o.Out, "Rolling from:\n%s\nTo:\n%s\n", string(oldRcData.Bytes()), string(newRcData.Bytes()))
|
|
return nil
|
|
}
|
|
updateCleanupPolicy := DeleteRollingUpdateCleanupPolicy
|
|
if o.KeepOldName {
|
|
updateCleanupPolicy = RenameRollingUpdateCleanupPolicy
|
|
}
|
|
config := &RollingUpdaterConfig{
|
|
Out: o.Out,
|
|
OldRc: oldRc,
|
|
NewRc: newRc,
|
|
UpdatePeriod: o.Period,
|
|
Interval: o.Interval,
|
|
Timeout: timeout,
|
|
CleanupPolicy: updateCleanupPolicy,
|
|
MaxUnavailable: intstr.FromInt(0),
|
|
MaxSurge: intstr.FromInt(1),
|
|
}
|
|
if o.Rollback {
|
|
err = AbortRollingUpdate(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
coreClient.ReplicationControllers(config.NewRc.Namespace).Update(config.NewRc)
|
|
}
|
|
err = updater.Update(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
message := "rolling updated"
|
|
if o.KeepOldName {
|
|
newRc.Name = o.OldName
|
|
} else {
|
|
message = fmt.Sprintf("rolling updated to %q", newRc.Name)
|
|
}
|
|
newRc, err = coreClient.ReplicationControllers(o.Namespace).Get(newRc.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printer, err := o.ToPrinter(message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return printer.PrintObj(newRc, o.Out)
|
|
}
|
|
|
|
func findNewName(args []string, oldRc *corev1.ReplicationController) string {
|
|
if len(args) >= 2 {
|
|
return args[1]
|
|
}
|
|
if oldRc != nil {
|
|
newName, _ := GetNextControllerAnnotation(oldRc)
|
|
return newName
|
|
}
|
|
return ""
|
|
}
|