mirror of https://github.com/k3s-io/k3s
Implement kubectl rollout history and undo for DaemonSet
parent
dbd1503b65
commit
edabdac094
|
@ -41,6 +41,8 @@ IMAGE_NGINX="gcr.io/google-containers/nginx:1.7.9"
|
|||
IMAGE_DEPLOYMENT_R1="gcr.io/google-containers/nginx:test-cmd" # deployment-revision1.yaml
|
||||
IMAGE_DEPLOYMENT_R2="$IMAGE_NGINX" # deployment-revision2.yaml
|
||||
IMAGE_PERL="gcr.io/google-containers/perl"
|
||||
IMAGE_DAEMONSET_R1="gcr.io/google-containers/pause:2.0"
|
||||
IMAGE_DAEMONSET_R2="gcr.io/google-containers/pause:latest"
|
||||
|
||||
# Expose kubectl directly for readability
|
||||
PATH="${KUBE_OUTPUT_HOSTBIN}":$PATH
|
||||
|
@ -71,6 +73,7 @@ subjectaccessreviews="subjectaccessreviews"
|
|||
thirdpartyresources="thirdpartyresources"
|
||||
customresourcedefinitions="customresourcedefinitions"
|
||||
daemonsets="daemonsets"
|
||||
controllerrevisions="controllerrevisions"
|
||||
|
||||
|
||||
# Stops the running kubectl proxy, if there is one.
|
||||
|
@ -2868,6 +2871,39 @@ run_daemonset_tests() {
|
|||
kubectl apply -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
|
||||
# Template Generation should stay 1
|
||||
kube::test::get_object_assert 'daemonsets bind' "{{${template_generation_field}}}" '1'
|
||||
# Clean up
|
||||
kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
|
||||
}
|
||||
|
||||
run_daemonset_history_tests() {
|
||||
kube::log::status "Testing kubectl(v1:daemonsets, v1:controllerrevisions)"
|
||||
|
||||
### Test rolling back a DaemonSet
|
||||
# Pre-condition: no DaemonSet or its pods exists
|
||||
kube::test::get_object_assert daemonsets "{{range.items}}{{$id_field}}:{{end}}" ''
|
||||
# Command
|
||||
# Create a DaemonSet (revision 1)
|
||||
kubectl apply -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
|
||||
# Rollback to revision 1 - should be no-op
|
||||
kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}"
|
||||
kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
|
||||
# Update the DaemonSet (revision 2)
|
||||
kubectl apply -f hack/testdata/rollingupdate-daemonset-rv2.yaml "${kube_flags[@]}"
|
||||
kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
|
||||
# Rollback to revision 1 with dry-run - should be no-op
|
||||
kubectl rollout undo daemonset --dry-run=true "${kube_flags[@]}"
|
||||
kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
|
||||
# Rollback to revision 1
|
||||
kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}"
|
||||
kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
|
||||
# Rollback to revision 1000000 - should fail
|
||||
output_message=$(! kubectl rollout undo daemonset --to-revision=1000000 "${kube_flags[@]}" 2>&1)
|
||||
kube::test::if_has_string "${output_message}" "unable to find specified revision"
|
||||
kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
|
||||
# Rollback to last revision
|
||||
kubectl rollout undo daemonset "${kube_flags[@]}"
|
||||
kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
|
||||
# Clean up
|
||||
kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
|
||||
}
|
||||
|
||||
|
@ -3103,6 +3139,7 @@ runTests() {
|
|||
pdb_min_available=".spec.minAvailable"
|
||||
pdb_max_unavailable=".spec.maxUnavailable"
|
||||
template_generation_field=".spec.templateGeneration"
|
||||
daemonset_image_field="(index .spec.template.spec.containers 0).image"
|
||||
|
||||
# Make sure "default" namespace exists.
|
||||
if kube::test::if_supports_resource "${namespaces}" ; then
|
||||
|
@ -3555,6 +3592,9 @@ runTests() {
|
|||
|
||||
if kube::test::if_supports_resource "${daemonsets}" ; then
|
||||
run_daemonset_tests
|
||||
if kube::test::if_supports_resource "${controllerrevisions}"; then
|
||||
run_daemonset_history_tests
|
||||
fi
|
||||
fi
|
||||
|
||||
###########################
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
apiVersion: extensions/v1beta1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: bind
|
||||
spec:
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 10%
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
service: bind
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: "service"
|
||||
operator: "In"
|
||||
values: ["bind"]
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
namespaces: []
|
||||
containers:
|
||||
- name: kubernetes-pause
|
||||
image: gcr.io/google-containers/pause:latest
|
|
@ -48,6 +48,7 @@ go_library(
|
|||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
|
|
|
@ -17,8 +17,6 @@ limitations under the License.
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
|
@ -29,6 +27,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
intstrutil "k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
||||
|
@ -297,23 +296,18 @@ func (dsc *DaemonSetsController) controlledHistories(ds *extensions.DaemonSet) (
|
|||
|
||||
// Match check if ds template is semantically equal to the template stored in history
|
||||
func Match(template *v1.PodTemplateSpec, history *apps.ControllerRevision) (bool, error) {
|
||||
t, err := decodeHistory(history)
|
||||
t, err := DecodeHistory(history)
|
||||
return apiequality.Semantic.DeepEqual(template, t), err
|
||||
}
|
||||
|
||||
func decodeHistory(history *apps.ControllerRevision) (*v1.PodTemplateSpec, error) {
|
||||
raw := history.Data.Raw
|
||||
decoder := json.NewDecoder(bytes.NewBuffer(raw))
|
||||
func DecodeHistory(history *apps.ControllerRevision) (*v1.PodTemplateSpec, error) {
|
||||
template := v1.PodTemplateSpec{}
|
||||
err := decoder.Decode(&template)
|
||||
err := json.Unmarshal(history.Data.Raw, &template)
|
||||
return &template, err
|
||||
}
|
||||
|
||||
func encodeTemplate(template *v1.PodTemplateSpec) ([]byte, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
err := encoder.Encode(template)
|
||||
return buffer.Bytes(), err
|
||||
return json.Marshal(template)
|
||||
}
|
||||
|
||||
func (dsc *DaemonSetsController) snapshot(ds *extensions.DaemonSet, revision int64) (*apps.ControllerRevision, error) {
|
||||
|
|
|
@ -66,6 +66,7 @@ go_library(
|
|||
"//pkg/apis/policy:go_default_library",
|
||||
"//pkg/apis/rbac:go_default_library",
|
||||
"//pkg/client/clientset_generated/clientset:go_default_library",
|
||||
"//pkg/client/clientset_generated/clientset/typed/apps/v1beta1:go_default_library",
|
||||
"//pkg/client/clientset_generated/clientset/typed/core/v1:go_default_library",
|
||||
"//pkg/client/clientset_generated/clientset/typed/extensions/v1beta1:go_default_library",
|
||||
"//pkg/client/clientset_generated/internalclientset:go_default_library",
|
||||
|
@ -76,6 +77,7 @@ go_library(
|
|||
"//pkg/client/retry:go_default_library",
|
||||
"//pkg/client/unversioned:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/controller/daemon:go_default_library",
|
||||
"//pkg/controller/deployment/util:go_default_library",
|
||||
"//pkg/credentialprovider:go_default_library",
|
||||
"//pkg/kubectl/resource:go_default_library",
|
||||
|
@ -88,6 +90,7 @@ go_library(
|
|||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/spf13/cobra:go_default_library",
|
||||
"//vendor/github.com/spf13/pflag:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
||||
|
|
|
@ -28,15 +28,20 @@ import (
|
|||
|
||||
var (
|
||||
rollout_long = templates.LongDesc(`
|
||||
Manage a deployment using subcommands like "kubectl rollout undo deployment/abc"`)
|
||||
Manage the rollout of a resource.` + rollout_valid_resources)
|
||||
|
||||
rollout_example = templates.Examples(`
|
||||
# Rollback to the previous deployment
|
||||
kubectl rollout undo deployment/abc`)
|
||||
kubectl rollout undo deployment/abc
|
||||
|
||||
# Check the rollout status of a daemonset
|
||||
kubectl rollout status daemonset/foo`)
|
||||
|
||||
rollout_valid_resources = dedent.Dedent(`
|
||||
Valid resource types include:
|
||||
|
||||
* deployments
|
||||
* daemonsets
|
||||
`)
|
||||
)
|
||||
|
||||
|
@ -44,7 +49,7 @@ func NewCmdRollout(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
|
|||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rollout SUBCOMMAND",
|
||||
Short: i18n.T("Manage a deployment rollout"),
|
||||
Short: i18n.T("Manage the rollout of a resource"),
|
||||
Long: rollout_long,
|
||||
Example: rollout_example,
|
||||
Run: cmdutil.DefaultSubCommandRun(errOut),
|
||||
|
@ -54,7 +59,6 @@ func NewCmdRollout(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
|
|||
cmd.AddCommand(NewCmdRolloutPause(f, out))
|
||||
cmd.AddCommand(NewCmdRolloutResume(f, out))
|
||||
cmd.AddCommand(NewCmdRolloutUndo(f, out))
|
||||
|
||||
cmd.AddCommand(NewCmdRolloutStatus(f, out))
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -37,14 +37,14 @@ var (
|
|||
# View the rollout history of a deployment
|
||||
kubectl rollout history deployment/abc
|
||||
|
||||
# View the details of deployment revision 3
|
||||
kubectl rollout history deployment/abc --revision=3`)
|
||||
# View the details of daemonset revision 3
|
||||
kubectl rollout history daemonset/abc --revision=3`)
|
||||
)
|
||||
|
||||
func NewCmdRolloutHistory(f cmdutil.Factory, out io.Writer) *cobra.Command {
|
||||
options := &resource.FilenameOptions{}
|
||||
|
||||
validArgs := []string{"deployment"}
|
||||
validArgs := []string{"deployment", "daemonset"}
|
||||
argAliases := kubectl.ResourceAliases(validArgs)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
|
@ -53,7 +53,7 @@ var (
|
|||
Mark the provided resource as paused
|
||||
|
||||
Paused resources will not be reconciled by a controller.
|
||||
Use \"kubectl rollout resume\" to resume a paused resource.
|
||||
Use "kubectl rollout resume" to resume a paused resource.
|
||||
Currently only deployments support being paused.`)
|
||||
|
||||
pause_example = templates.Examples(`
|
||||
|
|
|
@ -50,7 +50,7 @@ var (
|
|||
func NewCmdRolloutStatus(f cmdutil.Factory, out io.Writer) *cobra.Command {
|
||||
options := &resource.FilenameOptions{}
|
||||
|
||||
validArgs := []string{"deployment"}
|
||||
validArgs := []string{"deployment", "daemonset"}
|
||||
argAliases := kubectl.ResourceAliases(validArgs)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
|
@ -54,8 +54,8 @@ var (
|
|||
# Rollback to the previous deployment
|
||||
kubectl rollout undo deployment/abc
|
||||
|
||||
# Rollback to deployment revision 3
|
||||
kubectl rollout undo deployment/abc --to-revision=3
|
||||
# Rollback to daemonset revision 3
|
||||
kubectl rollout undo daemonset/abc --to-revision=3
|
||||
|
||||
# Rollback to the previous deployment with dry-run
|
||||
kubectl rollout undo --dry-run=true deployment/abc`)
|
||||
|
@ -64,7 +64,7 @@ var (
|
|||
func NewCmdRolloutUndo(f cmdutil.Factory, out io.Writer) *cobra.Command {
|
||||
options := &UndoOptions{}
|
||||
|
||||
validArgs := []string{"deployment"}
|
||||
validArgs := []string{"deployment", "daemonset"}
|
||||
argAliases := kubectl.ResourceAliases(validArgs)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
|
@ -29,8 +29,11 @@ import (
|
|||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/pkg/apis/apps"
|
||||
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
"k8s.io/kubernetes/pkg/controller/daemon"
|
||||
deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util"
|
||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||
sliceutil "k8s.io/kubernetes/pkg/util/slice"
|
||||
|
@ -49,6 +52,8 @@ func HistoryViewerFor(kind schema.GroupKind, c clientset.Interface) (HistoryView
|
|||
switch kind {
|
||||
case extensions.Kind("Deployment"), apps.Kind("Deployment"):
|
||||
return &DeploymentHistoryViewer{c}, nil
|
||||
case extensions.Kind("DaemonSet"):
|
||||
return &DaemonSetHistoryViewer{c}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no history viewer has been implemented for %q", kind)
|
||||
}
|
||||
|
@ -100,14 +105,7 @@ func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision i
|
|||
if !ok {
|
||||
return "", fmt.Errorf("unable to find the specified revision")
|
||||
}
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
internalTemplate := &api.PodTemplateSpec{}
|
||||
if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(template, internalTemplate, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to convert podtemplate, %v", err)
|
||||
}
|
||||
w := printersinternal.NewPrefixWriter(buf)
|
||||
printersinternal.DescribePodTemplate(internalTemplate, w)
|
||||
return buf.String(), nil
|
||||
return printTemplate(template)
|
||||
}
|
||||
|
||||
// Sort the revisionToChangeCause map by revision
|
||||
|
@ -131,6 +129,101 @@ func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision i
|
|||
})
|
||||
}
|
||||
|
||||
func printTemplate(template *v1.PodTemplateSpec) (string, error) {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
internalTemplate := &api.PodTemplateSpec{}
|
||||
if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(template, internalTemplate, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to convert podtemplate, %v", err)
|
||||
}
|
||||
w := printersinternal.NewPrefixWriter(buf)
|
||||
printersinternal.DescribePodTemplate(internalTemplate, w)
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
type DaemonSetHistoryViewer struct {
|
||||
c clientset.Interface
|
||||
}
|
||||
|
||||
// ViewHistory returns a revision-to-history map as the revision history of a deployment
|
||||
// TODO: this should be a describer
|
||||
func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
|
||||
ds, err := h.c.Extensions().DaemonSets(namespace).Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err)
|
||||
}
|
||||
allHistory, err := controlledHistories(h.c, ds)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err)
|
||||
}
|
||||
historyInfo := make(map[int64]*appsv1beta1.ControllerRevision)
|
||||
for _, history := range allHistory {
|
||||
// TODO: for now we assume revisions don't overlap, we may need to handle it
|
||||
historyInfo[history.Revision] = history
|
||||
}
|
||||
|
||||
if len(historyInfo) == 0 {
|
||||
return "No rollout history found.", nil
|
||||
}
|
||||
|
||||
// Print details of a specific revision
|
||||
if revision > 0 {
|
||||
history, ok := historyInfo[revision]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to find the specified revision")
|
||||
}
|
||||
template, err := daemon.DecodeHistory(history)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to decode history %s", history.Name)
|
||||
}
|
||||
return printTemplate(template)
|
||||
}
|
||||
|
||||
// Print an overview of all Revisions
|
||||
// Sort the revisionToChangeCause map by revision
|
||||
revisions := make([]int64, 0, len(historyInfo))
|
||||
for r := range historyInfo {
|
||||
revisions = append(revisions, r)
|
||||
}
|
||||
sliceutil.SortInts64(revisions)
|
||||
|
||||
return tabbedString(func(out io.Writer) error {
|
||||
fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
|
||||
for _, r := range revisions {
|
||||
// Find the change-cause of revision r
|
||||
changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
|
||||
if len(changeCause) == 0 {
|
||||
changeCause = "<none>"
|
||||
}
|
||||
fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// controlledHistories returns all ControllerRevisions controlled by the given DaemonSet
|
||||
// TODO: Use external version DaemonSet instead when #3955 is fixed
|
||||
func controlledHistories(c clientset.Interface, ds *extensions.DaemonSet) ([]*appsv1beta1.ControllerRevision, error) {
|
||||
var result []*appsv1beta1.ControllerRevision
|
||||
selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
versionedClient := versionedClientsetForDaemonSet(c)
|
||||
historyList, err := versionedClient.AppsV1beta1().ControllerRevisions(ds.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range historyList.Items {
|
||||
history := historyList.Items[i]
|
||||
// Skip history that doesn't belong to the DaemonSet
|
||||
if controllerRef := controller.GetControllerOf(&history); controllerRef == nil || controllerRef.UID != ds.UID {
|
||||
continue
|
||||
}
|
||||
result = append(result, &history)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TODO: copied here until this becomes a describer
|
||||
func tabbedString(f func(io.Writer) error) (string, error) {
|
||||
out := new(tabwriter.Writer)
|
||||
|
|
|
@ -21,8 +21,10 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"syscall"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
@ -30,14 +32,22 @@ import (
|
|||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/pkg/apis/apps"
|
||||
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
externalextensions "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
"k8s.io/kubernetes/pkg/client/retry"
|
||||
"k8s.io/kubernetes/pkg/controller/daemon"
|
||||
deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util"
|
||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||
sliceutil "k8s.io/kubernetes/pkg/util/slice"
|
||||
)
|
||||
|
||||
const (
|
||||
rollbackSuccess = "rolled back"
|
||||
rollbackSkipped = "skipped rollback"
|
||||
)
|
||||
|
||||
// Rollbacker provides an interface for resources that can be rolled back.
|
||||
type Rollbacker interface {
|
||||
Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error)
|
||||
|
@ -47,6 +57,8 @@ func RollbackerFor(kind schema.GroupKind, c clientset.Interface) (Rollbacker, er
|
|||
switch kind {
|
||||
case extensions.Kind("Deployment"), apps.Kind("Deployment"):
|
||||
return &DeploymentRollbacker{c}, nil
|
||||
case extensions.Kind("DaemonSet"):
|
||||
return &DaemonSetRollbacker{c}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no rollbacker has been implemented for %q", kind)
|
||||
}
|
||||
|
@ -126,9 +138,9 @@ func isRollbackEvent(e *api.Event) (bool, string) {
|
|||
for _, reason := range rollbackEventReasons {
|
||||
if e.Reason == reason {
|
||||
if reason == deploymentutil.RollbackDone {
|
||||
return true, "rolled back"
|
||||
return true, rollbackSuccess
|
||||
}
|
||||
return true, fmt.Sprintf("skipped rollback (%s: %s)", e.Reason, e.Message)
|
||||
return true, fmt.Sprintf("%s (%s: %s)", rollbackSkipped, e.Reason, e.Message)
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
|
@ -165,7 +177,7 @@ func simpleDryRun(deployment *extensions.Deployment, c clientset.Interface, toRe
|
|||
if toRevision > 0 {
|
||||
template, ok := revisionToSpec[toRevision]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to find specified revision")
|
||||
return "", revisionNotFoundErr(toRevision)
|
||||
}
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
internalTemplate := &api.PodTemplateSpec{}
|
||||
|
@ -195,3 +207,108 @@ func simpleDryRun(deployment *extensions.Deployment, c clientset.Interface, toRe
|
|||
printersinternal.DescribePodTemplate(internalTemplate, w)
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
type DaemonSetRollbacker struct {
|
||||
c clientset.Interface
|
||||
}
|
||||
|
||||
func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) {
|
||||
if toRevision < 0 {
|
||||
return "", revisionNotFoundErr(toRevision)
|
||||
}
|
||||
|
||||
ds, ok := obj.(*extensions.DaemonSet)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("passed object is not a DaemonSet: %#v", obj)
|
||||
}
|
||||
allHistory, err := controlledHistories(r.c, ds)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err)
|
||||
}
|
||||
|
||||
if toRevision == 0 && len(allHistory) <= 1 {
|
||||
return "", fmt.Errorf("no last revision to roll back to")
|
||||
}
|
||||
|
||||
// Find the history to rollback to
|
||||
var toHistory *appsv1beta1.ControllerRevision
|
||||
if toRevision == 0 {
|
||||
// If toRevision == 0, find the latest revision (2nd max)
|
||||
sort.Sort(historiesByRevision(allHistory))
|
||||
toHistory = allHistory[len(allHistory)-2]
|
||||
} else {
|
||||
for _, h := range allHistory {
|
||||
if h.Revision == toRevision {
|
||||
// If toRevision != 0, find the history with matching revision
|
||||
toHistory = h
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if toHistory == nil {
|
||||
return "", revisionNotFoundErr(toRevision)
|
||||
}
|
||||
|
||||
// Get the template of the history to rollback to
|
||||
toTemplate, err := getInternalTemplate(toHistory)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
content := bytes.NewBuffer([]byte{})
|
||||
w := printersinternal.NewPrefixWriter(content)
|
||||
printersinternal.DescribePodTemplate(toTemplate, w)
|
||||
return fmt.Sprintf("will roll back to %s", content.String()), nil
|
||||
}
|
||||
|
||||
// Update DaemonSet template, and retry on conflict
|
||||
skipUpdate := false
|
||||
retryErr := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
var err error
|
||||
ds, err = r.c.Extensions().DaemonSets(ds.Namespace).Get(ds.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if apiequality.Semantic.DeepEqual(toTemplate, &ds.Spec.Template) {
|
||||
skipUpdate = true
|
||||
return nil
|
||||
}
|
||||
ds.Spec.Template = *toTemplate
|
||||
_, err = r.c.Extensions().DaemonSets(ds.Namespace).Update(ds)
|
||||
return err
|
||||
})
|
||||
if retryErr != nil {
|
||||
return "", retryErr
|
||||
}
|
||||
if skipUpdate {
|
||||
return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
|
||||
}
|
||||
|
||||
return rollbackSuccess, nil
|
||||
}
|
||||
|
||||
func getInternalTemplate(toHistory *appsv1beta1.ControllerRevision) (*api.PodTemplateSpec, error) {
|
||||
template, err := daemon.DecodeHistory(toHistory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
internalTemplate := &api.PodTemplateSpec{}
|
||||
if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(template, internalTemplate, nil); err != nil {
|
||||
return nil, fmt.Errorf("failed to convert podtemplate, %v", err)
|
||||
}
|
||||
return internalTemplate, nil
|
||||
}
|
||||
|
||||
func revisionNotFoundErr(r int64) error {
|
||||
return fmt.Errorf("unable to find specified revision %v in history", r)
|
||||
}
|
||||
|
||||
// TODO: copied from daemon controller, should extract to a library
|
||||
type historiesByRevision []*appsv1beta1.ControllerRevision
|
||||
|
||||
func (h historiesByRevision) Len() int { return len(h) }
|
||||
func (h historiesByRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
func (h historiesByRevision) Less(i, j int) bool {
|
||||
return h[i].Revision < h[j].Revision
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package kubectl
|
|||
|
||||
import (
|
||||
externalclientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
|
||||
apps "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/apps/v1beta1"
|
||||
core "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/core/v1"
|
||||
extensions "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/extensions/v1beta1"
|
||||
internalclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
|
@ -32,3 +33,12 @@ func versionedClientsetForDeployment(internalClient internalclientset.Interface)
|
|||
ExtensionsV1beta1Client: extensions.New(internalClient.Extensions().RESTClient()),
|
||||
}
|
||||
}
|
||||
|
||||
func versionedClientsetForDaemonSet(internalClient internalclientset.Interface) externalclientset.Interface {
|
||||
if internalClient == nil {
|
||||
return &externalclientset.Clientset{}
|
||||
}
|
||||
return &externalclientset.Clientset{
|
||||
AppsV1beta1Client: apps.New(internalClient.Apps().RESTClient()),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue