Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StatefulSet kubectl rollout command #49674

Merged
merged 1 commit into from
Aug 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 99 additions & 46 deletions hack/make-rules/test-cmd-util.sh

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions hack/testdata/rollingupdate-statefulset-rv2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apiVersion: apps/v1beta2
kind: StatefulSet
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx-statefulset
updateStrategy:
type: RollingUpdate
serviceName: "nginx"
replicas: 0
template:
metadata:
labels:
app: nginx-statefulset
spec:
terminationGracePeriodSeconds: 5
containers:
- name: nginx
image: gcr.io/google_containers/nginx-slim:0.8
ports:
- containerPort: 80
name: web
command:
- sh
- -c
- 'while true; do sleep 1; done'
- name: pause
image: gcr.io/google-containers/pause:2.0
ports:
- containerPort: 81
name: web-2
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
# A headless service to create DNS records
apiVersion: v1
kind: Service
metadata:
annotations:
service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
name: nginx
labels:
app: nginx-statefulset
spec:
ports:
- port: 80
name: web
# *.nginx.default.svc.cluster.local
clusterIP: None
selector:
app: nginx-statefulset
---
apiVersion: apps/v1beta1
apiVersion: apps/v1beta2
kind: StatefulSet
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx-statefulset
updateStrategy:
type: RollingUpdate
serviceName: "nginx"
replicas: 0
template:
metadata:
labels:
app: nginx-statefulset
spec:
terminationGracePeriodSeconds: 0
terminationGracePeriodSeconds: 5
containers:
- name: nginx
image: gcr.io/google_containers/nginx-slim:0.7
Expand Down
4 changes: 2 additions & 2 deletions pkg/controller/statefulset/stateful_set_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,11 @@ func (ssc *defaultStatefulSetControl) updateStatefulSet(
collisionCount int32,
pods []*v1.Pod) (*apps.StatefulSetStatus, error) {
// get the current and update revisions of the set.
currentSet, err := applyRevision(set, currentRevision)
currentSet, err := ApplyRevision(set, currentRevision)
if err != nil {
return nil, err
}
updateSet, err := applyRevision(set, updateRevision)
updateSet, err := ApplyRevision(set, updateRevision)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/statefulset/stateful_set_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,7 @@ func TestStatefulSetControlRollback(t *testing.T) {
t.Fatalf("%s: %s", test.name, err)
}
history.SortControllerRevisions(revisions)
set, err = applyRevision(set, revisions[0])
set, err = ApplyRevision(set, revisions[0])
if err != nil {
t.Fatalf("%s: %s", test.name, err)
}
Expand Down
14 changes: 12 additions & 2 deletions pkg/controller/statefulset/stateful_set_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package statefulset

import (
"bytes"
"encoding/json"
"fmt"
"regexp"
Expand Down Expand Up @@ -266,6 +267,15 @@ func newVersionedStatefulSetPod(currentSet, updateSet *apps.StatefulSet, current
return pod
}

// Match check if the given StatefulSet's template matches the template stored in the given history.
func Match(ss *apps.StatefulSet, history *apps.ControllerRevision) (bool, error) {
patch, err := getPatch(ss)
if err != nil {
return false, err
}
return bytes.Equal(patch, history.Data.Raw), nil
}

// getPatch returns a strategic merge patch that can be applied to restore a StatefulSet to a
// previous version. If the returned error is nil the patch is valid. The current state that we save is just the
// PodSpecTemplate. We can modify this later to encompass more state (or less) and remain compatible with previously
Expand Down Expand Up @@ -319,9 +329,9 @@ func newRevision(set *apps.StatefulSet, revision int64, collisionCount *int32) (
return cr, nil
}

// applyRevision returns a new StatefulSet constructed by restoring the state in revision to set. If the returned error
// ApplyRevision returns a new StatefulSet constructed by restoring the state in revision to set. If the returned error
// is nil, the returned StatefulSet is valid.
func applyRevision(set *apps.StatefulSet, revision *apps.ControllerRevision) (*apps.StatefulSet, error) {
func ApplyRevision(set *apps.StatefulSet, revision *apps.ControllerRevision) (*apps.StatefulSet, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crimsonfaith91 looks like you need it in kubectl/rollback.go, so i don't see another way. perhaps someone else has an idea

obj, err := scheme.Scheme.DeepCopy(set)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/statefulset/stateful_set_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func TestCreateApplyRevision(t *testing.T) {
key := "foo"
expectedValue := "bar"
set.Annotations[key] = expectedValue
restoredSet, err := applyRevision(set, revision)
restoredSet, err := ApplyRevision(set, revision)
if err != nil {
t.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/kubectl/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ go_library(
"//pkg/client/unversioned:go_default_library",
"//pkg/controller/daemon:go_default_library",
"//pkg/controller/deployment/util:go_default_library",
"//pkg/controller/statefulset:go_default_library",
"//pkg/credentialprovider:go_default_library",
"//pkg/kubectl/resource:go_default_library",
"//pkg/kubectl/util:go_default_library",
Expand Down
1 change: 1 addition & 0 deletions pkg/kubectl/cmd/rollout/rollout.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var (

* deployments
* daemonsets
* statefulsets
`)
)

Expand Down
2 changes: 1 addition & 1 deletion pkg/kubectl/cmd/rollout/rollout_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ var (
func NewCmdRolloutHistory(f cmdutil.Factory, out io.Writer) *cobra.Command {
options := &resource.FilenameOptions{}

validArgs := []string{"deployment", "daemonset"}
validArgs := []string{"deployment", "daemonset", "statefulset"}
argAliases := kubectl.ResourceAliases(validArgs)

cmd := &cobra.Command{
Expand Down
2 changes: 1 addition & 1 deletion pkg/kubectl/cmd/rollout/rollout_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var (
func NewCmdRolloutStatus(f cmdutil.Factory, out io.Writer) *cobra.Command {
options := &resource.FilenameOptions{}

validArgs := []string{"deployment", "daemonset"}
validArgs := []string{"deployment", "daemonset", "statefulset"}
argAliases := kubectl.ResourceAliases(validArgs)

cmd := &cobra.Command{
Expand Down
2 changes: 1 addition & 1 deletion pkg/kubectl/cmd/rollout/rollout_undo.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ var (
func NewCmdRolloutUndo(f cmdutil.Factory, out io.Writer) *cobra.Command {
options := &UndoOptions{}

validArgs := []string{"deployment", "daemonset"}
validArgs := []string{"deployment", "daemonset", "statefulset"}
argAliases := kubectl.ResourceAliases(validArgs)

cmd := &cobra.Command{
Expand Down
54 changes: 41 additions & 13 deletions pkg/kubectl/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ type DaemonSetHistoryViewer struct {
// 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) {
versionedExtensionsClient := versionedExtensionsClientV1beta1(h.c)
versionedAppsClient := versionedAppsClientV1beta1(h.c)
ds, allHistory, err := controlledHistories(versionedExtensionsClient, versionedAppsClient, namespace, name)
versionedExtensionsClient := versionedExtensionsClientV1beta1(h.c)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change isn't necessary (unless there's a reason to move it?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched the order that puts versionedExtensionsClient before versionedAppsClient. There are 2 reasons behind:
(1) it is alphabetically ordered for having versionedAppsClient before versionedExtensionsClient
(2) controlledHistories function brings argument in the order of versionedAppsClient and then versionedExtensionsClient

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay

versionedObj, allHistory, err := controlledHistories(versionedAppsClient, versionedExtensionsClient, namespace, name, "DaemonSet")
if err != nil {
return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", name, err)
}
Expand All @@ -175,7 +175,13 @@ func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision in
if !ok {
return "", fmt.Errorf("unable to find the specified revision")
}
dsOfHistory, err := applyHistory(ds, history)

versionedDS, ok := versionedObj.(*extensionsv1beta1.DaemonSet)
if !ok {
return "", fmt.Errorf("unexpected non-DaemonSet object returned: %v", versionedDS)
}

dsOfHistory, err := applyHistory(versionedDS, history)
if err != nil {
return "", fmt.Errorf("unable to parse history %s", history.Name)
}
Expand Down Expand Up @@ -256,29 +262,51 @@ func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision
})
}

// controlledHistories returns all ControllerRevisions controlled by the given DaemonSet
func controlledHistories(extensions clientextensionsv1beta1.ExtensionsV1beta1Interface, apps clientappsv1beta1.AppsV1beta1Interface, namespace, name string) (*extensionsv1beta1.DaemonSet, []*appsv1beta1.ControllerRevision, error) {
ds, err := extensions.DaemonSets(namespace).Get(name, metav1.GetOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err)
// controlledHistories returns all ControllerRevisions controlled by the given API object
func controlledHistories(apps clientappsv1beta1.AppsV1beta1Interface, extensions clientextensionsv1beta1.ExtensionsV1beta1Interface, namespace, name, kind string) (runtime.Object, []*appsv1beta1.ControllerRevision, error) {
var obj runtime.Object
var labelSelector *metav1.LabelSelector

switch kind {
case "DaemonSet":
ds, err := extensions.DaemonSets(namespace).Get(name, metav1.GetOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err)
}
labelSelector = ds.Spec.Selector
obj = ds
case "StatefulSet":
ss, err := apps.StatefulSets(namespace).Get(name, metav1.GetOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve StatefulSet %s: %v", name, err)
}
labelSelector = ss.Spec.Selector
obj = ss
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a default case and return error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

default:
return nil, nil, fmt.Errorf("unsupported API object kind: %s", kind)
}

var result []*appsv1beta1.ControllerRevision
selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil {
return nil, nil, err
}
historyList, err := apps.ControllerRevisions(ds.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
historyList, err := apps.ControllerRevisions(namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
return nil, nil, err
}
accessor, err := meta.Accessor(obj)
if err != nil {
return nil, nil, fmt.Errorf("failed to obtain accessor for %s named %s: %v", kind, name, err)
}
for i := range historyList.Items {
history := historyList.Items[i]
// Only add history that belongs to the DaemonSet
if metav1.IsControlledBy(&history, ds) {
// Only add history that belongs to the API object
if metav1.IsControlledBy(&history, accessor) {
result = append(result, &history)
}
}
return ds, result, nil
return obj, result, nil
}

// applyHistory returns a specific revision of DaemonSet by applying the given history to a copy of the given DaemonSet
Expand Down
Loading