Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
57df11bc
Commit
57df11bc
authored
Oct 19, 2018
by
Jordan Liggitt
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Switch kubectl rollout to directly rolling back deployments
parent
0ba00ec3
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
136 additions
and
92 deletions
+136
-92
BUILD
pkg/kubectl/BUILD
+2
-0
rollback.go
pkg/kubectl/rollback.go
+101
-90
rollback_test.go
pkg/kubectl/rollback_test.go
+21
-0
deployment.go
pkg/kubectl/util/deployment/deployment.go
+10
-0
apps.sh
test/cmd/apps.sh
+1
-1
template-output.sh
test/cmd/template-output.sh
+1
-1
No files found.
pkg/kubectl/BUILD
View file @
57df11bc
...
@@ -31,6 +31,7 @@ go_test(
...
@@ -31,6 +31,7 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/apis/testapigroup/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/testapigroup/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
...
@@ -74,6 +75,7 @@ go_library(
...
@@ -74,6 +75,7 @@ go_library(
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/api/extensions/v1beta1:go_default_library",
"//staging/src/k8s.io/api/extensions/v1beta1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
...
...
pkg/kubectl/rollback.go
View file @
57df11bc
...
@@ -19,14 +19,11 @@ package kubectl
...
@@ -19,14 +19,11 @@ package kubectl
import
(
import
(
"bytes"
"bytes"
"fmt"
"fmt"
"os"
"os/signal"
"sort"
"sort"
"syscall"
appsv1
"k8s.io/api/apps/v1"
appsv1
"k8s.io/api/apps/v1"
corev1
"k8s.io/api/core/v1"
corev1
"k8s.io/api/core/v1"
extensionsv1beta1
"k8s.io/api/extensions/v1beta1
"
apiequality
"k8s.io/apimachinery/pkg/api/equality
"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/meta"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
metav1
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime"
...
@@ -34,12 +31,10 @@ import (
...
@@ -34,12 +31,10 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes"
kapps
"k8s.io/kubernetes/pkg/kubectl/apps"
kapps
"k8s.io/kubernetes/pkg/kubectl/apps"
"k8s.io/kubernetes/pkg/kubectl/scheme"
"k8s.io/kubernetes/pkg/kubectl/scheme"
deploymentutil
"k8s.io/kubernetes/pkg/kubectl/util/deployment"
deploymentutil
"k8s.io/kubernetes/pkg/kubectl/util/deployment"
sliceutil
"k8s.io/kubernetes/pkg/kubectl/util/slice"
)
)
const
(
const
(
...
@@ -119,124 +114,140 @@ func (r *DeploymentRollbacker) Rollback(obj runtime.Object, updatedAnnotations m
...
@@ -119,124 +114,140 @@ func (r *DeploymentRollbacker) Rollback(obj runtime.Object, updatedAnnotations m
return
""
,
fmt
.
Errorf
(
"failed to retrieve Deployment %s: %v"
,
name
,
err
)
return
""
,
fmt
.
Errorf
(
"failed to retrieve Deployment %s: %v"
,
name
,
err
)
}
}
rsForRevision
,
err
:=
deploymentRevision
(
deployment
,
r
.
c
,
toRevision
)
if
err
!=
nil
{
return
""
,
err
}
if
dryRun
{
if
dryRun
{
return
simpleDryRun
(
deployment
,
r
.
c
,
toRevision
)
return
printTemplate
(
&
rsForRevision
.
Spec
.
Template
)
}
}
if
deployment
.
Spec
.
Paused
{
if
deployment
.
Spec
.
Paused
{
return
""
,
fmt
.
Errorf
(
"you cannot rollback a paused deployment; resume it first with 'kubectl rollout resume deployment/%s' and try again"
,
name
)
return
""
,
fmt
.
Errorf
(
"you cannot rollback a paused deployment; resume it first with 'kubectl rollout resume deployment/%s' and try again"
,
name
)
}
}
deploymentRollback
:=
&
extensionsv1beta1
.
DeploymentRollback
{
Name
:
name
,
// Skip if the revision already matches current Deployment
UpdatedAnnotations
:
updatedAnnotations
,
if
equalIgnoreHash
(
&
rsForRevision
.
Spec
.
Template
,
&
deployment
.
Spec
.
Template
)
{
RollbackTo
:
extensionsv1beta1
.
RollbackConfig
{
return
fmt
.
Sprintf
(
"%s (current template already matches revision %d)"
,
rollbackSkipped
,
toRevision
),
nil
Revision
:
toRevision
,
},
}
}
result
:=
""
// Get current events
// remove hash label before patching back into the deployment
events
,
err
:=
r
.
c
.
CoreV1
()
.
Events
(
namespace
)
.
List
(
metav1
.
ListOptions
{})
delete
(
rsForRevision
.
Spec
.
Template
.
Labels
,
appsv1
.
DefaultDeploymentUniqueLabelKey
)
if
err
!=
nil
{
return
result
,
err
// compute deployment annotations
annotations
:=
map
[
string
]
string
{}
for
k
:=
range
annotationsToSkip
{
if
v
,
ok
:=
deployment
.
Annotations
[
k
];
ok
{
annotations
[
k
]
=
v
}
}
}
// Do the rollback
for
k
,
v
:=
range
rsForRevision
.
Annotations
{
// TODO: This is DEPRECATED. It should be updated. DaemonSets and StatefulSets implement rollback by
if
!
annotationsToSkip
[
k
]
{
// patching using history (ControllerRevision data). Deployments should probably also implement
annotations
[
k
]
=
v
// rollback using a patch.
}
if
err
:=
r
.
c
.
ExtensionsV1beta1
()
.
Deployments
(
namespace
)
.
Rollback
(
deploymentRollback
);
err
!=
nil
{
return
result
,
err
}
}
// Watch for the changes of events
watch
,
err
:=
r
.
c
.
CoreV1
()
.
Events
(
namespace
)
.
Watch
(
metav1
.
ListOptions
{
Watch
:
true
,
ResourceVersion
:
events
.
ResourceVersion
})
// make patch to restore
patchType
,
patch
,
err
:=
getDeploymentPatch
(
&
rsForRevision
.
Spec
.
Template
,
annotations
)
if
err
!=
nil
{
if
err
!=
nil
{
return
result
,
err
return
""
,
fmt
.
Errorf
(
"failed restoring revision %d: %v"
,
toRevision
,
err
)
}
}
result
=
watchRollbackEvent
(
watch
)
return
result
,
err
}
// watchRollbackEvent watches for rollback events and returns rollback result
// Restore revision
func
watchRollbackEvent
(
w
watch
.
Interface
)
string
{
if
_
,
err
=
r
.
c
.
AppsV1
()
.
Deployments
(
namespace
)
.
Patch
(
name
,
patchType
,
patch
);
err
!=
nil
{
signals
:=
make
(
chan
os
.
Signal
,
1
)
return
""
,
fmt
.
Errorf
(
"failed restoring revision %d: %v"
,
toRevision
,
err
)
signal
.
Notify
(
signals
,
os
.
Interrupt
,
os
.
Kill
,
syscall
.
SIGTERM
)
for
{
select
{
case
event
,
ok
:=
<-
w
.
ResultChan
()
:
if
!
ok
{
return
""
}
obj
,
ok
:=
event
.
Object
.
(
*
corev1
.
Event
)
if
!
ok
{
w
.
Stop
()
return
""
}
isRollback
,
result
:=
isRollbackEvent
(
obj
)
if
isRollback
{
w
.
Stop
()
return
result
}
case
<-
signals
:
w
.
Stop
()
}
}
}
return
rollbackSuccess
,
nil
}
}
// isRollbackEvent checks if the input event is about rollback, and returns true and
// equalIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash]
// related result string back if it is.
// We ignore pod-template-hash because:
func
isRollbackEvent
(
e
*
corev1
.
Event
)
(
bool
,
string
)
{
// 1. The hash result would be different upon podTemplateSpec API changes
rollbackEventReasons
:=
[]
string
{
deploymentutil
.
RollbackRevisionNotFound
,
deploymentutil
.
RollbackTemplateUnchanged
,
deploymentutil
.
RollbackDone
}
// (e.g. the addition of a new field will cause the hash code to change)
for
_
,
reason
:=
range
rollbackEventReasons
{
// 2. The deployment template won't have hash labels
if
e
.
Reason
==
reason
{
func
equalIgnoreHash
(
template1
,
template2
*
corev1
.
PodTemplateSpec
)
bool
{
if
reason
==
deploymentutil
.
RollbackDone
{
t1Copy
:=
template1
.
DeepCopy
()
return
true
,
rollbackSuccess
t2Copy
:=
template2
.
DeepCopy
()
}
// Remove hash labels from template.Labels before comparing
return
true
,
fmt
.
Sprintf
(
"%s (%s: %s)"
,
rollbackSkipped
,
e
.
Reason
,
e
.
Message
)
delete
(
t1Copy
.
Labels
,
appsv1
.
DefaultDeploymentUniqueLabelKey
)
}
delete
(
t2Copy
.
Labels
,
appsv1
.
DefaultDeploymentUniqueLabelKey
)
}
return
apiequality
.
Semantic
.
DeepEqual
(
t1Copy
,
t2Copy
)
return
false
,
""
}
}
func
simpleDryRun
(
deployment
*
appsv1
.
Deployment
,
c
kubernetes
.
Interface
,
toRevision
int64
)
(
string
,
error
)
{
// annotationsToSkip lists the annotations that should be preserved from the deployment and not
// copied from the replicaset when rolling a deployment back
var
annotationsToSkip
=
map
[
string
]
bool
{
corev1
.
LastAppliedConfigAnnotation
:
true
,
deploymentutil
.
RevisionAnnotation
:
true
,
deploymentutil
.
RevisionHistoryAnnotation
:
true
,
deploymentutil
.
DesiredReplicasAnnotation
:
true
,
deploymentutil
.
MaxReplicasAnnotation
:
true
,
appsv1
.
DeprecatedRollbackTo
:
true
,
}
// getPatch returns a patch that can be applied to restore a Deployment to a
// previous version. If the returned error is nil the patch is valid.
func
getDeploymentPatch
(
podTemplate
*
corev1
.
PodTemplateSpec
,
annotations
map
[
string
]
string
)
(
types
.
PatchType
,
[]
byte
,
error
)
{
// Create a patch of the Deployment that replaces spec.template
patch
,
err
:=
json
.
Marshal
([]
interface
{}{
map
[
string
]
interface
{}{
"op"
:
"replace"
,
"path"
:
"/spec/template"
,
"value"
:
podTemplate
,
},
map
[
string
]
interface
{}{
"op"
:
"replace"
,
"path"
:
"/metadata/annotations"
,
"value"
:
annotations
,
},
})
return
types
.
JSONPatchType
,
patch
,
err
}
func
deploymentRevision
(
deployment
*
appsv1
.
Deployment
,
c
kubernetes
.
Interface
,
toRevision
int64
)
(
revision
*
appsv1
.
ReplicaSet
,
err
error
)
{
_
,
allOldRSs
,
newRS
,
err
:=
deploymentutil
.
GetAllReplicaSets
(
deployment
,
c
.
AppsV1
())
_
,
allOldRSs
,
newRS
,
err
:=
deploymentutil
.
GetAllReplicaSets
(
deployment
,
c
.
AppsV1
())
if
err
!=
nil
{
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"failed to retrieve replica sets from deployment %s: %v"
,
deployment
.
Name
,
err
)
return
nil
,
fmt
.
Errorf
(
"failed to retrieve replica sets from deployment %s: %v"
,
deployment
.
Name
,
err
)
}
}
allRSs
:=
allOldRSs
allRSs
:=
allOldRSs
if
newRS
!=
nil
{
if
newRS
!=
nil
{
allRSs
=
append
(
allRSs
,
newRS
)
allRSs
=
append
(
allRSs
,
newRS
)
}
}
revisionToSpec
:=
make
(
map
[
int64
]
*
corev1
.
PodTemplateSpec
)
var
(
latestReplicaSet
*
appsv1
.
ReplicaSet
latestRevision
=
int64
(
-
1
)
previousReplicaSet
*
appsv1
.
ReplicaSet
previousRevision
=
int64
(
-
1
)
)
for
_
,
rs
:=
range
allRSs
{
for
_
,
rs
:=
range
allRSs
{
v
,
err
:=
deploymentutil
.
Revision
(
rs
)
if
v
,
err
:=
deploymentutil
.
Revision
(
rs
);
err
==
nil
{
if
err
!=
nil
{
if
toRevision
==
0
{
continue
if
latestRevision
<
v
{
// newest one we've seen so far
previousRevision
=
latestRevision
previousReplicaSet
=
latestReplicaSet
latestRevision
=
v
latestReplicaSet
=
rs
}
else
if
previousRevision
<
v
{
// second newest one we've seen so far
previousRevision
=
v
previousReplicaSet
=
rs
}
}
else
if
toRevision
==
v
{
return
rs
,
nil
}
}
}
revisionToSpec
[
v
]
=
&
rs
.
Spec
.
Template
}
if
len
(
revisionToSpec
)
<
2
{
return
""
,
fmt
.
Errorf
(
"no rollout history found for deployment %q"
,
deployment
.
Name
)
}
}
if
toRevision
>
0
{
if
toRevision
>
0
{
template
,
ok
:=
revisionToSpec
[
toRevision
]
return
nil
,
revisionNotFoundErr
(
toRevision
)
if
!
ok
{
return
""
,
revisionNotFoundErr
(
toRevision
)
}
return
printTemplate
(
template
)
}
}
// Sort the revisionToSpec map by revision
if
previousReplicaSet
==
nil
{
revisions
:=
make
([]
int64
,
0
,
len
(
revisionToSpec
))
return
nil
,
fmt
.
Errorf
(
"no rollout history found for deployment %q"
,
deployment
.
Name
)
for
r
:=
range
revisionToSpec
{
revisions
=
append
(
revisions
,
r
)
}
}
sliceutil
.
SortInts64
(
revisions
)
return
previousReplicaSet
,
nil
template
,
_
:=
revisionToSpec
[
revisions
[
len
(
revisions
)
-
2
]]
return
printTemplate
(
template
)
}
}
type
DaemonSetRollbacker
struct
{
type
DaemonSetRollbacker
struct
{
...
...
pkg/kubectl/rollback_test.go
View file @
57df11bc
...
@@ -20,7 +20,9 @@ import (
...
@@ -20,7 +20,9 @@ import (
"reflect"
"reflect"
"testing"
"testing"
corev1
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/fake"
)
)
...
@@ -44,3 +46,22 @@ func TestRollbackerFor(t *testing.T) {
...
@@ -44,3 +46,22 @@ func TestRollbackerFor(t *testing.T) {
}
}
}
}
}
}
func
TestGetDeploymentPatch
(
t
*
testing
.
T
)
{
patchType
,
patchBytes
,
err
:=
getDeploymentPatch
(
&
corev1
.
PodTemplateSpec
{
Spec
:
corev1
.
PodSpec
{
Containers
:
[]
corev1
.
Container
{{
Image
:
"foo"
}}}},
map
[
string
]
string
{
"a"
:
"true"
})
if
err
!=
nil
{
t
.
Error
(
err
)
}
if
patchType
!=
types
.
JSONPatchType
{
t
.
Errorf
(
"expected strategic merge patch, got %v"
,
patchType
)
}
expectedPatch
:=
`[`
+
`{"op":"replace","path":"/spec/template","value":{"metadata":{"creationTimestamp":null},"spec":{"containers":[{"name":"","image":"foo","resources":{}}]}}},`
+
`{"op":"replace","path":"/metadata/annotations","value":{"a":"true"}}`
+
`]`
if
string
(
patchBytes
)
!=
expectedPatch
{
t
.
Errorf
(
"expected:
\n
%s
\n
got
\n
%s"
,
expectedPatch
,
string
(
patchBytes
))
}
}
pkg/kubectl/util/deployment/deployment.go
View file @
57df11bc
...
@@ -33,6 +33,16 @@ import (
...
@@ -33,6 +33,16 @@ import (
const
(
const
(
// RevisionAnnotation is the revision annotation of a deployment's replica sets which records its rollout sequence
// RevisionAnnotation is the revision annotation of a deployment's replica sets which records its rollout sequence
RevisionAnnotation
=
"deployment.kubernetes.io/revision"
RevisionAnnotation
=
"deployment.kubernetes.io/revision"
// RevisionHistoryAnnotation maintains the history of all old revisions that a replica set has served for a deployment.
RevisionHistoryAnnotation
=
"deployment.kubernetes.io/revision-history"
// DesiredReplicasAnnotation is the desired replicas for a deployment recorded as an annotation
// in its replica sets. Helps in separating scaling events from the rollout process and for
// determining if the new replica set for a deployment is really saturated.
DesiredReplicasAnnotation
=
"deployment.kubernetes.io/desired-replicas"
// MaxReplicasAnnotation is the maximum replicas a deployment can have at a given point, which
// is deployment.spec.replicas + maxSurge. Used by the underlying replica sets to estimate their
// proportions in case the deployment has surge replicas.
MaxReplicasAnnotation
=
"deployment.kubernetes.io/max-replicas"
// RollbackRevisionNotFound is not found rollback event reason
// RollbackRevisionNotFound is not found rollback event reason
RollbackRevisionNotFound
=
"DeploymentRollbackRevisionNotFound"
RollbackRevisionNotFound
=
"DeploymentRollbackRevisionNotFound"
// RollbackTemplateUnchanged is the template unchanged rollback event reason
// RollbackTemplateUnchanged is the template unchanged rollback event reason
...
...
test/cmd/apps.sh
View file @
57df11bc
...
@@ -299,7 +299,7 @@ run_deployment_tests() {
...
@@ -299,7 +299,7 @@ run_deployment_tests() {
sleep
1
sleep
1
kube::test::get_object_assert deployment
"{{range.items}}{{
$image_field0
}}:{{end}}"
"
${
IMAGE_DEPLOYMENT_R1
}
:"
kube::test::get_object_assert deployment
"{{range.items}}{{
$image_field0
}}:{{end}}"
"
${
IMAGE_DEPLOYMENT_R1
}
:"
# Rollback to revision 1000000 - should be no-op
# Rollback to revision 1000000 - should be no-op
kubectl rollout undo deployment nginx
--to-revision
=
1000000
"
${
kube_flags
[@]
}
"
!
kubectl rollout undo deployment nginx
--to-revision
=
1000000
"
${
kube_flags
[@]
}
"
kube::test::get_object_assert deployment
"{{range.items}}{{
$image_field0
}}:{{end}}"
"
${
IMAGE_DEPLOYMENT_R1
}
:"
kube::test::get_object_assert deployment
"{{range.items}}{{
$image_field0
}}:{{end}}"
"
${
IMAGE_DEPLOYMENT_R1
}
:"
# Rollback to last revision
# Rollback to last revision
kubectl rollout undo deployment nginx
"
${
kube_flags
[@]
}
"
kubectl rollout undo deployment nginx
"
${
kube_flags
[@]
}
"
...
...
test/cmd/template-output.sh
View file @
57df11bc
...
@@ -222,7 +222,7 @@ EOF
...
@@ -222,7 +222,7 @@ EOF
kube::test::if_has_string
"
${
output_message
}
"
'deploy:'
kube::test::if_has_string
"
${
output_message
}
"
'deploy:'
# check that "rollout undo" supports --template output
# check that "rollout undo" supports --template output
output_message
=
$(
kubectl
"
${
kube_flags
[@]
}
"
rollout undo deploy/deploy
--template
=
"{{ .metadata.name }}:"
)
output_message
=
$(
kubectl
"
${
kube_flags
[@]
}
"
rollout undo deploy/deploy
--t
o-revision
=
1
--t
emplate
=
"{{ .metadata.name }}:"
)
kube::test::if_has_string
"
${
output_message
}
"
'deploy:'
kube::test::if_has_string
"
${
output_message
}
"
'deploy:'
# check that "config view" command supports --template output
# check that "config view" command supports --template output
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment