From 4ad08f1312ec5cfcb5aa2c636c8ecc38b72a1f17 Mon Sep 17 00:00:00 2001 From: Dan Mace Date: Thu, 23 Apr 2015 09:23:03 -0400 Subject: [PATCH] Implement a Rolling deployment strategy Implement a rolling deployment strategy which piggybacks on the upstream RollingUpdater functionality. --- assets/test/e2e/test.js | 2 +- cpu.pprof | Bin 0 -> 403416 bytes .../application-template-custombuild.json | 4 +- .../application-template-dockerbuild.json | 4 +- .../application-template-stibuild.json | 36 +-- pkg/api/serialization_test.go | 21 +- pkg/cmd/infra/deployer/deployer.go | 39 ++- pkg/cmd/infra/deployer/deployer_test.go | 30 ++- pkg/cmd/server/origin/master.go | 8 +- pkg/deploy/api/types.go | 18 ++ pkg/deploy/api/v1beta1/conversion.go | 6 + pkg/deploy/api/v1beta1/defaults.go | 46 ++++ pkg/deploy/api/v1beta1/defaults_test.go | 53 ++++ pkg/deploy/api/v1beta1/types.go | 18 ++ pkg/deploy/api/v1beta3/conversion.go | 6 + pkg/deploy/api/v1beta3/defaults.go | 46 ++++ pkg/deploy/api/v1beta3/defaults_test.go | 53 ++++ pkg/deploy/api/v1beta3/types.go | 18 ++ pkg/deploy/api/validation/validation.go | 24 ++ pkg/deploy/api/validation/validation_test.go | 62 +++++ pkg/deploy/controller/deployment/factory.go | 14 +- pkg/deploy/strategy/interfaces.go | 11 + pkg/deploy/strategy/recreate/recreate.go | 2 +- pkg/deploy/strategy/recreate/recreate_test.go | 43 ++-- pkg/deploy/strategy/rolling/rolling.go | 215 ++++++++++++++++ pkg/deploy/strategy/rolling/rolling_test.go | 238 ++++++++++++++++++ 26 files changed, 918 insertions(+), 99 deletions(-) create mode 100644 cpu.pprof create mode 100644 pkg/deploy/api/v1beta1/defaults.go create mode 100644 pkg/deploy/api/v1beta1/defaults_test.go create mode 100644 pkg/deploy/api/v1beta3/defaults.go create mode 100644 pkg/deploy/api/v1beta3/defaults_test.go create mode 100644 pkg/deploy/strategy/interfaces.go create mode 100644 pkg/deploy/strategy/rolling/rolling.go create mode 100644 pkg/deploy/strategy/rolling/rolling_test.go diff --git a/assets/test/e2e/test.js b/assets/test/e2e/test.js index d07eac6b0736..68bc02bec82c 100644 --- a/assets/test/e2e/test.js +++ b/assets/test/e2e/test.js @@ -88,7 +88,7 @@ describe('', function() { expect(element(by.cssContainingText("h2.service","frontend")).isPresent()).toBe(true); expect(element(by.cssContainingText(".pod-template-image","Build: ruby-sample-build")).isPresent()).toBe(true); expect(element(by.cssContainingText(".deployment-trigger","new image for test/origin-ruby-sample:latest")).isPresent()).toBe(true); - expect(element.all(by.css(".pod-running")).count()).toEqual(2); + expect(element.all(by.css(".pod-running")).count()).toEqual(3); }); }); diff --git a/cpu.pprof b/cpu.pprof new file mode 100644 index 0000000000000000000000000000000000000000..9e8d6598d0c291a9f5b13942b328ea8a2c1f2bbe GIT binary patch literal 403416 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 C3)JWU literal 0 HcmV?d00001 diff --git a/examples/sample-app/application-template-custombuild.json b/examples/sample-app/application-template-custombuild.json index df2e90dcd521..704cc1f8ac06 100644 --- a/examples/sample-app/application-template-custombuild.json +++ b/examples/sample-app/application-template-custombuild.json @@ -165,10 +165,10 @@ "replicaSelector": { "name": "frontend" }, - "replicas": 1 + "replicas": 2 }, "strategy": { - "type": "Recreate" + "type": "Rolling" } }, "triggers": [ diff --git a/examples/sample-app/application-template-dockerbuild.json b/examples/sample-app/application-template-dockerbuild.json index 7618c8166587..0cdeffdee85b 100644 --- a/examples/sample-app/application-template-dockerbuild.json +++ b/examples/sample-app/application-template-dockerbuild.json @@ -158,10 +158,10 @@ "replicaSelector": { "name": "frontend" }, - "replicas": 1 + "replicas": 2 }, "strategy": { - "type": "Recreate" + "type": "Rolling" } }, "triggers": [ diff --git a/examples/sample-app/application-template-stibuild.json b/examples/sample-app/application-template-stibuild.json index 764c69219ebc..7d394d8a6aaa 100644 --- a/examples/sample-app/application-template-stibuild.json +++ b/examples/sample-app/application-template-stibuild.json @@ -158,42 +158,10 @@ "replicaSelector": { "name": "frontend" }, - "replicas": 1 + "replicas": 2 }, "strategy": { - "type": "Recreate", - "recreateParams": { - "pre": { - "failurePolicy": "Abort", - "execNewPod": { - "containerName": "ruby-helloworld", - "command": [ - "/bin/true" - ], - "env": [ - { - "name": "CUSTOM_VAR1", - "value": "custom_value1" - } - ] - } - }, - "post": { - "failurePolicy": "Ignore", - "execNewPod": { - "containerName": "ruby-helloworld", - "command": [ - "/bin/false" - ], - "env": [ - { - "name": "CUSTOM_VAR2", - "value": "custom_value2" - } - ] - } - } - } + "type": "Rolling" } }, "triggers": [ diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index 79ea0e7edede..b729e5cf9155 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -105,9 +105,26 @@ func fuzzInternalObject(t *testing.T, forVersion string, item runtime.Object, se }, func(j *deploy.DeploymentStrategy, c fuzz.Continue) { c.FuzzNoCustom(j) - // TODO: we should not have to set defaults, instead we should be able to detect defaults were applied. - if len(j.Type) == 0 { + mkintp := func(i int) *int64 { + v := int64(i) + return &v + } + switch c.Intn(3) { + case 0: + // TODO: we should not have to set defaults, instead we should be able + // to detect defaults were applied. + j.Type = deploy.DeploymentStrategyTypeRolling + j.RollingParams = &deploy.RollingDeploymentStrategyParams{ + IntervalSeconds: mkintp(1), + UpdatePeriodSeconds: mkintp(1), + TimeoutSeconds: mkintp(120), + } + case 1: j.Type = deploy.DeploymentStrategyTypeRecreate + j.RollingParams = nil + case 2: + j.Type = deploy.DeploymentStrategyTypeCustom + j.RollingParams = nil } }, func(j *deploy.DeploymentCauseImageTrigger, c fuzz.Continue) { diff --git a/pkg/cmd/infra/deployer/deployer.go b/pkg/cmd/infra/deployer/deployer.go index 696d13c69ab0..35d19e838029 100644 --- a/pkg/cmd/infra/deployer/deployer.go +++ b/pkg/cmd/infra/deployer/deployer.go @@ -14,7 +14,9 @@ import ( "github.com/openshift/origin/pkg/cmd/util" "github.com/openshift/origin/pkg/cmd/util/clientcmd" deployapi "github.com/openshift/origin/pkg/deploy/api" - strategy "github.com/openshift/origin/pkg/deploy/strategy/recreate" + "github.com/openshift/origin/pkg/deploy/strategy" + "github.com/openshift/origin/pkg/deploy/strategy/recreate" + "github.com/openshift/origin/pkg/deploy/strategy/rolling" deployutil "github.com/openshift/origin/pkg/deploy/util" "github.com/openshift/origin/pkg/version" ) @@ -78,21 +80,35 @@ func NewCommandDeployer(name string) *cobra.Command { // deploy executes a deployment strategy. func deploy(kClient kclient.Interface, namespace, deploymentName string) error { - newDeployment, oldDeployments, err := getDeployerContext(&realReplicationControllerGetter{kClient}, namespace, deploymentName) - + deployment, oldDeployments, err := getDeployerContext(&realReplicationControllerGetter{kClient}, namespace, deploymentName) if err != nil { return err } - // TODO: Choose a strategy based on some input - strategy := strategy.NewRecreateDeploymentStrategy(kClient, latest.Codec) - return strategy.Deploy(newDeployment, oldDeployments) + config, err := deployutil.DecodeDeploymentConfig(deployment, latest.Codec) + if err != nil { + return fmt.Errorf("couldn't decode DeploymentConfig from deployment %s/%s: %v", deployment.Namespace, deployment.Name, err) + } + + var strategy strategy.DeploymentStrategy + + switch config.Template.Strategy.Type { + case deployapi.DeploymentStrategyTypeRecreate: + strategy = recreate.NewRecreateDeploymentStrategy(kClient, latest.Codec) + case deployapi.DeploymentStrategyTypeRolling: + recreate := recreate.NewRecreateDeploymentStrategy(kClient, latest.Codec) + strategy = rolling.NewRollingDeploymentStrategy(deployment.Namespace, kClient, latest.Codec, recreate) + default: + return fmt.Errorf("unsupported strategy type: %s", config.Template.Strategy.Type) + } + + return strategy.Deploy(deployment, oldDeployments) } // getDeployerContext finds the target deployment and any deployments it considers to be prior to the // target deployment. Only deployments whose LatestVersion is less than the target deployment are // considered to be prior. -func getDeployerContext(controllerGetter replicationControllerGetter, namespace, deploymentName string) (*kapi.ReplicationController, []kapi.ObjectReference, error) { +func getDeployerContext(controllerGetter replicationControllerGetter, namespace, deploymentName string) (*kapi.ReplicationController, []*kapi.ReplicationController, error) { var err error var newDeployment *kapi.ReplicationController var newConfig *deployapi.DeploymentConfig @@ -112,14 +128,14 @@ func getDeployerContext(controllerGetter replicationControllerGetter, namespace, // encoded DeploymentConfigs to the new one by LatestVersion. Treat a failure to interpret a given // old deployment as a fatal error to prevent overlapping deployments. var allControllers *kapi.ReplicationControllerList - oldDeployments := []kapi.ObjectReference{} + oldDeployments := []*kapi.ReplicationController{} if allControllers, err = controllerGetter.List(newDeployment.Namespace, labels.Everything()); err != nil { return nil, nil, fmt.Errorf("Unable to get list replication controllers in deployment namespace %s: %v", newDeployment.Namespace, err) } glog.Infof("Inspecting %d potential prior deployments", len(allControllers.Items)) - for _, controller := range allControllers.Items { + for i, controller := range allControllers.Items { if configName, hasConfigName := controller.Annotations[deployapi.DeploymentConfigAnnotation]; !hasConfigName { glog.Infof("Disregarding replicationController %s (not a deployment)", controller.Name) continue @@ -135,10 +151,7 @@ func getDeployerContext(controllerGetter replicationControllerGetter, namespace, if oldVersion < newConfig.LatestVersion { glog.Infof("Marking deployment %s as a prior deployment", controller.Name) - oldDeployments = append(oldDeployments, kapi.ObjectReference{ - Namespace: controller.Namespace, - Name: controller.Name, - }) + oldDeployments = append(oldDeployments, &allControllers.Items[i]) } else { glog.Infof("Disregarding deployment %s (same as or newer than target)", controller.Name) } diff --git a/pkg/cmd/infra/deployer/deployer_test.go b/pkg/cmd/infra/deployer/deployer_test.go index ad91712af4f4..255f2aa54de2 100644 --- a/pkg/cmd/infra/deployer/deployer_test.go +++ b/pkg/cmd/infra/deployer/deployer_test.go @@ -96,13 +96,13 @@ func TestGetDeploymentContextNoPriorDeployments(t *testing.T) { func TestGetDeploymentContextWithPriorDeployments(t *testing.T) { getter := &testReplicationControllerGetter{ getFunc: func(namespace, name string) (*kapi.ReplicationController, error) { - deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(2), kapi.Codec) + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(3), kapi.Codec) return deployment, nil }, listFunc: func(namespace string, selector labels.Selector) (*kapi.ReplicationControllerList, error) { deployment1, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) deployment2, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(2), kapi.Codec) - deployment3, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(3), kapi.Codec) + deployment3, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(4), kapi.Codec) deployment4, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) deployment4.Annotations[deployapi.DeploymentConfigAnnotation] = "another-config" return &kapi.ReplicationControllerList{ @@ -131,8 +131,30 @@ func TestGetDeploymentContextWithPriorDeployments(t *testing.T) { t.Fatal("expected non-nil oldDeployments") } - if e, a := 1, len(oldDeployments); e != a { - t.Fatalf("expected oldDeployments with size %d, got %d: %#v", e, a, oldDeployments) + expected := []string{"config-1", "config-2"} + for _, e := range expected { + found := false + for _, d := range oldDeployments { + if d.Name == e { + found = true + break + } + } + if !found { + t.Errorf("expected to find old deployment %s", e) + } + } + for _, d := range oldDeployments { + ok := false + for _, e := range expected { + if d.Name == e { + ok = true + break + } + } + if !ok { + t.Errorf("unexpected old deployment %s", d.Name) + } } } diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 3526824b7bbc..c65c85ee60f3 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -837,10 +837,10 @@ func (c *MasterConfig) RunDeploymentController() error { env = append(env, clientcmd.EnvVarsFromConfig(c.DeployerClientConfig())...) factory := deploycontroller.DeploymentControllerFactory{ - KubeClient: kclient, - Codec: latest.Codec, - Environment: env, - RecreateStrategyImage: c.ImageFor("deployer"), + KubeClient: kclient, + Codec: latest.Codec, + Environment: env, + DeployerImage: c.ImageFor("deployer"), } controller := factory.Create() diff --git a/pkg/deploy/api/types.go b/pkg/deploy/api/types.go index 059693e919cb..1f4c22fd5a55 100644 --- a/pkg/deploy/api/types.go +++ b/pkg/deploy/api/types.go @@ -54,6 +54,8 @@ type DeploymentStrategy struct { CustomParams *CustomDeploymentStrategyParams `json:"customParams,omitempty"` // RecreateParams are the input to the Recreate deployment strategy. RecreateParams *RecreateDeploymentStrategyParams `json:"recreateParams,omitempty"` + // RollingParams are the input to the Rolling deployment strategy. + RollingParams *RollingDeploymentStrategyParams `json:"rollingParams,omitempty"` // Compute resource requirements to execute the deployment Resources kapi.ResourceRequirements `json:"resources,omitempty"` } @@ -66,6 +68,8 @@ const ( DeploymentStrategyTypeRecreate DeploymentStrategyType = "Recreate" // DeploymentStrategyTypeCustom is a user defined strategy. DeploymentStrategyTypeCustom DeploymentStrategyType = "Custom" + // DeploymentStrategyTypeRolling uses the Kubernetes RollingUpdater. + DeploymentStrategyTypeRolling DeploymentStrategyType = "Rolling" ) // CustomDeploymentStrategyParams are the input to the Custom deployment strategy. @@ -123,6 +127,20 @@ type ExecNewPodHook struct { ContainerName string `json:"containerName"` } +// RollingDeploymentStrategyParams are the input to the Rolling deployment +// strategy. +type RollingDeploymentStrategyParams struct { + // UpdatePeriodSeconds is the time to wait between individual pod updates. + // If the value is nil, a default will be used. + UpdatePeriodSeconds *int64 `json:"updatePeriodSeconds,omitempty" description:"the time to wait between individual pod updates"` + // IntervalSeconds is the time to wait between polling deployment status + // after update. If the value is nil, a default will be used. + IntervalSeconds *int64 `json:"intervalSeconds,omitempty" description:"the time to wait between polling deployment status after update"` + // TimeoutSeconds is the time to wait for updates before giving up. If the + // value is nil, a default will be used. + TimeoutSeconds *int64 `json:"timeoutSeconds,omitempty" description:"the time to wait for updates before giving up"` +} + // DeploymentList is a collection of deployments. // DEPRECATED: Like Deployment, this is no longer used. type DeploymentList struct { diff --git a/pkg/deploy/api/v1beta1/conversion.go b/pkg/deploy/api/v1beta1/conversion.go index 3fea8d43de1e..d9ae552d5e17 100644 --- a/pkg/deploy/api/v1beta1/conversion.go +++ b/pkg/deploy/api/v1beta1/conversion.go @@ -19,6 +19,9 @@ func init() { if err := s.Convert(&in.RecreateParams, &out.RecreateParams, 0); err != nil { return err } + if err := s.Convert(&in.RollingParams, &out.RollingParams, 0); err != nil { + return err + } if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { return err } @@ -34,6 +37,9 @@ func init() { if err := s.Convert(&in.RecreateParams, &out.RecreateParams, 0); err != nil { return err } + if err := s.Convert(&in.RollingParams, &out.RollingParams, 0); err != nil { + return err + } if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { return err } diff --git a/pkg/deploy/api/v1beta1/defaults.go b/pkg/deploy/api/v1beta1/defaults.go new file mode 100644 index 000000000000..f203f60d6e9b --- /dev/null +++ b/pkg/deploy/api/v1beta1/defaults.go @@ -0,0 +1,46 @@ +package v1beta1 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + deployapi "github.com/openshift/origin/pkg/deploy/api" +) + +func init() { + mkintp := func(i int) *int64 { + v := int64(i) + return &v + } + + err := api.Scheme.AddDefaultingFuncs( + func(obj *deployapi.DeploymentStrategy) { + if len(obj.Type) == 0 { + obj.Type = deployapi.DeploymentStrategyTypeRolling + } + + if obj.Type == deployapi.DeploymentStrategyTypeRolling && obj.RollingParams == nil { + obj.RollingParams = &deployapi.RollingDeploymentStrategyParams{ + IntervalSeconds: mkintp(1), + UpdatePeriodSeconds: mkintp(1), + TimeoutSeconds: mkintp(120), + } + } + }, + func(obj *deployapi.RollingDeploymentStrategyParams) { + if obj.IntervalSeconds == nil { + obj.IntervalSeconds = mkintp(1) + } + + if obj.UpdatePeriodSeconds == nil { + obj.UpdatePeriodSeconds = mkintp(1) + } + + if obj.TimeoutSeconds == nil { + obj.TimeoutSeconds = mkintp(120) + } + }, + ) + if err != nil { + panic(err) + } +} diff --git a/pkg/deploy/api/v1beta1/defaults_test.go b/pkg/deploy/api/v1beta1/defaults_test.go new file mode 100644 index 000000000000..94d20cce9a9f --- /dev/null +++ b/pkg/deploy/api/v1beta1/defaults_test.go @@ -0,0 +1,53 @@ +package v1beta1_test + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + + newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" + deployapi "github.com/openshift/origin/pkg/deploy/api" +) + +func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { + data, err := current.Codec.Encode(obj) + if err != nil { + t.Errorf("%v\n %#v", err, obj) + return nil + } + obj2, err := newer.Codec.Decode(data) + if err != nil { + t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj) + return nil + } + obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object) + err = newer.Scheme.Convert(obj2, obj3) + if err != nil { + t.Errorf("%v\nSource: %#v", err, obj2) + return nil + } + return obj3 +} + +func TestDefaults_rollingParams(t *testing.T) { + c := &deployapi.DeploymentConfig{ + Template: deployapi.DeploymentTemplate{}, + } + o := roundTrip(t, runtime.Object(c)) + config := o.(*deployapi.DeploymentConfig) + strat := config.Template.Strategy + if e, a := deployapi.DeploymentStrategyTypeRolling, strat.Type; e != a { + t.Errorf("expected strategy type %s, got %s", e, a) + } + if e, a := int64(1), *strat.RollingParams.UpdatePeriodSeconds; e != a { + t.Errorf("expected UpdatePeriodSeconds %d, got %d", e, a) + } + if e, a := int64(1), *strat.RollingParams.IntervalSeconds; e != a { + t.Errorf("expected IntervalSeconds %d, got %d", e, a) + } + if e, a := int64(120), *strat.RollingParams.TimeoutSeconds; e != a { + t.Errorf("expected UpdatePeriodSeconds %d, got %d", e, a) + } +} diff --git a/pkg/deploy/api/v1beta1/types.go b/pkg/deploy/api/v1beta1/types.go index 394d5a792b8e..7669506b1be8 100644 --- a/pkg/deploy/api/v1beta1/types.go +++ b/pkg/deploy/api/v1beta1/types.go @@ -55,6 +55,8 @@ type DeploymentStrategy struct { CustomParams *CustomDeploymentStrategyParams `json:"customParams,omitempty"` // RecreateParams are the input to the Recreate deployment strategy. RecreateParams *RecreateDeploymentStrategyParams `json:"recreateParams,omitempty"` + // RollingParams are the input to the Rolling deployment strategy. + RollingParams *RollingDeploymentStrategyParams `json:"rollingParams,omitempty"` // Compute resource requirements to execute the deployment Resources kapi.ResourceRequirements `json:"resources,omitempty"` } @@ -67,6 +69,8 @@ const ( DeploymentStrategyTypeRecreate DeploymentStrategyType = "Recreate" // DeploymentStrategyTypeCustom is a user defined strategy. DeploymentStrategyTypeCustom DeploymentStrategyType = "Custom" + // DeploymentStrategyTypeRolling uses the Kubernetes RollingUpdater. + DeploymentStrategyTypeRolling DeploymentStrategyType = "Rolling" ) // CustomParams are the input to the Custom deployment strategy. @@ -124,6 +128,20 @@ type ExecNewPodHook struct { ContainerName string `json:"containerName"` } +// RollingDeploymentStrategyParams are the input to the Rolling deployment +// strategy. +type RollingDeploymentStrategyParams struct { + // UpdatePeriodSeconds is the time to wait between individual pod updates. + // If the value is nil, a default will be used. + UpdatePeriodSeconds *int64 `json:"updatePeriodSeconds,omitempty" description:"the time to wait between individual pod updates"` + // IntervalSeconds is the time to wait between polling deployment status + // after update. If the value is nil, a default will be used. + IntervalSeconds *int64 `json:"intervalSeconds,omitempty" description:"the time to wait between polling deployment status after update"` + // TimeoutSeconds is the time to wait for updates before giving up. If the + // value is nil, a default will be used. + TimeoutSeconds *int64 `json:"timeoutSeconds,omitempty" description:"the time to wait for updates before giving up"` +} + // A DeploymentList is a collection of deployments. // DEPRECATED: Like Deployment, this is no longer used. type DeploymentList struct { diff --git a/pkg/deploy/api/v1beta3/conversion.go b/pkg/deploy/api/v1beta3/conversion.go index b3956127cde1..58c8d214a00f 100644 --- a/pkg/deploy/api/v1beta3/conversion.go +++ b/pkg/deploy/api/v1beta3/conversion.go @@ -93,6 +93,9 @@ func init() { if err := s.Convert(&in.RecreateParams, &out.RecreateParams, 0); err != nil { return err } + if err := s.Convert(&in.RollingParams, &out.RollingParams, 0); err != nil { + return err + } if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { return err } @@ -108,6 +111,9 @@ func init() { if err := s.Convert(&in.RecreateParams, &out.RecreateParams, 0); err != nil { return err } + if err := s.Convert(&in.RollingParams, &out.RollingParams, 0); err != nil { + return err + } if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { return err } diff --git a/pkg/deploy/api/v1beta3/defaults.go b/pkg/deploy/api/v1beta3/defaults.go new file mode 100644 index 000000000000..53e5aea2245f --- /dev/null +++ b/pkg/deploy/api/v1beta3/defaults.go @@ -0,0 +1,46 @@ +package v1beta3 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + deployapi "github.com/openshift/origin/pkg/deploy/api" +) + +func init() { + mkintp := func(i int) *int64 { + v := int64(i) + return &v + } + + err := api.Scheme.AddDefaultingFuncs( + func(obj *deployapi.DeploymentStrategy) { + if len(obj.Type) == 0 { + obj.Type = deployapi.DeploymentStrategyTypeRolling + } + + if obj.Type == deployapi.DeploymentStrategyTypeRolling && obj.RollingParams == nil { + obj.RollingParams = &deployapi.RollingDeploymentStrategyParams{ + IntervalSeconds: mkintp(1), + UpdatePeriodSeconds: mkintp(1), + TimeoutSeconds: mkintp(120), + } + } + }, + func(obj *deployapi.RollingDeploymentStrategyParams) { + if obj.IntervalSeconds == nil { + obj.IntervalSeconds = mkintp(1) + } + + if obj.UpdatePeriodSeconds == nil { + obj.UpdatePeriodSeconds = mkintp(1) + } + + if obj.TimeoutSeconds == nil { + obj.TimeoutSeconds = mkintp(120) + } + }, + ) + if err != nil { + panic(err) + } +} diff --git a/pkg/deploy/api/v1beta3/defaults_test.go b/pkg/deploy/api/v1beta3/defaults_test.go new file mode 100644 index 000000000000..ce08463bfea6 --- /dev/null +++ b/pkg/deploy/api/v1beta3/defaults_test.go @@ -0,0 +1,53 @@ +package v1beta3_test + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + + newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" + deployapi "github.com/openshift/origin/pkg/deploy/api" +) + +func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { + data, err := current.Codec.Encode(obj) + if err != nil { + t.Errorf("%v\n %#v", err, obj) + return nil + } + obj2, err := newer.Codec.Decode(data) + if err != nil { + t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj) + return nil + } + obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object) + err = newer.Scheme.Convert(obj2, obj3) + if err != nil { + t.Errorf("%v\nSource: %#v", err, obj2) + return nil + } + return obj3 +} + +func TestDefaults_rollingParams(t *testing.T) { + c := &deployapi.DeploymentConfig{ + Template: deployapi.DeploymentTemplate{}, + } + o := roundTrip(t, runtime.Object(c)) + config := o.(*deployapi.DeploymentConfig) + strat := config.Template.Strategy + if e, a := deployapi.DeploymentStrategyTypeRolling, strat.Type; e != a { + t.Errorf("expected strategy type %s, got %s", e, a) + } + if e, a := int64(1), *strat.RollingParams.UpdatePeriodSeconds; e != a { + t.Errorf("expected UpdatePeriodSeconds %d, got %d", e, a) + } + if e, a := int64(1), *strat.RollingParams.IntervalSeconds; e != a { + t.Errorf("expected IntervalSeconds %d, got %d", e, a) + } + if e, a := int64(120), *strat.RollingParams.TimeoutSeconds; e != a { + t.Errorf("expected UpdatePeriodSeconds %d, got %d", e, a) + } +} diff --git a/pkg/deploy/api/v1beta3/types.go b/pkg/deploy/api/v1beta3/types.go index 4ecb63b64308..8119c1ef475f 100644 --- a/pkg/deploy/api/v1beta3/types.go +++ b/pkg/deploy/api/v1beta3/types.go @@ -30,6 +30,8 @@ type DeploymentStrategy struct { CustomParams *CustomDeploymentStrategyParams `json:"customParams,omitempty"` // RecreateParams are the input to the Recreate deployment strategy. RecreateParams *RecreateDeploymentStrategyParams `json:"recreateParams,omitempty"` + // RollingParams are the input to the Rolling deployment strategy. + RollingParams *RollingDeploymentStrategyParams `json:"rollingParams,omitempty"` // Compute resource requirements to execute the deployment Resources kapi.ResourceRequirements `json:"resources,omitempty"` } @@ -42,6 +44,8 @@ const ( DeploymentStrategyTypeRecreate DeploymentStrategyType = "Recreate" // DeploymentStrategyTypeCustom is a user defined strategy. DeploymentStrategyTypeCustom DeploymentStrategyType = "Custom" + // DeploymentStrategyTypeRolling uses the Kubernetes RollingUpdater. + DeploymentStrategyTypeRolling DeploymentStrategyType = "Rolling" ) // CustomParams are the input to the Custom deployment strategy. @@ -99,6 +103,20 @@ type ExecNewPodHook struct { ContainerName string `json:"containerName"` } +// RollingDeploymentStrategyParams are the input to the Rolling deployment +// strategy. +type RollingDeploymentStrategyParams struct { + // UpdatePeriodSeconds is the time to wait between individual pod updates. + // If the value is nil, a default will be used. + UpdatePeriodSeconds *int64 `json:"updatePeriodSeconds,omitempty" description:"the time to wait between individual pod updates"` + // IntervalSeconds is the time to wait between polling deployment status + // after update. If the value is nil, a default will be used. + IntervalSeconds *int64 `json:"intervalSeconds,omitempty" description:"the time to wait between polling deployment status after update"` + // TimeoutSeconds is the time to wait for updates before giving up. If the + // value is nil, a default will be used. + TimeoutSeconds *int64 `json:"timeoutSeconds,omitempty" description:"the time to wait for updates before giving up"` +} + // These constants represent keys used for correlating objects related to deployments. const ( // DeploymentConfigAnnotation is an annotation name used to correlate a deployment with the diff --git a/pkg/deploy/api/validation/validation.go b/pkg/deploy/api/validation/validation.go index cbab79aeca56..7fd3e3e3a7c2 100644 --- a/pkg/deploy/api/validation/validation.go +++ b/pkg/deploy/api/validation/validation.go @@ -82,6 +82,12 @@ func validateDeploymentStrategy(strategy *deployapi.DeploymentStrategy) fielderr if strategy.RecreateParams != nil { errs = append(errs, validateRecreateParams(strategy.RecreateParams).Prefix("recreateParams")...) } + case deployapi.DeploymentStrategyTypeRolling: + if strategy.RollingParams == nil { + errs = append(errs, fielderrors.NewFieldRequired("rollingParams")) + } else { + errs = append(errs, validateRollingParams(strategy.RollingParams).Prefix("rollingParams")...) + } case deployapi.DeploymentStrategyTypeCustom: if strategy.CustomParams == nil { errs = append(errs, fielderrors.NewFieldRequired("customParams")) @@ -168,6 +174,24 @@ func validateEnv(vars []kapi.EnvVar) fielderrors.ValidationErrorList { return allErrs } +func validateRollingParams(params *deployapi.RollingDeploymentStrategyParams) fielderrors.ValidationErrorList { + errs := fielderrors.ValidationErrorList{} + + if params.IntervalSeconds != nil && *params.IntervalSeconds < 1 { + errs = append(errs, fielderrors.NewFieldInvalid("intervalSeconds", *params.IntervalSeconds, "must be >0")) + } + + if params.UpdatePeriodSeconds != nil && *params.UpdatePeriodSeconds < 1 { + errs = append(errs, fielderrors.NewFieldInvalid("updatePeriodSeconds", *params.UpdatePeriodSeconds, "must be >0")) + } + + if params.TimeoutSeconds != nil && *params.TimeoutSeconds < 1 { + errs = append(errs, fielderrors.NewFieldInvalid("timeoutSeconds", *params.TimeoutSeconds, "must be >0")) + } + + return errs +} + func validateTrigger(trigger *deployapi.DeploymentTriggerPolicy) fielderrors.ValidationErrorList { errs := fielderrors.ValidationErrorList{} diff --git a/pkg/deploy/api/validation/validation_test.go b/pkg/deploy/api/validation/validation_test.go index 6c2e0297fa64..d04e86d88d47 100644 --- a/pkg/deploy/api/validation/validation_test.go +++ b/pkg/deploy/api/validation/validation_test.go @@ -328,6 +328,63 @@ func TestValidateDeploymentConfigMissingFields(t *testing.T) { fielderrors.ValidationErrorTypeRequired, "template.strategy.recreateParams.pre.execNewPod.containerName", }, + "invalid template.strategy.rollingParams.intervalSeconds": { + api.DeploymentConfig{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"}, + Triggers: manualTrigger(), + Template: api.DeploymentTemplate{ + Strategy: api.DeploymentStrategy{ + Type: api.DeploymentStrategyTypeRolling, + RollingParams: &api.RollingDeploymentStrategyParams{ + IntervalSeconds: mkintp(-20), + UpdatePeriodSeconds: mkintp(1), + TimeoutSeconds: mkintp(1), + }, + }, + ControllerTemplate: test.OkControllerTemplate(), + }, + }, + fielderrors.ValidationErrorTypeInvalid, + "template.strategy.rollingParams.intervalSeconds", + }, + "invalid template.strategy.rollingParams.updatePeriodSeconds": { + api.DeploymentConfig{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"}, + Triggers: manualTrigger(), + Template: api.DeploymentTemplate{ + Strategy: api.DeploymentStrategy{ + Type: api.DeploymentStrategyTypeRolling, + RollingParams: &api.RollingDeploymentStrategyParams{ + IntervalSeconds: mkintp(1), + UpdatePeriodSeconds: mkintp(-20), + TimeoutSeconds: mkintp(1), + }, + }, + ControllerTemplate: test.OkControllerTemplate(), + }, + }, + fielderrors.ValidationErrorTypeInvalid, + "template.strategy.rollingParams.updatePeriodSeconds", + }, + "invalid template.strategy.rollingParams.timeoutSeconds": { + api.DeploymentConfig{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"}, + Triggers: manualTrigger(), + Template: api.DeploymentTemplate{ + Strategy: api.DeploymentStrategy{ + Type: api.DeploymentStrategyTypeRolling, + RollingParams: &api.RollingDeploymentStrategyParams{ + IntervalSeconds: mkintp(1), + UpdatePeriodSeconds: mkintp(1), + TimeoutSeconds: mkintp(-20), + }, + }, + ControllerTemplate: test.OkControllerTemplate(), + }, + }, + fielderrors.ValidationErrorTypeInvalid, + "template.strategy.rollingParams.timeoutSeconds", + }, } for k, v := range errorCases { @@ -464,3 +521,8 @@ func TestValidateDeploymentConfigImageRepositorySupported(t *testing.T) { t.Errorf("expected imageChangeParams.from.kind %s, got %s", e, a) } } + +func mkintp(i int) *int64 { + v := int64(i) + return &v +} diff --git a/pkg/deploy/controller/deployment/factory.go b/pkg/deploy/controller/deployment/factory.go index 3cded92ca1d0..73853fe2d577 100644 --- a/pkg/deploy/controller/deployment/factory.go +++ b/pkg/deploy/controller/deployment/factory.go @@ -28,8 +28,8 @@ type DeploymentControllerFactory struct { Codec runtime.Codec // Environment is a set of environment which should be injected into all deployer pod containers. Environment []kapi.EnvVar - // RecreateStrategyImage specifies which Docker image which should implement the Recreate strategy. - RecreateStrategyImage string + // DeployerImage specifies which Docker image can support the default strategies. + DeployerImage string } // Create creates a DeploymentController. @@ -100,9 +100,9 @@ func (factory *DeploymentControllerFactory) Create() controller.RunnableControll // makeContainer creates containers in the following way: // -// 1. For the Recreate strategy, use the factory's RecreateStrategyImage as -// the container image, and the factory's Environment as the container -// environment. +// 1. For the Recreate and Rolling strategies, strategy, use the factory's +// DeployerImage as the container image, and the factory's Environment +// as the container environment. // 2. For all Custom strategy, use the strategy's image for the container // image, and use the combination of the factory's Environment and the // strategy's environment as the container environment. @@ -117,10 +117,10 @@ func (factory *DeploymentControllerFactory) makeContainer(strategy *deployapi.De // Every strategy type should be handled here. switch strategy.Type { - case deployapi.DeploymentStrategyTypeRecreate: + case deployapi.DeploymentStrategyTypeRecreate, deployapi.DeploymentStrategyTypeRolling: // Use the factory-configured image. return &kapi.Container{ - Image: factory.RecreateStrategyImage, + Image: factory.DeployerImage, Env: environment, }, nil case deployapi.DeploymentStrategyTypeCustom: diff --git a/pkg/deploy/strategy/interfaces.go b/pkg/deploy/strategy/interfaces.go new file mode 100644 index 000000000000..871b637b3e8a --- /dev/null +++ b/pkg/deploy/strategy/interfaces.go @@ -0,0 +1,11 @@ +package strategy + +import ( + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +// DeploymentStrategy knows how to make a deployment active. +type DeploymentStrategy interface { + // Deploy makes a deployment active. + Deploy(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error +} diff --git a/pkg/deploy/strategy/recreate/recreate.go b/pkg/deploy/strategy/recreate/recreate.go index 05e574886623..3318daf8aae9 100644 --- a/pkg/deploy/strategy/recreate/recreate.go +++ b/pkg/deploy/strategy/recreate/recreate.go @@ -58,7 +58,7 @@ func NewRecreateDeploymentStrategy(client kclient.Interface, codec runtime.Codec } // Deploy makes deployment active and disables oldDeployments. -func (s *RecreateDeploymentStrategy) Deploy(deployment *kapi.ReplicationController, oldDeployments []kapi.ObjectReference) error { +func (s *RecreateDeploymentStrategy) Deploy(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error { var err error var deploymentConfig *deployapi.DeploymentConfig diff --git a/pkg/deploy/strategy/recreate/recreate_test.go b/pkg/deploy/strategy/recreate/recreate_test.go index 5313d8f9645f..9a207b1729a9 100644 --- a/pkg/deploy/strategy/recreate/recreate_test.go +++ b/pkg/deploy/strategy/recreate/recreate_test.go @@ -40,7 +40,7 @@ func TestRecreate_initialDeployment(t *testing.T) { // Deployment replicas should follow the config as there's no explicit // desired annotation. deployment, _ = deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) } @@ -51,7 +51,7 @@ func TestRecreate_initialDeployment(t *testing.T) { // Deployment replicas should follow the explicit annotation. deployment, _ = deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) deployment.Annotations[deployapi.DesiredReplicasAnnotation] = "2" - err = strategy.Deploy(deployment, []kapi.ObjectReference{}) + err = strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) } @@ -63,7 +63,7 @@ func TestRecreate_initialDeployment(t *testing.T) { // invalid. deployment, _ = deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) deployment.Annotations[deployapi.DesiredReplicasAnnotation] = "invalid" - err = strategy.Deploy(deployment, []kapi.ObjectReference{}) + err = strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) } @@ -112,12 +112,7 @@ func TestRecreate_secondDeploymentWithSuccessfulRetries(t *testing.T) { }, } - err := strategy.Deploy(newDeployment, []kapi.ObjectReference{ - { - Namespace: oldDeployment.Namespace, - Name: oldDeployment.Name, - }, - }) + err := strategy.Deploy(newDeployment, []*kapi.ReplicationController{oldDeployment}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) @@ -165,12 +160,7 @@ func TestRecreate_secondDeploymentScaleUpRetries(t *testing.T) { }, } - err := strategy.Deploy(newDeployment, []kapi.ObjectReference{ - { - Namespace: oldDeployment.Namespace, - Name: oldDeployment.Name, - }, - }) + err := strategy.Deploy(newDeployment, []*kapi.ReplicationController{oldDeployment}) if err == nil { t.Fatalf("expected a deploy error: %#v", err) @@ -222,12 +212,7 @@ func TestRecreate_secondDeploymentScaleDownRetries(t *testing.T) { }, } - err := strategy.Deploy(newDeployment, []kapi.ObjectReference{ - { - Namespace: oldDeployment.Namespace, - Name: oldDeployment.Name, - }, - }) + err := strategy.Deploy(newDeployment, []*kapi.ReplicationController{oldDeployment}) if err == nil { t.Fatalf("expected a deploy error: %#v", err) @@ -264,7 +249,7 @@ func TestRecreate_deploymentPreHookSuccess(t *testing.T) { }, } - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) @@ -305,7 +290,7 @@ func TestRecreate_deploymentPreHookFailAbort(t *testing.T) { }, } - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err == nil { t.Fatalf("expected a deploy error") } @@ -338,7 +323,7 @@ func TestRecreate_deploymentPreHookFailureIgnored(t *testing.T) { }, } - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) @@ -384,7 +369,7 @@ func TestRecreate_deploymentPreHookFailureRetried(t *testing.T) { }, } - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) @@ -425,7 +410,7 @@ func TestRecreate_deploymentPostHookSuccess(t *testing.T) { }, } - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) @@ -466,7 +451,7 @@ func TestRecreate_deploymentPostHookAbortUnsupported(t *testing.T) { }, } - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) @@ -507,7 +492,7 @@ func TestRecreate_deploymentPostHookFailIgnore(t *testing.T) { }, } - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) @@ -553,7 +538,7 @@ func TestRecreate_deploymentPostHookFailureRetried(t *testing.T) { }, } - err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) if err != nil { t.Fatalf("unexpected deploy error: %#v", err) diff --git a/pkg/deploy/strategy/rolling/rolling.go b/pkg/deploy/strategy/rolling/rolling.go new file mode 100644 index 000000000000..7a4f9bdcb7ca --- /dev/null +++ b/pkg/deploy/strategy/rolling/rolling.go @@ -0,0 +1,215 @@ +package rolling + +import ( + "fmt" + "strconv" + "time" + + "github.com/golang/glog" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait" + + deployapi "github.com/openshift/origin/pkg/deploy/api" + "github.com/openshift/origin/pkg/deploy/strategy" + deployutil "github.com/openshift/origin/pkg/deploy/util" +) + +// TODO: This should perhaps be made public upstream. See: +// https://github.com/GoogleCloudPlatform/kubernetes/issues/7851 +const sourceIdAnnotation = "kubectl.kubernetes.io/update-source-id" + +// RollingDeploymentStrategy is a Strategy which implements rolling +// deployments using the upstream Kubernetes RollingUpdater. +// +// Currently, there are some caveats: +// +// 1. When there is no existing prior deployment, deployment delegates to +// another strategy. +// 2. The interface to the RollingUpdater is not very clean. +// +// These caveats can be resolved with future upstream refactorings to +// RollingUpdater[1][2]. +// +// [1] https://github.com/GoogleCloudPlatform/kubernetes/pull/7183 +// [2] https://github.com/GoogleCloudPlatform/kubernetes/issues/7851 +type RollingDeploymentStrategy struct { + // initialStrategy is used when there are no prior deployments. + initialStrategy strategy.DeploymentStrategy + // client is used to deal with ReplicationControllers. + client kubectl.RollingUpdaterClient + // rollingUpdate knows how to perform a rolling update. + rollingUpdate func(config *kubectl.RollingUpdaterConfig) error + // codec is used to access the encoded config on a deployment. + codec runtime.Codec +} + +// NewRollingDeploymentStrategy makes a new RollingDeploymentStrategy. +func NewRollingDeploymentStrategy(namespace string, client kclient.Interface, codec runtime.Codec, initialStrategy strategy.DeploymentStrategy) *RollingDeploymentStrategy { + updaterClient := &rollingUpdaterClient{ + ControllerHasDesiredReplicasFn: func(rc *kapi.ReplicationController) wait.ConditionFunc { + return kclient.ControllerHasDesiredReplicas(client, rc) + }, + GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) { + return client.ReplicationControllers(namespace).Get(name) + }, + UpdateReplicationControllerFn: func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) { + return client.ReplicationControllers(namespace).Update(rc) + }, + // This guards against the RollingUpdater's built-in behavior to create + // RCs when the supplied old RC is nil. We won't pass nil, but it doesn't + // hurt to further guard against it since we would have no way to identify + // or clean up orphaned RCs RollingUpdater might inadvertently create. + CreateReplicationControllerFn: func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) { + return nil, fmt.Errorf("unexpected attempt to create deployment: %#v", rc) + }, + // We give the RollingUpdater a policy which should prevent it from + // deleting the source deployment after the transition, but it doesn't + // hurt to guard by removing its ability to delete. + DeleteReplicationControllerFn: func(namespace, name string) error { + return fmt.Errorf("unexpected attempt to delete %s/%s", namespace, name) + }, + } + return &RollingDeploymentStrategy{ + codec: codec, + initialStrategy: initialStrategy, + client: updaterClient, + rollingUpdate: func(config *kubectl.RollingUpdaterConfig) error { + updater := kubectl.NewRollingUpdater(namespace, updaterClient) + return updater.Update(config) + }, + } +} + +func (s *RollingDeploymentStrategy) Deploy(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error { + config, err := deployutil.DecodeDeploymentConfig(deployment, s.codec) + if err != nil { + return fmt.Errorf("Couldn't decode DeploymentConfig from deployment %s: %v", deployment.Name, err) + } + + // Find the latest deployment (if any). + latest, err := s.findLatestDeployment(oldDeployments) + if err != nil { + return fmt.Errorf("couldn't determine latest deployment: %v", err) + } + + // If there's no prior deployment, delegate to another strategy since the + // rolling updater only supports transitioning between two deployments. + if latest == nil { + return s.initialStrategy.Deploy(deployment, oldDeployments) + } + + // HACK: Assign the source ID annotation that the rolling updater expects, + // unless it already exists on the deployment. + // + // Related upstream issue: + // https://github.com/GoogleCloudPlatform/kubernetes/pull/7183 + if _, hasSourceId := deployment.Annotations[sourceIdAnnotation]; !hasSourceId { + deployment.Annotations[sourceIdAnnotation] = fmt.Sprintf("%s:%s", latest.Name, latest.ObjectMeta.UID) + if updated, err := s.client.UpdateReplicationController(deployment.Namespace, deployment); err != nil { + return fmt.Errorf("couldn't assign source annotation to deployment %s/%s: %v", deployment.Namespace, deployment.Name, err) + } else { + deployment = updated + } + } + + // HACK: There's a validation in the rolling updater which assumes that when + // an existing RC is supplied, it will have >0 replicas- a validation which + // is then disregarded as the desired count is obtained from the annotation + // on the RC. For now, fake it out by just setting replicas to 1. + // + // Related upstream issue: + // https://github.com/GoogleCloudPlatform/kubernetes/pull/7183 + deployment.Spec.Replicas = 1 + + glog.Infof("OldRc: %s, replicas=%d", latest.Name, latest.Spec.Replicas) + // Perform a rolling update. + params := config.Template.Strategy.RollingParams + rollingConfig := &kubectl.RollingUpdaterConfig{ + Out: &rollingUpdaterWriter{}, + OldRc: latest, + NewRc: deployment, + UpdatePeriod: time.Duration(*params.UpdatePeriodSeconds) * time.Second, + Interval: time.Duration(*params.IntervalSeconds) * time.Second, + Timeout: time.Duration(*params.TimeoutSeconds) * time.Second, + CleanupPolicy: kubectl.PreserveRollingUpdateCleanupPolicy, + } + glog.Infof("Starting rolling update with config: %#v (UpdatePeriod %d, Interval %d, Timeout %d) (UpdatePeriodSeconds %d, IntervalSeconds %d, TimeoutSeconds %d)", + rollingConfig, + rollingConfig.UpdatePeriod, + rollingConfig.Interval, + rollingConfig.Timeout, + *params.UpdatePeriodSeconds, + *params.IntervalSeconds, + *params.TimeoutSeconds, + ) + return s.rollingUpdate(rollingConfig) +} + +// findLatestDeployment retrieves deployments identified by oldDeployments and +// returns the latest one from the list, or nil if there are no old +// deployments. +func (s *RollingDeploymentStrategy) findLatestDeployment(oldDeployments []*kapi.ReplicationController) (*kapi.ReplicationController, error) { + // Find the latest deployment from the list of old deployments. + var latest *kapi.ReplicationController + latestVersion := 0 + for _, deployment := range oldDeployments { + if val, hasVersion := deployment.Annotations[deployapi.DeploymentVersionAnnotation]; hasVersion { + version, err := strconv.Atoi(val) + if err != nil { + return nil, fmt.Errorf("deployment %s/%s has invalid version annotation value '%s': %v", deployment.Namespace, deployment.Name, val, err) + } + if version > latestVersion { + latest = deployment + latestVersion = version + } + } else { + glog.Infof("Ignoring deployment with missing version annotation: %s/%s", deployment.Namespace, deployment.Name) + } + } + if latest != nil { + glog.Infof("Found latest deployment %s", latest.Name) + } else { + glog.Info("No latest deployment found") + } + return latest, nil +} + +type rollingUpdaterClient struct { + GetReplicationControllerFn func(namespace, name string) (*kapi.ReplicationController, error) + UpdateReplicationControllerFn func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) + CreateReplicationControllerFn func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) + DeleteReplicationControllerFn func(namespace, name string) error + ControllerHasDesiredReplicasFn func(rc *kapi.ReplicationController) wait.ConditionFunc +} + +func (c *rollingUpdaterClient) GetReplicationController(namespace, name string) (*kapi.ReplicationController, error) { + return c.GetReplicationControllerFn(namespace, name) +} + +func (c *rollingUpdaterClient) UpdateReplicationController(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) { + return c.UpdateReplicationControllerFn(namespace, rc) +} + +func (c *rollingUpdaterClient) CreateReplicationController(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) { + return c.CreateReplicationControllerFn(namespace, rc) +} + +func (c *rollingUpdaterClient) DeleteReplicationController(namespace, name string) error { + return c.DeleteReplicationControllerFn(namespace, name) +} + +func (c *rollingUpdaterClient) ControllerHasDesiredReplicas(rc *kapi.ReplicationController) wait.ConditionFunc { + return c.ControllerHasDesiredReplicasFn(rc) +} + +// rollingUpdaterWriter is an io.Writer that delegates to glog. +type rollingUpdaterWriter struct{} + +func (w *rollingUpdaterWriter) Write(p []byte) (n int, err error) { + glog.Info(fmt.Sprintf("RollingUpdater: %s", p)) + return len(p), nil +} diff --git a/pkg/deploy/strategy/rolling/rolling_test.go b/pkg/deploy/strategy/rolling/rolling_test.go new file mode 100644 index 000000000000..84547d949efa --- /dev/null +++ b/pkg/deploy/strategy/rolling/rolling_test.go @@ -0,0 +1,238 @@ +package rolling + +import ( + "fmt" + "testing" + "time" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + + api "github.com/openshift/origin/pkg/api/latest" + deployapi "github.com/openshift/origin/pkg/deploy/api" + deploytest "github.com/openshift/origin/pkg/deploy/api/test" + deployutil "github.com/openshift/origin/pkg/deploy/util" +) + +func TestRolling_deployInitial(t *testing.T) { + initialStrategyInvoked := false + + strategy := &RollingDeploymentStrategy{ + codec: api.Codec, + client: &rollingUpdaterClient{ + GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) { + t.Fatalf("unexpected call to GetReplicationController") + return nil, nil + }, + }, + initialStrategy: &testStrategy{ + deployFn: func(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error { + initialStrategyInvoked = true + return nil + }, + }, + rollingUpdate: func(config *kubectl.RollingUpdaterConfig) error { + t.Fatalf("unexpected call to rollingUpdate") + return nil + }, + } + + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) + err := strategy.Deploy(deployment, []*kapi.ReplicationController{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !initialStrategyInvoked { + t.Fatalf("expected initial strategy to be invoked") + } +} + +func TestRolling_deployRolling(t *testing.T) { + latest, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) + config := deploytest.OkDeploymentConfig(2) + config.Template.Strategy = deployapi.DeploymentStrategy{ + Type: deployapi.DeploymentStrategyTypeRolling, + RollingParams: &deployapi.RollingDeploymentStrategyParams{ + IntervalSeconds: mkintp(1), + UpdatePeriodSeconds: mkintp(2), + TimeoutSeconds: mkintp(3), + }, + } + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + var rollingConfig *kubectl.RollingUpdaterConfig + deploymentUpdated := false + strategy := &RollingDeploymentStrategy{ + codec: api.Codec, + client: &rollingUpdaterClient{ + GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) { + if name != latest.Name { + t.Fatalf("unexpected call to GetReplicationController for %s", name) + } + return latest, nil + }, + UpdateReplicationControllerFn: func(namespace string, rc *kapi.ReplicationController) (*kapi.ReplicationController, error) { + if rc.Name != deployment.Name { + t.Fatalf("unexpected call to UpdateReplicationController for %s", rc.Name) + } + deploymentUpdated = true + return rc, nil + }, + }, + initialStrategy: &testStrategy{ + deployFn: func(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error { + t.Fatalf("unexpected call to initial strategy") + return nil + }, + }, + rollingUpdate: func(config *kubectl.RollingUpdaterConfig) error { + rollingConfig = config + return nil + }, + } + + err := strategy.Deploy(deployment, []*kapi.ReplicationController{latest}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rollingConfig == nil { + t.Fatalf("expected rolling update to be invoked") + } + + if e, a := latest, rollingConfig.OldRc; e != a { + t.Errorf("expected rollingConfig.OldRc %v, got %v", e, a) + } + + if e, a := deployment, rollingConfig.NewRc; e != a { + t.Errorf("expected rollingConfig.NewRc %v, got %v", e, a) + } + + if e, a := 1*time.Second, rollingConfig.Interval; e != a { + t.Errorf("expected Interval %d, got %d", e, a) + } + + if e, a := 2*time.Second, rollingConfig.UpdatePeriod; e != a { + t.Errorf("expected UpdatePeriod %d, got %d", e, a) + } + + if e, a := 3*time.Second, rollingConfig.Timeout; e != a { + t.Errorf("expected Timeout %d, got %d", e, a) + } + + // verify hack + if e, a := 1, rollingConfig.NewRc.Spec.Replicas; e != a { + t.Errorf("expected rollingConfig.NewRc.Spec.Replicas %d, got %d", e, a) + } + + // verify hack + if !deploymentUpdated { + t.Errorf("expected deployment to be updated for source annotation") + } + sid := fmt.Sprintf("%s:%s", latest.Name, latest.ObjectMeta.UID) + if e, a := sid, rollingConfig.NewRc.Annotations[sourceIdAnnotation]; e != a { + t.Errorf("expected sourceIdAnnotation %s, got %s", e, a) + } +} + +func TestRolling_findLatestDeployment(t *testing.T) { + deployments := map[string]*kapi.ReplicationController{} + for i := 1; i <= 10; i++ { + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(i), kapi.Codec) + deployments[deployment.Name] = deployment + } + + ignoredDeployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(12), kapi.Codec) + delete(ignoredDeployment.Annotations, deployapi.DeploymentVersionAnnotation) + deployments[ignoredDeployment.Name] = ignoredDeployment + + strategy := &RollingDeploymentStrategy{ + codec: api.Codec, + client: &rollingUpdaterClient{ + GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) { + deployment, found := deployments[name] + if !found { + return nil, kerrors.NewNotFound("ReplicationController", name) + } + return deployment, nil + }, + }, + } + + type scenario struct { + old []string + latest string + } + + scenarios := []scenario{ + { + old: []string{ + "config-1", + "config-2", + "config-3", + }, + latest: "config-3", + }, + { + old: []string{ + "config-3", + "config-1", + "config-7", + ignoredDeployment.Name, + }, + latest: "config-7", + }, + } + + for _, scenario := range scenarios { + old := []*kapi.ReplicationController{} + for _, oldName := range scenario.old { + old = append(old, deployments[oldName]) + } + found, err := strategy.findLatestDeployment(old) + if err != nil { + t.Errorf("unexpected error for scenario: %v: %v", scenario, err) + continue + } + + if found == nil { + t.Errorf("expected to find a deployment for scenario: %v", scenario) + continue + } + + if e, a := scenario.latest, found.Name; e != a { + t.Errorf("expected latest %s, got %s for scenario: %v", e, a, scenario) + } + } +} + +func TestRolling_findLatestDeploymentInvalidDeployment(t *testing.T) { + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) + deployment.Annotations[deployapi.DeploymentVersionAnnotation] = "" + + strategy := &RollingDeploymentStrategy{ + codec: api.Codec, + client: &rollingUpdaterClient{ + GetReplicationControllerFn: func(namespace, name string) (*kapi.ReplicationController, error) { + return deployment, nil + }, + }, + } + _, err := strategy.findLatestDeployment([]*kapi.ReplicationController{deployment}) + if err == nil { + t.Errorf("expected an error") + } +} + +type testStrategy struct { + deployFn func(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error +} + +func (s *testStrategy) Deploy(deployment *kapi.ReplicationController, oldDeployments []*kapi.ReplicationController) error { + return s.deployFn(deployment, oldDeployments) +} + +func mkintp(i int) *int64 { + v := int64(i) + return &v +}