Browse code

Implement a Rolling deployment strategy

Implement a rolling deployment strategy which piggybacks on the upstream
RollingUpdater functionality.

Dan Mace authored on 2015/04/23 22:23:03
Showing 26 changed files
... ...
@@ -88,7 +88,7 @@ describe('', function() {
88 88
       expect(element(by.cssContainingText("h2.service","frontend")).isPresent()).toBe(true);
89 89
       expect(element(by.cssContainingText(".pod-template-image","Build: ruby-sample-build")).isPresent()).toBe(true);
90 90
       expect(element(by.cssContainingText(".deployment-trigger","new image for test/origin-ruby-sample:latest")).isPresent()).toBe(true);
91
-      expect(element.all(by.css(".pod-running")).count()).toEqual(2);
91
+      expect(element.all(by.css(".pod-running")).count()).toEqual(3);
92 92
     });
93 93
   }); 
94 94
 
95 95
new file mode 100644
96 96
Binary files /dev/null and b/cpu.pprof differ
... ...
@@ -165,10 +165,10 @@
165 165
           "replicaSelector": {
166 166
             "name": "frontend"
167 167
           },
168
-          "replicas": 1
168
+          "replicas": 2
169 169
         },
170 170
         "strategy": {
171
-          "type": "Recreate"
171
+          "type": "Rolling"
172 172
         }
173 173
       },
174 174
       "triggers": [
... ...
@@ -158,10 +158,10 @@
158 158
           "replicaSelector": {
159 159
             "name": "frontend"
160 160
           },
161
-          "replicas": 1
161
+          "replicas": 2
162 162
         },
163 163
         "strategy": {
164
-          "type": "Recreate"
164
+          "type": "Rolling"
165 165
         }
166 166
       },
167 167
       "triggers": [
... ...
@@ -158,42 +158,10 @@
158 158
           "replicaSelector": {
159 159
             "name": "frontend"
160 160
           },
161
-          "replicas": 1
161
+          "replicas": 2
162 162
         },
163 163
         "strategy": {
164
-          "type": "Recreate",
165
-          "recreateParams": {
166
-            "pre": {
167
-              "failurePolicy": "Abort",
168
-              "execNewPod": {
169
-                "containerName": "ruby-helloworld",
170
-                "command": [
171
-                  "/bin/true"
172
-                ],
173
-                "env": [
174
-                  {
175
-                    "name": "CUSTOM_VAR1",
176
-                    "value": "custom_value1"
177
-                  }
178
-                ]
179
-              }
180
-            },
181
-            "post": {
182
-              "failurePolicy": "Ignore",
183
-              "execNewPod": {
184
-                "containerName": "ruby-helloworld",
185
-                "command": [
186
-                  "/bin/false"
187
-                ],
188
-                "env": [
189
-                  {
190
-                    "name": "CUSTOM_VAR2",
191
-                    "value": "custom_value2"
192
-                  }
193
-                ]
194
-              }
195
-            }
196
-          }
164
+          "type": "Rolling"
197 165
         }
198 166
       },
199 167
       "triggers": [
... ...
@@ -105,9 +105,26 @@ func fuzzInternalObject(t *testing.T, forVersion string, item runtime.Object, se
105 105
 		},
106 106
 		func(j *deploy.DeploymentStrategy, c fuzz.Continue) {
107 107
 			c.FuzzNoCustom(j)
108
-			// TODO: we should not have to set defaults, instead we should be able to detect defaults were applied.
109
-			if len(j.Type) == 0 {
108
+			mkintp := func(i int) *int64 {
109
+				v := int64(i)
110
+				return &v
111
+			}
112
+			switch c.Intn(3) {
113
+			case 0:
114
+				// TODO: we should not have to set defaults, instead we should be able
115
+				// to detect defaults were applied.
116
+				j.Type = deploy.DeploymentStrategyTypeRolling
117
+				j.RollingParams = &deploy.RollingDeploymentStrategyParams{
118
+					IntervalSeconds:     mkintp(1),
119
+					UpdatePeriodSeconds: mkintp(1),
120
+					TimeoutSeconds:      mkintp(120),
121
+				}
122
+			case 1:
110 123
 				j.Type = deploy.DeploymentStrategyTypeRecreate
124
+				j.RollingParams = nil
125
+			case 2:
126
+				j.Type = deploy.DeploymentStrategyTypeCustom
127
+				j.RollingParams = nil
111 128
 			}
112 129
 		},
113 130
 		func(j *deploy.DeploymentCauseImageTrigger, c fuzz.Continue) {
... ...
@@ -14,7 +14,9 @@ import (
14 14
 	"github.com/openshift/origin/pkg/cmd/util"
15 15
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
16 16
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
17
-	strategy "github.com/openshift/origin/pkg/deploy/strategy/recreate"
17
+	"github.com/openshift/origin/pkg/deploy/strategy"
18
+	"github.com/openshift/origin/pkg/deploy/strategy/recreate"
19
+	"github.com/openshift/origin/pkg/deploy/strategy/rolling"
18 20
 	deployutil "github.com/openshift/origin/pkg/deploy/util"
19 21
 	"github.com/openshift/origin/pkg/version"
20 22
 )
... ...
@@ -78,21 +80,35 @@ func NewCommandDeployer(name string) *cobra.Command {
78 78
 
79 79
 // deploy executes a deployment strategy.
80 80
 func deploy(kClient kclient.Interface, namespace, deploymentName string) error {
81
-	newDeployment, oldDeployments, err := getDeployerContext(&realReplicationControllerGetter{kClient}, namespace, deploymentName)
82
-
81
+	deployment, oldDeployments, err := getDeployerContext(&realReplicationControllerGetter{kClient}, namespace, deploymentName)
83 82
 	if err != nil {
84 83
 		return err
85 84
 	}
86 85
 
87
-	// TODO: Choose a strategy based on some input
88
-	strategy := strategy.NewRecreateDeploymentStrategy(kClient, latest.Codec)
89
-	return strategy.Deploy(newDeployment, oldDeployments)
86
+	config, err := deployutil.DecodeDeploymentConfig(deployment, latest.Codec)
87
+	if err != nil {
88
+		return fmt.Errorf("couldn't decode DeploymentConfig from deployment %s/%s: %v", deployment.Namespace, deployment.Name, err)
89
+	}
90
+
91
+	var strategy strategy.DeploymentStrategy
92
+
93
+	switch config.Template.Strategy.Type {
94
+	case deployapi.DeploymentStrategyTypeRecreate:
95
+		strategy = recreate.NewRecreateDeploymentStrategy(kClient, latest.Codec)
96
+	case deployapi.DeploymentStrategyTypeRolling:
97
+		recreate := recreate.NewRecreateDeploymentStrategy(kClient, latest.Codec)
98
+		strategy = rolling.NewRollingDeploymentStrategy(deployment.Namespace, kClient, latest.Codec, recreate)
99
+	default:
100
+		return fmt.Errorf("unsupported strategy type: %s", config.Template.Strategy.Type)
101
+	}
102
+
103
+	return strategy.Deploy(deployment, oldDeployments)
90 104
 }
91 105
 
92 106
 // getDeployerContext finds the target deployment and any deployments it considers to be prior to the
93 107
 // target deployment. Only deployments whose LatestVersion is less than the target deployment are
94 108
 // considered to be prior.
95
-func getDeployerContext(controllerGetter replicationControllerGetter, namespace, deploymentName string) (*kapi.ReplicationController, []kapi.ObjectReference, error) {
109
+func getDeployerContext(controllerGetter replicationControllerGetter, namespace, deploymentName string) (*kapi.ReplicationController, []*kapi.ReplicationController, error) {
96 110
 	var err error
97 111
 	var newDeployment *kapi.ReplicationController
98 112
 	var newConfig *deployapi.DeploymentConfig
... ...
@@ -112,14 +128,14 @@ func getDeployerContext(controllerGetter replicationControllerGetter, namespace,
112 112
 	// encoded DeploymentConfigs to the new one by LatestVersion. Treat a failure to interpret a given
113 113
 	// old deployment as a fatal error to prevent overlapping deployments.
114 114
 	var allControllers *kapi.ReplicationControllerList
115
-	oldDeployments := []kapi.ObjectReference{}
115
+	oldDeployments := []*kapi.ReplicationController{}
116 116
 
117 117
 	if allControllers, err = controllerGetter.List(newDeployment.Namespace, labels.Everything()); err != nil {
118 118
 		return nil, nil, fmt.Errorf("Unable to get list replication controllers in deployment namespace %s: %v", newDeployment.Namespace, err)
119 119
 	}
120 120
 
121 121
 	glog.Infof("Inspecting %d potential prior deployments", len(allControllers.Items))
122
-	for _, controller := range allControllers.Items {
122
+	for i, controller := range allControllers.Items {
123 123
 		if configName, hasConfigName := controller.Annotations[deployapi.DeploymentConfigAnnotation]; !hasConfigName {
124 124
 			glog.Infof("Disregarding replicationController %s (not a deployment)", controller.Name)
125 125
 			continue
... ...
@@ -135,10 +151,7 @@ func getDeployerContext(controllerGetter replicationControllerGetter, namespace,
135 135
 
136 136
 		if oldVersion < newConfig.LatestVersion {
137 137
 			glog.Infof("Marking deployment %s as a prior deployment", controller.Name)
138
-			oldDeployments = append(oldDeployments, kapi.ObjectReference{
139
-				Namespace: controller.Namespace,
140
-				Name:      controller.Name,
141
-			})
138
+			oldDeployments = append(oldDeployments, &allControllers.Items[i])
142 139
 		} else {
143 140
 			glog.Infof("Disregarding deployment %s (same as or newer than target)", controller.Name)
144 141
 		}
... ...
@@ -96,13 +96,13 @@ func TestGetDeploymentContextNoPriorDeployments(t *testing.T) {
96 96
 func TestGetDeploymentContextWithPriorDeployments(t *testing.T) {
97 97
 	getter := &testReplicationControllerGetter{
98 98
 		getFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
99
-			deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(2), kapi.Codec)
99
+			deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(3), kapi.Codec)
100 100
 			return deployment, nil
101 101
 		},
102 102
 		listFunc: func(namespace string, selector labels.Selector) (*kapi.ReplicationControllerList, error) {
103 103
 			deployment1, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)
104 104
 			deployment2, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(2), kapi.Codec)
105
-			deployment3, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(3), kapi.Codec)
105
+			deployment3, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(4), kapi.Codec)
106 106
 			deployment4, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)
107 107
 			deployment4.Annotations[deployapi.DeploymentConfigAnnotation] = "another-config"
108 108
 			return &kapi.ReplicationControllerList{
... ...
@@ -131,8 +131,30 @@ func TestGetDeploymentContextWithPriorDeployments(t *testing.T) {
131 131
 		t.Fatal("expected non-nil oldDeployments")
132 132
 	}
133 133
 
134
-	if e, a := 1, len(oldDeployments); e != a {
135
-		t.Fatalf("expected oldDeployments with size %d, got %d: %#v", e, a, oldDeployments)
134
+	expected := []string{"config-1", "config-2"}
135
+	for _, e := range expected {
136
+		found := false
137
+		for _, d := range oldDeployments {
138
+			if d.Name == e {
139
+				found = true
140
+				break
141
+			}
142
+		}
143
+		if !found {
144
+			t.Errorf("expected to find old deployment %s", e)
145
+		}
146
+	}
147
+	for _, d := range oldDeployments {
148
+		ok := false
149
+		for _, e := range expected {
150
+			if d.Name == e {
151
+				ok = true
152
+				break
153
+			}
154
+		}
155
+		if !ok {
156
+			t.Errorf("unexpected old deployment %s", d.Name)
157
+		}
136 158
 	}
137 159
 }
138 160
 
... ...
@@ -837,10 +837,10 @@ func (c *MasterConfig) RunDeploymentController() error {
837 837
 	env = append(env, clientcmd.EnvVarsFromConfig(c.DeployerClientConfig())...)
838 838
 
839 839
 	factory := deploycontroller.DeploymentControllerFactory{
840
-		KubeClient:            kclient,
841
-		Codec:                 latest.Codec,
842
-		Environment:           env,
843
-		RecreateStrategyImage: c.ImageFor("deployer"),
840
+		KubeClient:    kclient,
841
+		Codec:         latest.Codec,
842
+		Environment:   env,
843
+		DeployerImage: c.ImageFor("deployer"),
844 844
 	}
845 845
 
846 846
 	controller := factory.Create()
... ...
@@ -54,6 +54,8 @@ type DeploymentStrategy struct {
54 54
 	CustomParams *CustomDeploymentStrategyParams `json:"customParams,omitempty"`
55 55
 	// RecreateParams are the input to the Recreate deployment strategy.
56 56
 	RecreateParams *RecreateDeploymentStrategyParams `json:"recreateParams,omitempty"`
57
+	// RollingParams are the input to the Rolling deployment strategy.
58
+	RollingParams *RollingDeploymentStrategyParams `json:"rollingParams,omitempty"`
57 59
 	// Compute resource requirements to execute the deployment
58 60
 	Resources kapi.ResourceRequirements `json:"resources,omitempty"`
59 61
 }
... ...
@@ -66,6 +68,8 @@ const (
66 66
 	DeploymentStrategyTypeRecreate DeploymentStrategyType = "Recreate"
67 67
 	// DeploymentStrategyTypeCustom is a user defined strategy.
68 68
 	DeploymentStrategyTypeCustom DeploymentStrategyType = "Custom"
69
+	// DeploymentStrategyTypeRolling uses the Kubernetes RollingUpdater.
70
+	DeploymentStrategyTypeRolling DeploymentStrategyType = "Rolling"
69 71
 )
70 72
 
71 73
 // CustomDeploymentStrategyParams are the input to the Custom deployment strategy.
... ...
@@ -123,6 +127,20 @@ type ExecNewPodHook struct {
123 123
 	ContainerName string `json:"containerName"`
124 124
 }
125 125
 
126
+// RollingDeploymentStrategyParams are the input to the Rolling deployment
127
+// strategy.
128
+type RollingDeploymentStrategyParams struct {
129
+	// UpdatePeriodSeconds is the time to wait between individual pod updates.
130
+	// If the value is nil, a default will be used.
131
+	UpdatePeriodSeconds *int64 `json:"updatePeriodSeconds,omitempty" description:"the time to wait between individual pod updates"`
132
+	// IntervalSeconds is the time to wait between polling deployment status
133
+	// after update. If the value is nil, a default will be used.
134
+	IntervalSeconds *int64 `json:"intervalSeconds,omitempty" description:"the time to wait between polling deployment status after update"`
135
+	// TimeoutSeconds is the time to wait for updates before giving up. If the
136
+	// value is nil, a default will be used.
137
+	TimeoutSeconds *int64 `json:"timeoutSeconds,omitempty" description:"the time to wait for updates before giving up"`
138
+}
139
+
126 140
 // DeploymentList is a collection of deployments.
127 141
 // DEPRECATED: Like Deployment, this is no longer used.
128 142
 type DeploymentList struct {
... ...
@@ -19,6 +19,9 @@ func init() {
19 19
 			if err := s.Convert(&in.RecreateParams, &out.RecreateParams, 0); err != nil {
20 20
 				return err
21 21
 			}
22
+			if err := s.Convert(&in.RollingParams, &out.RollingParams, 0); err != nil {
23
+				return err
24
+			}
22 25
 			if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil {
23 26
 				return err
24 27
 			}
... ...
@@ -34,6 +37,9 @@ func init() {
34 34
 			if err := s.Convert(&in.RecreateParams, &out.RecreateParams, 0); err != nil {
35 35
 				return err
36 36
 			}
37
+			if err := s.Convert(&in.RollingParams, &out.RollingParams, 0); err != nil {
38
+				return err
39
+			}
37 40
 			if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil {
38 41
 				return err
39 42
 			}
40 43
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+package v1beta1
1
+
2
+import (
3
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
4
+
5
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
6
+)
7
+
8
+func init() {
9
+	mkintp := func(i int) *int64 {
10
+		v := int64(i)
11
+		return &v
12
+	}
13
+
14
+	err := api.Scheme.AddDefaultingFuncs(
15
+		func(obj *deployapi.DeploymentStrategy) {
16
+			if len(obj.Type) == 0 {
17
+				obj.Type = deployapi.DeploymentStrategyTypeRolling
18
+			}
19
+
20
+			if obj.Type == deployapi.DeploymentStrategyTypeRolling && obj.RollingParams == nil {
21
+				obj.RollingParams = &deployapi.RollingDeploymentStrategyParams{
22
+					IntervalSeconds:     mkintp(1),
23
+					UpdatePeriodSeconds: mkintp(1),
24
+					TimeoutSeconds:      mkintp(120),
25
+				}
26
+			}
27
+		},
28
+		func(obj *deployapi.RollingDeploymentStrategyParams) {
29
+			if obj.IntervalSeconds == nil {
30
+				obj.IntervalSeconds = mkintp(1)
31
+			}
32
+
33
+			if obj.UpdatePeriodSeconds == nil {
34
+				obj.UpdatePeriodSeconds = mkintp(1)
35
+			}
36
+
37
+			if obj.TimeoutSeconds == nil {
38
+				obj.TimeoutSeconds = mkintp(120)
39
+			}
40
+		},
41
+	)
42
+	if err != nil {
43
+		panic(err)
44
+	}
45
+}
0 46
new file mode 100644
... ...
@@ -0,0 +1,53 @@
0
+package v1beta1_test
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
7
+
8
+	newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
9
+	current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
10
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
11
+)
12
+
13
+func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
14
+	data, err := current.Codec.Encode(obj)
15
+	if err != nil {
16
+		t.Errorf("%v\n %#v", err, obj)
17
+		return nil
18
+	}
19
+	obj2, err := newer.Codec.Decode(data)
20
+	if err != nil {
21
+		t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj)
22
+		return nil
23
+	}
24
+	obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object)
25
+	err = newer.Scheme.Convert(obj2, obj3)
26
+	if err != nil {
27
+		t.Errorf("%v\nSource: %#v", err, obj2)
28
+		return nil
29
+	}
30
+	return obj3
31
+}
32
+
33
+func TestDefaults_rollingParams(t *testing.T) {
34
+	c := &deployapi.DeploymentConfig{
35
+		Template: deployapi.DeploymentTemplate{},
36
+	}
37
+	o := roundTrip(t, runtime.Object(c))
38
+	config := o.(*deployapi.DeploymentConfig)
39
+	strat := config.Template.Strategy
40
+	if e, a := deployapi.DeploymentStrategyTypeRolling, strat.Type; e != a {
41
+		t.Errorf("expected strategy type %s, got %s", e, a)
42
+	}
43
+	if e, a := int64(1), *strat.RollingParams.UpdatePeriodSeconds; e != a {
44
+		t.Errorf("expected UpdatePeriodSeconds %d, got %d", e, a)
45
+	}
46
+	if e, a := int64(1), *strat.RollingParams.IntervalSeconds; e != a {
47
+		t.Errorf("expected IntervalSeconds %d, got %d", e, a)
48
+	}
49
+	if e, a := int64(120), *strat.RollingParams.TimeoutSeconds; e != a {
50
+		t.Errorf("expected UpdatePeriodSeconds %d, got %d", e, a)
51
+	}
52
+}
... ...
@@ -55,6 +55,8 @@ type DeploymentStrategy struct {
55 55
 	CustomParams *CustomDeploymentStrategyParams `json:"customParams,omitempty"`
56 56
 	// RecreateParams are the input to the Recreate deployment strategy.
57 57
 	RecreateParams *RecreateDeploymentStrategyParams `json:"recreateParams,omitempty"`
58
+	// RollingParams are the input to the Rolling deployment strategy.
59
+	RollingParams *RollingDeploymentStrategyParams `json:"rollingParams,omitempty"`
58 60
 	// Compute resource requirements to execute the deployment
59 61
 	Resources kapi.ResourceRequirements `json:"resources,omitempty"`
60 62
 }
... ...
@@ -67,6 +69,8 @@ const (
67 67
 	DeploymentStrategyTypeRecreate DeploymentStrategyType = "Recreate"
68 68
 	// DeploymentStrategyTypeCustom is a user defined strategy.
69 69
 	DeploymentStrategyTypeCustom DeploymentStrategyType = "Custom"
70
+	// DeploymentStrategyTypeRolling uses the Kubernetes RollingUpdater.
71
+	DeploymentStrategyTypeRolling DeploymentStrategyType = "Rolling"
70 72
 )
71 73
 
72 74
 // CustomParams are the input to the Custom deployment strategy.
... ...
@@ -124,6 +128,20 @@ type ExecNewPodHook struct {
124 124
 	ContainerName string `json:"containerName"`
125 125
 }
126 126
 
127
+// RollingDeploymentStrategyParams are the input to the Rolling deployment
128
+// strategy.
129
+type RollingDeploymentStrategyParams struct {
130
+	// UpdatePeriodSeconds is the time to wait between individual pod updates.
131
+	// If the value is nil, a default will be used.
132
+	UpdatePeriodSeconds *int64 `json:"updatePeriodSeconds,omitempty" description:"the time to wait between individual pod updates"`
133
+	// IntervalSeconds is the time to wait between polling deployment status
134
+	// after update. If the value is nil, a default will be used.
135
+	IntervalSeconds *int64 `json:"intervalSeconds,omitempty" description:"the time to wait between polling deployment status after update"`
136
+	// TimeoutSeconds is the time to wait for updates before giving up. If the
137
+	// value is nil, a default will be used.
138
+	TimeoutSeconds *int64 `json:"timeoutSeconds,omitempty" description:"the time to wait for updates before giving up"`
139
+}
140
+
127 141
 // A DeploymentList is a collection of deployments.
128 142
 // DEPRECATED: Like Deployment, this is no longer used.
129 143
 type DeploymentList struct {
... ...
@@ -93,6 +93,9 @@ func init() {
93 93
 			if err := s.Convert(&in.RecreateParams, &out.RecreateParams, 0); err != nil {
94 94
 				return err
95 95
 			}
96
+			if err := s.Convert(&in.RollingParams, &out.RollingParams, 0); err != nil {
97
+				return err
98
+			}
96 99
 			if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil {
97 100
 				return err
98 101
 			}
... ...
@@ -108,6 +111,9 @@ func init() {
108 108
 			if err := s.Convert(&in.RecreateParams, &out.RecreateParams, 0); err != nil {
109 109
 				return err
110 110
 			}
111
+			if err := s.Convert(&in.RollingParams, &out.RollingParams, 0); err != nil {
112
+				return err
113
+			}
111 114
 			if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil {
112 115
 				return err
113 116
 			}
114 117
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+package v1beta3
1
+
2
+import (
3
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
4
+
5
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
6
+)
7
+
8
+func init() {
9
+	mkintp := func(i int) *int64 {
10
+		v := int64(i)
11
+		return &v
12
+	}
13
+
14
+	err := api.Scheme.AddDefaultingFuncs(
15
+		func(obj *deployapi.DeploymentStrategy) {
16
+			if len(obj.Type) == 0 {
17
+				obj.Type = deployapi.DeploymentStrategyTypeRolling
18
+			}
19
+
20
+			if obj.Type == deployapi.DeploymentStrategyTypeRolling && obj.RollingParams == nil {
21
+				obj.RollingParams = &deployapi.RollingDeploymentStrategyParams{
22
+					IntervalSeconds:     mkintp(1),
23
+					UpdatePeriodSeconds: mkintp(1),
24
+					TimeoutSeconds:      mkintp(120),
25
+				}
26
+			}
27
+		},
28
+		func(obj *deployapi.RollingDeploymentStrategyParams) {
29
+			if obj.IntervalSeconds == nil {
30
+				obj.IntervalSeconds = mkintp(1)
31
+			}
32
+
33
+			if obj.UpdatePeriodSeconds == nil {
34
+				obj.UpdatePeriodSeconds = mkintp(1)
35
+			}
36
+
37
+			if obj.TimeoutSeconds == nil {
38
+				obj.TimeoutSeconds = mkintp(120)
39
+			}
40
+		},
41
+	)
42
+	if err != nil {
43
+		panic(err)
44
+	}
45
+}
0 46
new file mode 100644
... ...
@@ -0,0 +1,53 @@
0
+package v1beta3_test
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
7
+
8
+	newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
9
+	current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3"
10
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
11
+)
12
+
13
+func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
14
+	data, err := current.Codec.Encode(obj)
15
+	if err != nil {
16
+		t.Errorf("%v\n %#v", err, obj)
17
+		return nil
18
+	}
19
+	obj2, err := newer.Codec.Decode(data)
20
+	if err != nil {
21
+		t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj)
22
+		return nil
23
+	}
24
+	obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object)
25
+	err = newer.Scheme.Convert(obj2, obj3)
26
+	if err != nil {
27
+		t.Errorf("%v\nSource: %#v", err, obj2)
28
+		return nil
29
+	}
30
+	return obj3
31
+}
32
+
33
+func TestDefaults_rollingParams(t *testing.T) {
34
+	c := &deployapi.DeploymentConfig{
35
+		Template: deployapi.DeploymentTemplate{},
36
+	}
37
+	o := roundTrip(t, runtime.Object(c))
38
+	config := o.(*deployapi.DeploymentConfig)
39
+	strat := config.Template.Strategy
40
+	if e, a := deployapi.DeploymentStrategyTypeRolling, strat.Type; e != a {
41
+		t.Errorf("expected strategy type %s, got %s", e, a)
42
+	}
43
+	if e, a := int64(1), *strat.RollingParams.UpdatePeriodSeconds; e != a {
44
+		t.Errorf("expected UpdatePeriodSeconds %d, got %d", e, a)
45
+	}
46
+	if e, a := int64(1), *strat.RollingParams.IntervalSeconds; e != a {
47
+		t.Errorf("expected IntervalSeconds %d, got %d", e, a)
48
+	}
49
+	if e, a := int64(120), *strat.RollingParams.TimeoutSeconds; e != a {
50
+		t.Errorf("expected UpdatePeriodSeconds %d, got %d", e, a)
51
+	}
52
+}
... ...
@@ -30,6 +30,8 @@ type DeploymentStrategy struct {
30 30
 	CustomParams *CustomDeploymentStrategyParams `json:"customParams,omitempty"`
31 31
 	// RecreateParams are the input to the Recreate deployment strategy.
32 32
 	RecreateParams *RecreateDeploymentStrategyParams `json:"recreateParams,omitempty"`
33
+	// RollingParams are the input to the Rolling deployment strategy.
34
+	RollingParams *RollingDeploymentStrategyParams `json:"rollingParams,omitempty"`
33 35
 	// Compute resource requirements to execute the deployment
34 36
 	Resources kapi.ResourceRequirements `json:"resources,omitempty"`
35 37
 }
... ...
@@ -42,6 +44,8 @@ const (
42 42
 	DeploymentStrategyTypeRecreate DeploymentStrategyType = "Recreate"
43 43
 	// DeploymentStrategyTypeCustom is a user defined strategy.
44 44
 	DeploymentStrategyTypeCustom DeploymentStrategyType = "Custom"
45
+	// DeploymentStrategyTypeRolling uses the Kubernetes RollingUpdater.
46
+	DeploymentStrategyTypeRolling DeploymentStrategyType = "Rolling"
45 47
 )
46 48
 
47 49
 // CustomParams are the input to the Custom deployment strategy.
... ...
@@ -99,6 +103,20 @@ type ExecNewPodHook struct {
99 99
 	ContainerName string `json:"containerName"`
100 100
 }
101 101
 
102
+// RollingDeploymentStrategyParams are the input to the Rolling deployment
103
+// strategy.
104
+type RollingDeploymentStrategyParams struct {
105
+	// UpdatePeriodSeconds is the time to wait between individual pod updates.
106
+	// If the value is nil, a default will be used.
107
+	UpdatePeriodSeconds *int64 `json:"updatePeriodSeconds,omitempty" description:"the time to wait between individual pod updates"`
108
+	// IntervalSeconds is the time to wait between polling deployment status
109
+	// after update. If the value is nil, a default will be used.
110
+	IntervalSeconds *int64 `json:"intervalSeconds,omitempty" description:"the time to wait between polling deployment status after update"`
111
+	// TimeoutSeconds is the time to wait for updates before giving up. If the
112
+	// value is nil, a default will be used.
113
+	TimeoutSeconds *int64 `json:"timeoutSeconds,omitempty" description:"the time to wait for updates before giving up"`
114
+}
115
+
102 116
 // These constants represent keys used for correlating objects related to deployments.
103 117
 const (
104 118
 	// DeploymentConfigAnnotation is an annotation name used to correlate a deployment with the
... ...
@@ -82,6 +82,12 @@ func validateDeploymentStrategy(strategy *deployapi.DeploymentStrategy) fielderr
82 82
 		if strategy.RecreateParams != nil {
83 83
 			errs = append(errs, validateRecreateParams(strategy.RecreateParams).Prefix("recreateParams")...)
84 84
 		}
85
+	case deployapi.DeploymentStrategyTypeRolling:
86
+		if strategy.RollingParams == nil {
87
+			errs = append(errs, fielderrors.NewFieldRequired("rollingParams"))
88
+		} else {
89
+			errs = append(errs, validateRollingParams(strategy.RollingParams).Prefix("rollingParams")...)
90
+		}
85 91
 	case deployapi.DeploymentStrategyTypeCustom:
86 92
 		if strategy.CustomParams == nil {
87 93
 			errs = append(errs, fielderrors.NewFieldRequired("customParams"))
... ...
@@ -168,6 +174,24 @@ func validateEnv(vars []kapi.EnvVar) fielderrors.ValidationErrorList {
168 168
 	return allErrs
169 169
 }
170 170
 
171
+func validateRollingParams(params *deployapi.RollingDeploymentStrategyParams) fielderrors.ValidationErrorList {
172
+	errs := fielderrors.ValidationErrorList{}
173
+
174
+	if params.IntervalSeconds != nil && *params.IntervalSeconds < 1 {
175
+		errs = append(errs, fielderrors.NewFieldInvalid("intervalSeconds", *params.IntervalSeconds, "must be >0"))
176
+	}
177
+
178
+	if params.UpdatePeriodSeconds != nil && *params.UpdatePeriodSeconds < 1 {
179
+		errs = append(errs, fielderrors.NewFieldInvalid("updatePeriodSeconds", *params.UpdatePeriodSeconds, "must be >0"))
180
+	}
181
+
182
+	if params.TimeoutSeconds != nil && *params.TimeoutSeconds < 1 {
183
+		errs = append(errs, fielderrors.NewFieldInvalid("timeoutSeconds", *params.TimeoutSeconds, "must be >0"))
184
+	}
185
+
186
+	return errs
187
+}
188
+
171 189
 func validateTrigger(trigger *deployapi.DeploymentTriggerPolicy) fielderrors.ValidationErrorList {
172 190
 	errs := fielderrors.ValidationErrorList{}
173 191
 
... ...
@@ -328,6 +328,63 @@ func TestValidateDeploymentConfigMissingFields(t *testing.T) {
328 328
 			fielderrors.ValidationErrorTypeRequired,
329 329
 			"template.strategy.recreateParams.pre.execNewPod.containerName",
330 330
 		},
331
+		"invalid template.strategy.rollingParams.intervalSeconds": {
332
+			api.DeploymentConfig{
333
+				ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"},
334
+				Triggers:   manualTrigger(),
335
+				Template: api.DeploymentTemplate{
336
+					Strategy: api.DeploymentStrategy{
337
+						Type: api.DeploymentStrategyTypeRolling,
338
+						RollingParams: &api.RollingDeploymentStrategyParams{
339
+							IntervalSeconds:     mkintp(-20),
340
+							UpdatePeriodSeconds: mkintp(1),
341
+							TimeoutSeconds:      mkintp(1),
342
+						},
343
+					},
344
+					ControllerTemplate: test.OkControllerTemplate(),
345
+				},
346
+			},
347
+			fielderrors.ValidationErrorTypeInvalid,
348
+			"template.strategy.rollingParams.intervalSeconds",
349
+		},
350
+		"invalid template.strategy.rollingParams.updatePeriodSeconds": {
351
+			api.DeploymentConfig{
352
+				ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"},
353
+				Triggers:   manualTrigger(),
354
+				Template: api.DeploymentTemplate{
355
+					Strategy: api.DeploymentStrategy{
356
+						Type: api.DeploymentStrategyTypeRolling,
357
+						RollingParams: &api.RollingDeploymentStrategyParams{
358
+							IntervalSeconds:     mkintp(1),
359
+							UpdatePeriodSeconds: mkintp(-20),
360
+							TimeoutSeconds:      mkintp(1),
361
+						},
362
+					},
363
+					ControllerTemplate: test.OkControllerTemplate(),
364
+				},
365
+			},
366
+			fielderrors.ValidationErrorTypeInvalid,
367
+			"template.strategy.rollingParams.updatePeriodSeconds",
368
+		},
369
+		"invalid template.strategy.rollingParams.timeoutSeconds": {
370
+			api.DeploymentConfig{
371
+				ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"},
372
+				Triggers:   manualTrigger(),
373
+				Template: api.DeploymentTemplate{
374
+					Strategy: api.DeploymentStrategy{
375
+						Type: api.DeploymentStrategyTypeRolling,
376
+						RollingParams: &api.RollingDeploymentStrategyParams{
377
+							IntervalSeconds:     mkintp(1),
378
+							UpdatePeriodSeconds: mkintp(1),
379
+							TimeoutSeconds:      mkintp(-20),
380
+						},
381
+					},
382
+					ControllerTemplate: test.OkControllerTemplate(),
383
+				},
384
+			},
385
+			fielderrors.ValidationErrorTypeInvalid,
386
+			"template.strategy.rollingParams.timeoutSeconds",
387
+		},
331 388
 	}
332 389
 
333 390
 	for k, v := range errorCases {
... ...
@@ -464,3 +521,8 @@ func TestValidateDeploymentConfigImageRepositorySupported(t *testing.T) {
464 464
 		t.Errorf("expected imageChangeParams.from.kind %s, got %s", e, a)
465 465
 	}
466 466
 }
467
+
468
+func mkintp(i int) *int64 {
469
+	v := int64(i)
470
+	return &v
471
+}
... ...
@@ -28,8 +28,8 @@ type DeploymentControllerFactory struct {
28 28
 	Codec runtime.Codec
29 29
 	// Environment is a set of environment which should be injected into all deployer pod containers.
30 30
 	Environment []kapi.EnvVar
31
-	// RecreateStrategyImage specifies which Docker image which should implement the Recreate strategy.
32
-	RecreateStrategyImage string
31
+	// DeployerImage specifies which Docker image can support the default strategies.
32
+	DeployerImage string
33 33
 }
34 34
 
35 35
 // Create creates a DeploymentController.
... ...
@@ -100,9 +100,9 @@ func (factory *DeploymentControllerFactory) Create() controller.RunnableControll
100 100
 
101 101
 // makeContainer creates containers in the following way:
102 102
 //
103
-//   1. For the Recreate strategy, use the factory's RecreateStrategyImage as
104
-//      the container image, and the factory's Environment as the container
105
-//      environment.
103
+//   1. For the Recreate and Rolling strategies, strategy, use the factory's
104
+//      DeployerImage as the container image, and the factory's Environment
105
+//      as the container environment.
106 106
 //   2. For all Custom strategy, use the strategy's image for the container
107 107
 //      image, and use the combination of the factory's Environment and the
108 108
 //      strategy's environment as the container environment.
... ...
@@ -117,10 +117,10 @@ func (factory *DeploymentControllerFactory) makeContainer(strategy *deployapi.De
117 117
 
118 118
 	// Every strategy type should be handled here.
119 119
 	switch strategy.Type {
120
-	case deployapi.DeploymentStrategyTypeRecreate:
120
+	case deployapi.DeploymentStrategyTypeRecreate, deployapi.DeploymentStrategyTypeRolling:
121 121
 		// Use the factory-configured image.
122 122
 		return &kapi.Container{
123
-			Image: factory.RecreateStrategyImage,
123
+			Image: factory.DeployerImage,
124 124
 			Env:   environment,
125 125
 		}, nil
126 126
 	case deployapi.DeploymentStrategyTypeCustom:
127 127
new file mode 100644
... ...
@@ -0,0 +1,11 @@
0
+package strategy
1
+
2
+import (
3
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
4
+)
5
+
6
+// DeploymentStrategy knows how to make a deployment active.
7
+type DeploymentStrategy interface {
8
+	// Deploy makes a deployment active.
9
+	Deploy(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error
10
+}
... ...
@@ -58,7 +58,7 @@ func NewRecreateDeploymentStrategy(client kclient.Interface, codec runtime.Codec
58 58
 }
59 59
 
60 60
 // Deploy makes deployment active and disables oldDeployments.
61
-func (s *RecreateDeploymentStrategy) Deploy(deployment *kapi.ReplicationController, oldDeployments []kapi.ObjectReference) error {
61
+func (s *RecreateDeploymentStrategy) Deploy(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error {
62 62
 	var err error
63 63
 	var deploymentConfig *deployapi.DeploymentConfig
64 64
 
... ...
@@ -40,7 +40,7 @@ func TestRecreate_initialDeployment(t *testing.T) {
40 40
 	// Deployment replicas should follow the config as there's no explicit
41 41
 	// desired annotation.
42 42
 	deployment, _ = deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)
43
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
43
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
44 44
 	if err != nil {
45 45
 		t.Fatalf("unexpected deploy error: %#v", err)
46 46
 	}
... ...
@@ -51,7 +51,7 @@ func TestRecreate_initialDeployment(t *testing.T) {
51 51
 	// Deployment replicas should follow the explicit annotation.
52 52
 	deployment, _ = deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)
53 53
 	deployment.Annotations[deployapi.DesiredReplicasAnnotation] = "2"
54
-	err = strategy.Deploy(deployment, []kapi.ObjectReference{})
54
+	err = strategy.Deploy(deployment, []*kapi.ReplicationController{})
55 55
 	if err != nil {
56 56
 		t.Fatalf("unexpected deploy error: %#v", err)
57 57
 	}
... ...
@@ -63,7 +63,7 @@ func TestRecreate_initialDeployment(t *testing.T) {
63 63
 	// invalid.
64 64
 	deployment, _ = deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)
65 65
 	deployment.Annotations[deployapi.DesiredReplicasAnnotation] = "invalid"
66
-	err = strategy.Deploy(deployment, []kapi.ObjectReference{})
66
+	err = strategy.Deploy(deployment, []*kapi.ReplicationController{})
67 67
 	if err != nil {
68 68
 		t.Fatalf("unexpected deploy error: %#v", err)
69 69
 	}
... ...
@@ -112,12 +112,7 @@ func TestRecreate_secondDeploymentWithSuccessfulRetries(t *testing.T) {
112 112
 		},
113 113
 	}
114 114
 
115
-	err := strategy.Deploy(newDeployment, []kapi.ObjectReference{
116
-		{
117
-			Namespace: oldDeployment.Namespace,
118
-			Name:      oldDeployment.Name,
119
-		},
120
-	})
115
+	err := strategy.Deploy(newDeployment, []*kapi.ReplicationController{oldDeployment})
121 116
 
122 117
 	if err != nil {
123 118
 		t.Fatalf("unexpected deploy error: %#v", err)
... ...
@@ -165,12 +160,7 @@ func TestRecreate_secondDeploymentScaleUpRetries(t *testing.T) {
165 165
 		},
166 166
 	}
167 167
 
168
-	err := strategy.Deploy(newDeployment, []kapi.ObjectReference{
169
-		{
170
-			Namespace: oldDeployment.Namespace,
171
-			Name:      oldDeployment.Name,
172
-		},
173
-	})
168
+	err := strategy.Deploy(newDeployment, []*kapi.ReplicationController{oldDeployment})
174 169
 
175 170
 	if err == nil {
176 171
 		t.Fatalf("expected a deploy error: %#v", err)
... ...
@@ -222,12 +212,7 @@ func TestRecreate_secondDeploymentScaleDownRetries(t *testing.T) {
222 222
 		},
223 223
 	}
224 224
 
225
-	err := strategy.Deploy(newDeployment, []kapi.ObjectReference{
226
-		{
227
-			Namespace: oldDeployment.Namespace,
228
-			Name:      oldDeployment.Name,
229
-		},
230
-	})
225
+	err := strategy.Deploy(newDeployment, []*kapi.ReplicationController{oldDeployment})
231 226
 
232 227
 	if err == nil {
233 228
 		t.Fatalf("expected a deploy error: %#v", err)
... ...
@@ -264,7 +249,7 @@ func TestRecreate_deploymentPreHookSuccess(t *testing.T) {
264 264
 		},
265 265
 	}
266 266
 
267
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
267
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
268 268
 
269 269
 	if err != nil {
270 270
 		t.Fatalf("unexpected deploy error: %#v", err)
... ...
@@ -305,7 +290,7 @@ func TestRecreate_deploymentPreHookFailAbort(t *testing.T) {
305 305
 		},
306 306
 	}
307 307
 
308
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
308
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
309 309
 	if err == nil {
310 310
 		t.Fatalf("expected a deploy error")
311 311
 	}
... ...
@@ -338,7 +323,7 @@ func TestRecreate_deploymentPreHookFailureIgnored(t *testing.T) {
338 338
 		},
339 339
 	}
340 340
 
341
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
341
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
342 342
 
343 343
 	if err != nil {
344 344
 		t.Fatalf("unexpected deploy error: %#v", err)
... ...
@@ -384,7 +369,7 @@ func TestRecreate_deploymentPreHookFailureRetried(t *testing.T) {
384 384
 		},
385 385
 	}
386 386
 
387
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
387
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
388 388
 
389 389
 	if err != nil {
390 390
 		t.Fatalf("unexpected deploy error: %#v", err)
... ...
@@ -425,7 +410,7 @@ func TestRecreate_deploymentPostHookSuccess(t *testing.T) {
425 425
 		},
426 426
 	}
427 427
 
428
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
428
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
429 429
 
430 430
 	if err != nil {
431 431
 		t.Fatalf("unexpected deploy error: %#v", err)
... ...
@@ -466,7 +451,7 @@ func TestRecreate_deploymentPostHookAbortUnsupported(t *testing.T) {
466 466
 		},
467 467
 	}
468 468
 
469
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
469
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
470 470
 
471 471
 	if err != nil {
472 472
 		t.Fatalf("unexpected deploy error: %#v", err)
... ...
@@ -507,7 +492,7 @@ func TestRecreate_deploymentPostHookFailIgnore(t *testing.T) {
507 507
 		},
508 508
 	}
509 509
 
510
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
510
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
511 511
 
512 512
 	if err != nil {
513 513
 		t.Fatalf("unexpected deploy error: %#v", err)
... ...
@@ -553,7 +538,7 @@ func TestRecreate_deploymentPostHookFailureRetried(t *testing.T) {
553 553
 		},
554 554
 	}
555 555
 
556
-	err := strategy.Deploy(deployment, []kapi.ObjectReference{})
556
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
557 557
 
558 558
 	if err != nil {
559 559
 		t.Fatalf("unexpected deploy error: %#v", err)
560 560
new file mode 100644
... ...
@@ -0,0 +1,215 @@
0
+package rolling
1
+
2
+import (
3
+	"fmt"
4
+	"strconv"
5
+	"time"
6
+
7
+	"github.com/golang/glog"
8
+
9
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
10
+	kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
11
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
12
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
13
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
14
+
15
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
16
+	"github.com/openshift/origin/pkg/deploy/strategy"
17
+	deployutil "github.com/openshift/origin/pkg/deploy/util"
18
+)
19
+
20
+// TODO: This should perhaps be made public upstream. See:
21
+// https://github.com/GoogleCloudPlatform/kubernetes/issues/7851
22
+const sourceIdAnnotation = "kubectl.kubernetes.io/update-source-id"
23
+
24
+// RollingDeploymentStrategy is a Strategy which implements rolling
25
+// deployments using the upstream Kubernetes RollingUpdater.
26
+//
27
+// Currently, there are some caveats:
28
+//
29
+// 1. When there is no existing prior deployment, deployment delegates to
30
+// another strategy.
31
+// 2. The interface to the RollingUpdater is not very clean.
32
+//
33
+// These caveats can be resolved with future upstream refactorings to
34
+// RollingUpdater[1][2].
35
+//
36
+// [1] https://github.com/GoogleCloudPlatform/kubernetes/pull/7183
37
+// [2] https://github.com/GoogleCloudPlatform/kubernetes/issues/7851
38
+type RollingDeploymentStrategy struct {
39
+	// initialStrategy is used when there are no prior deployments.
40
+	initialStrategy strategy.DeploymentStrategy
41
+	// client is used to deal with ReplicationControllers.
42
+	client kubectl.RollingUpdaterClient
43
+	// rollingUpdate knows how to perform a rolling update.
44
+	rollingUpdate func(config *kubectl.RollingUpdaterConfig) error
45
+	// codec is used to access the encoded config on a deployment.
46
+	codec runtime.Codec
47
+}
48
+
49
+// NewRollingDeploymentStrategy makes a new RollingDeploymentStrategy.
50
+func NewRollingDeploymentStrategy(namespace string, client kclient.Interface, codec runtime.Codec, initialStrategy strategy.DeploymentStrategy) *RollingDeploymentStrategy {
51
+	updaterClient := &rollingUpdaterClient{
52
+		ControllerHasDesiredReplicasFn: func(rc *kapi.ReplicationController) wait.ConditionFunc {
53
+			return kclient.ControllerHasDesiredReplicas(client, rc)
54
+		},
55
+		GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) {
56
+			return client.ReplicationControllers(namespace).Get(name)
57
+		},
58
+		UpdateReplicationControllerFn: func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) {
59
+			return client.ReplicationControllers(namespace).Update(rc)
60
+		},
61
+		// This guards against the RollingUpdater's built-in behavior to create
62
+		// RCs when the supplied old RC is nil. We won't pass nil, but it doesn't
63
+		// hurt to further guard against it since we would have no way to identify
64
+		// or clean up orphaned RCs RollingUpdater might inadvertently create.
65
+		CreateReplicationControllerFn: func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) {
66
+			return nil, fmt.Errorf("unexpected attempt to create deployment: %#v", rc)
67
+		},
68
+		// We give the RollingUpdater a policy which should prevent it from
69
+		// deleting the source deployment after the transition, but it doesn't
70
+		// hurt to guard by removing its ability to delete.
71
+		DeleteReplicationControllerFn: func(namespace, name string) error {
72
+			return fmt.Errorf("unexpected attempt to delete %s/%s", namespace, name)
73
+		},
74
+	}
75
+	return &RollingDeploymentStrategy{
76
+		codec:           codec,
77
+		initialStrategy: initialStrategy,
78
+		client:          updaterClient,
79
+		rollingUpdate: func(config *kubectl.RollingUpdaterConfig) error {
80
+			updater := kubectl.NewRollingUpdater(namespace, updaterClient)
81
+			return updater.Update(config)
82
+		},
83
+	}
84
+}
85
+
86
+func (s *RollingDeploymentStrategy) Deploy(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error {
87
+	config, err := deployutil.DecodeDeploymentConfig(deployment, s.codec)
88
+	if err != nil {
89
+		return fmt.Errorf("Couldn't decode DeploymentConfig from deployment %s: %v", deployment.Name, err)
90
+	}
91
+
92
+	// Find the latest deployment (if any).
93
+	latest, err := s.findLatestDeployment(oldDeployments)
94
+	if err != nil {
95
+		return fmt.Errorf("couldn't determine latest deployment: %v", err)
96
+	}
97
+
98
+	// If there's no prior deployment, delegate to another strategy since the
99
+	// rolling updater only supports transitioning between two deployments.
100
+	if latest == nil {
101
+		return s.initialStrategy.Deploy(deployment, oldDeployments)
102
+	}
103
+
104
+	// HACK: Assign the source ID annotation that the rolling updater expects,
105
+	// unless it already exists on the deployment.
106
+	//
107
+	// Related upstream issue:
108
+	// https://github.com/GoogleCloudPlatform/kubernetes/pull/7183
109
+	if _, hasSourceId := deployment.Annotations[sourceIdAnnotation]; !hasSourceId {
110
+		deployment.Annotations[sourceIdAnnotation] = fmt.Sprintf("%s:%s", latest.Name, latest.ObjectMeta.UID)
111
+		if updated, err := s.client.UpdateReplicationController(deployment.Namespace, deployment); err != nil {
112
+			return fmt.Errorf("couldn't assign source annotation to deployment %s/%s: %v", deployment.Namespace, deployment.Name, err)
113
+		} else {
114
+			deployment = updated
115
+		}
116
+	}
117
+
118
+	// HACK: There's a validation in the rolling updater which assumes that when
119
+	// an existing RC is supplied, it will have >0 replicas- a validation which
120
+	// is then disregarded as the desired count is obtained from the annotation
121
+	// on the RC. For now, fake it out by just setting replicas to 1.
122
+	//
123
+	// Related upstream issue:
124
+	// https://github.com/GoogleCloudPlatform/kubernetes/pull/7183
125
+	deployment.Spec.Replicas = 1
126
+
127
+	glog.Infof("OldRc: %s, replicas=%d", latest.Name, latest.Spec.Replicas)
128
+	// Perform a rolling update.
129
+	params := config.Template.Strategy.RollingParams
130
+	rollingConfig := &kubectl.RollingUpdaterConfig{
131
+		Out:           &rollingUpdaterWriter{},
132
+		OldRc:         latest,
133
+		NewRc:         deployment,
134
+		UpdatePeriod:  time.Duration(*params.UpdatePeriodSeconds) * time.Second,
135
+		Interval:      time.Duration(*params.IntervalSeconds) * time.Second,
136
+		Timeout:       time.Duration(*params.TimeoutSeconds) * time.Second,
137
+		CleanupPolicy: kubectl.PreserveRollingUpdateCleanupPolicy,
138
+	}
139
+	glog.Infof("Starting rolling update with config: %#v (UpdatePeriod %d, Interval %d, Timeout %d) (UpdatePeriodSeconds %d, IntervalSeconds %d, TimeoutSeconds %d)",
140
+		rollingConfig,
141
+		rollingConfig.UpdatePeriod,
142
+		rollingConfig.Interval,
143
+		rollingConfig.Timeout,
144
+		*params.UpdatePeriodSeconds,
145
+		*params.IntervalSeconds,
146
+		*params.TimeoutSeconds,
147
+	)
148
+	return s.rollingUpdate(rollingConfig)
149
+}
150
+
151
+// findLatestDeployment retrieves deployments identified by oldDeployments and
152
+// returns the latest one from the list, or nil if there are no old
153
+// deployments.
154
+func (s *RollingDeploymentStrategy) findLatestDeployment(oldDeployments []*kapi.ReplicationController) (*kapi.ReplicationController, error) {
155
+	// Find the latest deployment from the list of old deployments.
156
+	var latest *kapi.ReplicationController
157
+	latestVersion := 0
158
+	for _, deployment := range oldDeployments {
159
+		if val, hasVersion := deployment.Annotations[deployapi.DeploymentVersionAnnotation]; hasVersion {
160
+			version, err := strconv.Atoi(val)
161
+			if err != nil {
162
+				return nil, fmt.Errorf("deployment %s/%s has invalid version annotation value '%s': %v", deployment.Namespace, deployment.Name, val, err)
163
+			}
164
+			if version > latestVersion {
165
+				latest = deployment
166
+				latestVersion = version
167
+			}
168
+		} else {
169
+			glog.Infof("Ignoring deployment with missing version annotation: %s/%s", deployment.Namespace, deployment.Name)
170
+		}
171
+	}
172
+	if latest != nil {
173
+		glog.Infof("Found latest deployment %s", latest.Name)
174
+	} else {
175
+		glog.Info("No latest deployment found")
176
+	}
177
+	return latest, nil
178
+}
179
+
180
+type rollingUpdaterClient struct {
181
+	GetReplicationControllerFn     func(namespace, name string) (*kapi.ReplicationController, error)
182
+	UpdateReplicationControllerFn  func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error)
183
+	CreateReplicationControllerFn  func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error)
184
+	DeleteReplicationControllerFn  func(namespace, name string) error
185
+	ControllerHasDesiredReplicasFn func(rc *kapi.ReplicationController) wait.ConditionFunc
186
+}
187
+
188
+func (c *rollingUpdaterClient) GetReplicationController(namespace, name string) (*kapi.ReplicationController, error) {
189
+	return c.GetReplicationControllerFn(namespace, name)
190
+}
191
+
192
+func (c *rollingUpdaterClient) UpdateReplicationController(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) {
193
+	return c.UpdateReplicationControllerFn(namespace, rc)
194
+}
195
+
196
+func (c *rollingUpdaterClient) CreateReplicationController(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) {
197
+	return c.CreateReplicationControllerFn(namespace, rc)
198
+}
199
+
200
+func (c *rollingUpdaterClient) DeleteReplicationController(namespace, name string) error {
201
+	return c.DeleteReplicationControllerFn(namespace, name)
202
+}
203
+
204
+func (c *rollingUpdaterClient) ControllerHasDesiredReplicas(rc *kapi.ReplicationController) wait.ConditionFunc {
205
+	return c.ControllerHasDesiredReplicasFn(rc)
206
+}
207
+
208
+// rollingUpdaterWriter is an io.Writer that delegates to glog.
209
+type rollingUpdaterWriter struct{}
210
+
211
+func (w *rollingUpdaterWriter) Write(p []byte) (n int, err error) {
212
+	glog.Info(fmt.Sprintf("RollingUpdater: %s", p))
213
+	return len(p), nil
214
+}
0 215
new file mode 100644
... ...
@@ -0,0 +1,238 @@
0
+package rolling
1
+
2
+import (
3
+	"fmt"
4
+	"testing"
5
+	"time"
6
+
7
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
8
+	kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
10
+
11
+	api "github.com/openshift/origin/pkg/api/latest"
12
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
13
+	deploytest "github.com/openshift/origin/pkg/deploy/api/test"
14
+	deployutil "github.com/openshift/origin/pkg/deploy/util"
15
+)
16
+
17
+func TestRolling_deployInitial(t *testing.T) {
18
+	initialStrategyInvoked := false
19
+
20
+	strategy := &RollingDeploymentStrategy{
21
+		codec: api.Codec,
22
+		client: &rollingUpdaterClient{
23
+			GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) {
24
+				t.Fatalf("unexpected call to GetReplicationController")
25
+				return nil, nil
26
+			},
27
+		},
28
+		initialStrategy: &testStrategy{
29
+			deployFn: func(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error {
30
+				initialStrategyInvoked = true
31
+				return nil
32
+			},
33
+		},
34
+		rollingUpdate: func(config *kubectl.RollingUpdaterConfig) error {
35
+			t.Fatalf("unexpected call to rollingUpdate")
36
+			return nil
37
+		},
38
+	}
39
+
40
+	deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)
41
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{})
42
+	if err != nil {
43
+		t.Fatalf("unexpected error: %v", err)
44
+	}
45
+	if !initialStrategyInvoked {
46
+		t.Fatalf("expected initial strategy to be invoked")
47
+	}
48
+}
49
+
50
+func TestRolling_deployRolling(t *testing.T) {
51
+	latest, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)
52
+	config := deploytest.OkDeploymentConfig(2)
53
+	config.Template.Strategy = deployapi.DeploymentStrategy{
54
+		Type: deployapi.DeploymentStrategyTypeRolling,
55
+		RollingParams: &deployapi.RollingDeploymentStrategyParams{
56
+			IntervalSeconds:     mkintp(1),
57
+			UpdatePeriodSeconds: mkintp(2),
58
+			TimeoutSeconds:      mkintp(3),
59
+		},
60
+	}
61
+	deployment, _ := deployutil.MakeDeployment(config, kapi.Codec)
62
+
63
+	var rollingConfig *kubectl.RollingUpdaterConfig
64
+	deploymentUpdated := false
65
+	strategy := &RollingDeploymentStrategy{
66
+		codec: api.Codec,
67
+		client: &rollingUpdaterClient{
68
+			GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) {
69
+				if name != latest.Name {
70
+					t.Fatalf("unexpected call to GetReplicationController for %s", name)
71
+				}
72
+				return latest, nil
73
+			},
74
+			UpdateReplicationControllerFn: func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) {
75
+				if rc.Name != deployment.Name {
76
+					t.Fatalf("unexpected call to UpdateReplicationController for %s", rc.Name)
77
+				}
78
+				deploymentUpdated = true
79
+				return rc, nil
80
+			},
81
+		},
82
+		initialStrategy: &testStrategy{
83
+			deployFn: func(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error {
84
+				t.Fatalf("unexpected call to initial strategy")
85
+				return nil
86
+			},
87
+		},
88
+		rollingUpdate: func(config *kubectl.RollingUpdaterConfig) error {
89
+			rollingConfig = config
90
+			return nil
91
+		},
92
+	}
93
+
94
+	err := strategy.Deploy(deployment, []*kapi.ReplicationController{latest})
95
+	if err != nil {
96
+		t.Fatalf("unexpected error: %v", err)
97
+	}
98
+	if rollingConfig == nil {
99
+		t.Fatalf("expected rolling update to be invoked")
100
+	}
101
+
102
+	if e, a := latest, rollingConfig.OldRc; e != a {
103
+		t.Errorf("expected rollingConfig.OldRc %v, got %v", e, a)
104
+	}
105
+
106
+	if e, a := deployment, rollingConfig.NewRc; e != a {
107
+		t.Errorf("expected rollingConfig.NewRc %v, got %v", e, a)
108
+	}
109
+
110
+	if e, a := 1*time.Second, rollingConfig.Interval; e != a {
111
+		t.Errorf("expected Interval %d, got %d", e, a)
112
+	}
113
+
114
+	if e, a := 2*time.Second, rollingConfig.UpdatePeriod; e != a {
115
+		t.Errorf("expected UpdatePeriod %d, got %d", e, a)
116
+	}
117
+
118
+	if e, a := 3*time.Second, rollingConfig.Timeout; e != a {
119
+		t.Errorf("expected Timeout %d, got %d", e, a)
120
+	}
121
+
122
+	// verify hack
123
+	if e, a := 1, rollingConfig.NewRc.Spec.Replicas; e != a {
124
+		t.Errorf("expected rollingConfig.NewRc.Spec.Replicas %d, got %d", e, a)
125
+	}
126
+
127
+	// verify hack
128
+	if !deploymentUpdated {
129
+		t.Errorf("expected deployment to be updated for source annotation")
130
+	}
131
+	sid := fmt.Sprintf("%s:%s", latest.Name, latest.ObjectMeta.UID)
132
+	if e, a := sid, rollingConfig.NewRc.Annotations[sourceIdAnnotation]; e != a {
133
+		t.Errorf("expected sourceIdAnnotation %s, got %s", e, a)
134
+	}
135
+}
136
+
137
+func TestRolling_findLatestDeployment(t *testing.T) {
138
+	deployments := map[string]*kapi.ReplicationController{}
139
+	for i := 1; i <= 10; i++ {
140
+		deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(i), kapi.Codec)
141
+		deployments[deployment.Name] = deployment
142
+	}
143
+
144
+	ignoredDeployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(12), kapi.Codec)
145
+	delete(ignoredDeployment.Annotations, deployapi.DeploymentVersionAnnotation)
146
+	deployments[ignoredDeployment.Name] = ignoredDeployment
147
+
148
+	strategy := &RollingDeploymentStrategy{
149
+		codec: api.Codec,
150
+		client: &rollingUpdaterClient{
151
+			GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) {
152
+				deployment, found := deployments[name]
153
+				if !found {
154
+					return nil, kerrors.NewNotFound("ReplicationController", name)
155
+				}
156
+				return deployment, nil
157
+			},
158
+		},
159
+	}
160
+
161
+	type scenario struct {
162
+		old    []string
163
+		latest string
164
+	}
165
+
166
+	scenarios := []scenario{
167
+		{
168
+			old: []string{
169
+				"config-1",
170
+				"config-2",
171
+				"config-3",
172
+			},
173
+			latest: "config-3",
174
+		},
175
+		{
176
+			old: []string{
177
+				"config-3",
178
+				"config-1",
179
+				"config-7",
180
+				ignoredDeployment.Name,
181
+			},
182
+			latest: "config-7",
183
+		},
184
+	}
185
+
186
+	for _, scenario := range scenarios {
187
+		old := []*kapi.ReplicationController{}
188
+		for _, oldName := range scenario.old {
189
+			old = append(old, deployments[oldName])
190
+		}
191
+		found, err := strategy.findLatestDeployment(old)
192
+		if err != nil {
193
+			t.Errorf("unexpected error for scenario: %v: %v", scenario, err)
194
+			continue
195
+		}
196
+
197
+		if found == nil {
198
+			t.Errorf("expected to find a deployment for scenario: %v", scenario)
199
+			continue
200
+		}
201
+
202
+		if e, a := scenario.latest, found.Name; e != a {
203
+			t.Errorf("expected latest %s, got %s for scenario: %v", e, a, scenario)
204
+		}
205
+	}
206
+}
207
+
208
+func TestRolling_findLatestDeploymentInvalidDeployment(t *testing.T) {
209
+	deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec)
210
+	deployment.Annotations[deployapi.DeploymentVersionAnnotation] = ""
211
+
212
+	strategy := &RollingDeploymentStrategy{
213
+		codec: api.Codec,
214
+		client: &rollingUpdaterClient{
215
+			GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) {
216
+				return deployment, nil
217
+			},
218
+		},
219
+	}
220
+	_, err := strategy.findLatestDeployment([]*kapi.ReplicationController{deployment})
221
+	if err == nil {
222
+		t.Errorf("expected an error")
223
+	}
224
+}
225
+
226
+type testStrategy struct {
227
+	deployFn func(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error
228
+}
229
+
230
+func (s *testStrategy) Deploy(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error {
231
+	return s.deployFn(deployment, oldDeployments)
232
+}
233
+
234
+func mkintp(i int) *int64 {
235
+	v := int64(i)
236
+	return &v
237
+}