From 6326f5d871ac7190187af0e4ced8461b4b94d75b Mon Sep 17 00:00:00 2001 From: Dan Mace Date: Tue, 7 Apr 2015 14:35:20 -0400 Subject: [PATCH 1/4] Implement deployment hooks --- pkg/cmd/cli/describe/deployments.go | 59 +-- pkg/cmd/cli/describe/describer_test.go | 31 ++ pkg/deploy/api/types.go | 47 +++ pkg/deploy/api/v1beta1/types.go | 47 +++ pkg/deploy/strategy/recreate/recreate.go | 155 ++++++- pkg/deploy/strategy/recreate/recreate_test.go | 383 +++++++++++++++++- pkg/deploy/strategy/support/doc.go | 2 + pkg/deploy/strategy/support/lifecycle.go | 135 ++++++ pkg/deploy/strategy/support/lifecycle_test.go | 223 ++++++++++ 9 files changed, 1044 insertions(+), 38 deletions(-) create mode 100644 pkg/deploy/strategy/support/doc.go create mode 100644 pkg/deploy/strategy/support/lifecycle.go create mode 100644 pkg/deploy/strategy/support/lifecycle_test.go diff --git a/pkg/cmd/cli/describe/deployments.go b/pkg/cmd/cli/describe/deployments.go index aa12f8b6c094..d34cb0d2c87f 100644 --- a/pkg/cmd/cli/describe/deployments.go +++ b/pkg/cmd/cli/describe/deployments.go @@ -93,8 +93,10 @@ func (d *DeploymentConfigDescriber) Describe(namespace, name string) (string, er formatString(out, "Latest Version", strconv.Itoa(deploymentConfig.LatestVersion)) } - printStrategy(deploymentConfig.Template.Strategy, out) printTriggers(deploymentConfig.Triggers, out) + + formatString(out, "Strategy", deploymentConfig.Template.Strategy.Type) + printStrategy(deploymentConfig.Template.Strategy, out) printReplicationControllerSpec(deploymentConfig.Template.ControllerTemplate, out) deploymentName := deployutil.LatestDeploymentNameForConfig(deploymentConfig) @@ -113,53 +115,64 @@ func (d *DeploymentConfigDescriber) Describe(namespace, name string) (string, er }) } -func printStrategy(strategy deployapi.DeploymentStrategy, w io.Writer) { - fmt.Fprintf(w, "Strategy:\t%s\n", strategy.Type) +func printStrategy(strategy deployapi.DeploymentStrategy, w *tabwriter.Writer) { switch strategy.Type { case deployapi.DeploymentStrategyTypeRecreate: + if strategy.RecreateParams != nil { + pre := strategy.RecreateParams.Pre + post := strategy.RecreateParams.Post + if pre != nil { + printHook("Pre-deployment", pre, w) + } + if post != nil { + printHook("Post-deployment", post, w) + } + } case deployapi.DeploymentStrategyTypeCustom: - fmt.Fprintf(w, "\t- Image:\t%s\n", strategy.CustomParams.Image) + fmt.Fprintf(w, "\t Image:\t%s\n", strategy.CustomParams.Image) if len(strategy.CustomParams.Environment) > 0 { - fmt.Fprintf(w, "\t- Environment:\t%s\n", formatLabels(convertEnv(strategy.CustomParams.Environment))) + fmt.Fprintf(w, "\t Environment:\t%s\n", formatLabels(convertEnv(strategy.CustomParams.Environment))) } if len(strategy.CustomParams.Command) > 0 { - fmt.Fprintf(w, "\t- Command:\t%v\n", strings.Join(strategy.CustomParams.Command, " ")) + fmt.Fprintf(w, "\t Command:\t%v\n", strings.Join(strategy.CustomParams.Command, " ")) } } } -func printTriggers(triggers []deployapi.DeploymentTriggerPolicy, w io.Writer) { +func printHook(prefix string, hook *deployapi.LifecycleHook, w io.Writer) { + if hook.ExecNewPod != nil { + fmt.Fprintf(w, "\t %s hook (pod type, failure policy: %s)\n", prefix, hook.FailurePolicy) + fmt.Fprintf(w, "\t Container:\t%s\n", hook.ExecNewPod.ContainerName) + fmt.Fprintf(w, "\t Command:\t%v\n", strings.Join(hook.ExecNewPod.Command, " ")) + fmt.Fprintf(w, "\t Env:\t%s\n", formatLabels(convertEnv(hook.ExecNewPod.Env))) + } +} + +func printTriggers(triggers []deployapi.DeploymentTriggerPolicy, w *tabwriter.Writer) { if len(triggers) == 0 { - fmt.Fprint(w, "Triggers:\t\n") + formatString(w, "Triggers", "") return } - fmt.Fprint(w, "Triggers:\n") + labels := []string{} + for _, t := range triggers { - fmt.Fprintf(w, "\t- %s\n", t.Type) switch t.Type { case deployapi.DeploymentTriggerOnConfigChange: - fmt.Fprintf(w, "\t\t\n") + labels = append(labels, "Config") case deployapi.DeploymentTriggerOnImageChange: if len(t.ImageChangeParams.RepositoryName) > 0 { - fmt.Fprintf(w, "\t\tAutomatic:\t%v\n\t\tRepository:\t%s\n\t\tTag:\t%s\n", - t.ImageChangeParams.Automatic, - t.ImageChangeParams.RepositoryName, - t.ImageChangeParams.Tag, - ) + labels = append(labels, fmt.Sprintf("Image(%s@%s, auto=%v)", t.ImageChangeParams.RepositoryName, t.ImageChangeParams.Tag, t.ImageChangeParams.Automatic)) } else if len(t.ImageChangeParams.From.Name) > 0 { - fmt.Fprintf(w, "\t\tAutomatic:\t%v\n\t\tImage Repository:\t%s\n\t\tTag:\t%s\n", - t.ImageChangeParams.Automatic, - t.ImageChangeParams.From.Name, - t.ImageChangeParams.Tag, - ) + labels = append(labels, fmt.Sprintf("Image(%s@%s, auto=%v)", t.ImageChangeParams.From.Name, t.ImageChangeParams.Tag, t.ImageChangeParams.Automatic)) } - default: - fmt.Fprint(w, "unknown\n") } } + + desc := strings.Join(labels, ", ") + formatString(w, "Triggers", desc) } func printReplicationControllerSpec(spec kapi.ReplicationControllerSpec, w io.Writer) error { diff --git a/pkg/cmd/cli/describe/describer_test.go b/pkg/cmd/cli/describe/describer_test.go index 6d20f5937a1c..c7261035e44d 100644 --- a/pkg/cmd/cli/describe/describer_test.go +++ b/pkg/cmd/cli/describe/describer_test.go @@ -107,6 +107,37 @@ func TestDeploymentConfigDescriber(t *testing.T) { config.Triggers[0].ImageChangeParams.RepositoryName = "" config.Triggers[0].ImageChangeParams.From = kapi.ObjectReference{Name: "imageRepo"} describe() + + config.Template.Strategy = deployapitest.OkStrategy() + config.Template.Strategy.RecreateParams = &deployapi.RecreateDeploymentStrategyParams{ + Pre: &deployapi.LifecycleHook{ + FailurePolicy: deployapi.LifecycleHookFailurePolicyAbort, + ExecNewPod: &deployapi.ExecNewPodHook{ + ContainerName: "container", + Command: []string{"/command1", "args"}, + Env: []kapi.EnvVar{ + { + Name: "KEY1", + Value: "value1", + }, + }, + }, + }, + Post: &deployapi.LifecycleHook{ + FailurePolicy: deployapi.LifecycleHookFailurePolicyIgnore, + ExecNewPod: &deployapi.ExecNewPodHook{ + ContainerName: "container", + Command: []string{"/command2", "args"}, + Env: []kapi.EnvVar{ + { + Name: "KEY2", + Value: "value2", + }, + }, + }, + }, + } + describe() } func mkPod(status kapi.PodPhase, exitCode int) *kapi.Pod { diff --git a/pkg/deploy/api/types.go b/pkg/deploy/api/types.go index abbe9e5bf180..5cec3a54d68e 100644 --- a/pkg/deploy/api/types.go +++ b/pkg/deploy/api/types.go @@ -52,6 +52,8 @@ type DeploymentStrategy struct { Type DeploymentStrategyType `json:"type,omitempty"` // CustomParams are the input to the Custom deployment strategy. CustomParams *CustomDeploymentStrategyParams `json:"customParams,omitempty"` + // RecreateParams are the input to the Recreate deployment strategy. + RecreateParams *RecreateDeploymentStrategyParams `json:"recreateParams,omitempty"` } // DeploymentStrategyType refers to a specific DeploymentStrategy implementation. @@ -74,6 +76,51 @@ type CustomDeploymentStrategyParams struct { Command []string `json:"command,omitempty"` } +// RecreateDeploymentStrategyParams are the input to the Recreate deployment +// strategy. +type RecreateDeploymentStrategyParams struct { + // Pre is a lifecycle hook which is executed before the strategy manipulates + // the deployment. All LifecycleHookFailurePolicy values are supported. + Pre *LifecycleHook `json:"pre"` + // Post is a lifecycle hook which is executed after the strategy has + // finished all deployment logic. The LifecycleHookFailurePolicyAbort policy + // is NOT supported. + Post *LifecycleHook `json:"post"` +} + +// Handler defines a specific deployment lifecycle action. +type LifecycleHook struct { + // FailurePolicy specifies what action to take if the hook fails. + FailurePolicy LifecycleHookFailurePolicy `json:"failurePolicy"` + // ExecNewPod specifies the options for a lifecycle hook backed by a pod. + ExecNewPod *ExecNewPodHook `json:"execNewPod,omitempty"` +} + +// HandlerFailurePolicy describes possibles actions to take if a hook fails. +type LifecycleHookFailurePolicy string + +const ( + // LifecycleHookFailurePolicyRetry means retry the hook until it succeeds. + LifecycleHookFailurePolicyRetry LifecycleHookFailurePolicy = "Retry" + // LifecycleHookFailurePolicyAbort means abort the deployment (if possible). + LifecycleHookFailurePolicyAbort LifecycleHookFailurePolicy = "Abort" + // LifecycleHookFailurePolicyIgnore means ignore failure and continue the deployment. + LifecycleHookFailurePolicyIgnore LifecycleHookFailurePolicy = "Ignore" +) + +// ExecNewPodHook is a hook implementation which runs a command in a new pod +// based on the specified container which is assumed to be part of the +// deployment template. +type ExecNewPodHook struct { + // Command is the action command and its arguments. + Command []string `json:"command"` + // Env is a set of environment variables to supply to the hook pod's container. + Env []kapi.EnvVar `json:"env,omitempty"` + // ContainerName is the name of a container in the deployment pod template + // whose Docker image will be used for the hook pod's container. + ContainerName string `json:"containerName"` +} + // A DeploymentList is a collection of deployments. // DEPRECATED: Like Deployment, this is no longer used. type DeploymentList struct { diff --git a/pkg/deploy/api/v1beta1/types.go b/pkg/deploy/api/v1beta1/types.go index 0c673a2f8f65..02d44474bea4 100644 --- a/pkg/deploy/api/v1beta1/types.go +++ b/pkg/deploy/api/v1beta1/types.go @@ -53,6 +53,8 @@ type DeploymentStrategy struct { Type DeploymentStrategyType `json:"type,omitempty"` // CustomParams are the input to the Custom deployment strategy. CustomParams *CustomDeploymentStrategyParams `json:"customParams,omitempty"` + // RecreateParams are the input to the Recreate deployment strategy. + RecreateParams *RecreateDeploymentStrategyParams `json:"recreateParams,omitempty"` } // DeploymentStrategyType refers to a specific DeploymentStrategy implementation. @@ -75,6 +77,51 @@ type CustomDeploymentStrategyParams struct { Command []string `json:"command,omitempty"` } +// RecreateDeploymentStrategyParams are the input to the Recreate deployment +// strategy. +type RecreateDeploymentStrategyParams struct { + // Pre is a lifecycle hook which is executed before the strategy manipulates + // the deployment. All LifecycleHookFailurePolicy values are supported. + Pre *LifecycleHook `json:"pre"` + // Post is a lifecycle hook which is executed after the strategy has + // finished all deployment logic. The LifecycleHookFailurePolicyAbort policy + // is NOT supported. + Post *LifecycleHook `json:"post"` +} + +// Handler defines a specific deployment lifecycle action. +type LifecycleHook struct { + // FailurePolicy specifies what action to take if the hook fails. + FailurePolicy LifecycleHookFailurePolicy `json:"failurePolicy"` + // ExecNewPod specifies the options for a lifecycle hook backed by a pod. + ExecNewPod *ExecNewPodHook `json:"execNewPod,omitempty"` +} + +// HandlerFailurePolicy describes possibles actions to take if a hook fails. +type LifecycleHookFailurePolicy string + +const ( + // LifecycleHookFailurePolicyRetry means retry the hook until it succeeds. + LifecycleHookFailurePolicyRetry LifecycleHookFailurePolicy = "Retry" + // LifecycleHookFailurePolicyAbort means abort the deployment (if possible). + LifecycleHookFailurePolicyAbort LifecycleHookFailurePolicy = "Abort" + // LifecycleHookFailurePolicyIgnore means ignore failure and continue the deployment. + LifecycleHookFailurePolicyIgnore LifecycleHookFailurePolicy = "Ignore" +) + +// ExecNewPodHook is a hook implementation which runs a command in a new pod +// based on the specified container which is assumed to be part of the +// deployment template. +type ExecNewPodHook struct { + // Command is the action command and its arguments. + Command []string `json:"command"` + // Env is a set of environment variables to supply to the hook pod's container. + Env []kapi.EnvVar `json:"env,omitempty"` + // ContainerName is the name of a container in the deployment pod template + // whose Docker image will be used for the hook pod's container. + ContainerName string `json:"containerName"` +} + // A DeploymentList is a collection of deployments. // DEPRECATED: Like Deployment, this is no longer used. type DeploymentList struct { diff --git a/pkg/deploy/strategy/recreate/recreate.go b/pkg/deploy/strategy/recreate/recreate.go index 5504ffc77905..8099d789d712 100644 --- a/pkg/deploy/strategy/recreate/recreate.go +++ b/pkg/deploy/strategy/recreate/recreate.go @@ -10,30 +10,47 @@ import ( kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" deployapi "github.com/openshift/origin/pkg/deploy/api" + stratsupport "github.com/openshift/origin/pkg/deploy/strategy/support" deployutil "github.com/openshift/origin/pkg/deploy/util" ) -// RecreateDeploymentStrategy is a simple strategy appropriate as a default. Its behavior is to increase the -// replica count of the new deployment to 1, and to decrease the replica count of previous deployments -// to zero. +// RecreateDeploymentStrategy is a simple strategy appropriate as a default. +// Its behavior is to increase the replica count of the new deployment to 1, +// and to decrease the replica count of previous deployments to zero. // -// A failure to disable any existing deployments will be considered a deployment failure. +// A failure to disable any existing deployments will be considered a +// deployment failure. type RecreateDeploymentStrategy struct { // client is used to interact with ReplicatonControllers. client replicationControllerClient // codec is used to decode DeploymentConfigs contained in deployments. codec runtime.Codec + // hookExecutor can execute a lifecycle hook. + hookExecutor hookExecutor retryTimeout time.Duration retryPeriod time.Duration } +// NewRecreateDeploymentStrategy makes a RecreateDeploymentStrategy backed by +// a real HookExecutor and client. func NewRecreateDeploymentStrategy(client kclient.Interface, codec runtime.Codec) *RecreateDeploymentStrategy { return &RecreateDeploymentStrategy{ - client: &realReplicationController{client}, - codec: codec, + client: &realReplicationControllerClient{client}, + codec: codec, + hookExecutor: &stratsupport.HookExecutor{ + PodClient: &stratsupport.HookExecutorPodClientImpl{ + CreatePodFunc: func(namespace string, pod *kapi.Pod) (*kapi.Pod, error) { + return client.Pods(namespace).Create(pod) + }, + WatchPodFunc: func(namespace, name string) (watch.Interface, error) { + return newPodWatch(client, namespace, name, 5*time.Second), nil + }, + }, + }, retryTimeout: 10 * time.Second, retryPeriod: 1 * time.Second, } @@ -48,11 +65,37 @@ func (s *RecreateDeploymentStrategy) Deploy(deployment *kapi.ReplicationControll return fmt.Errorf("Couldn't decode DeploymentConfig from deployment %s: %v", deployment.Name, err) } + // Execute any pre-hook. + if deploymentConfig.Template.Strategy.RecreateParams != nil { + preHook := deploymentConfig.Template.Strategy.RecreateParams.Pre + if preHook != nil { + preHookLoop: + for { + err := s.hookExecutor.Execute(preHook, deployment) + if err == nil { + glog.Info("Pre hook finished successfully") + break + } + switch preHook.FailurePolicy { + case deployapi.LifecycleHookFailurePolicyAbort: + return fmt.Errorf("Pre hook failed, aborting: %s", err) + case deployapi.LifecycleHookFailurePolicyIgnore: + glog.Infof("Pre hook failed, ignoring: %s", err) + break preHookLoop + case deployapi.LifecycleHookFailurePolicyRetry: + glog.Infof("Pre hook failed, retrying: %s", err) + time.Sleep(s.retryPeriod) + } + } + } + } + + // Scale up the new deployment. if err = s.updateReplicas(deployment.Namespace, deployment.Name, deploymentConfig.Template.ControllerTemplate.Replicas); err != nil { return err } - // For this simple deploy, disable previous replication controllers. + // Disable any old deployments. glog.Infof("Found %d prior deployments to disable", len(oldDeployments)) allProcessed := true for _, oldDeployment := range oldDeployments { @@ -62,6 +105,30 @@ func (s *RecreateDeploymentStrategy) Deploy(deployment *kapi.ReplicationControll } } + // Execute any post-hook. + if deploymentConfig.Template.Strategy.RecreateParams != nil { + postHook := deploymentConfig.Template.Strategy.RecreateParams.Post + if postHook != nil { + postHookLoop: + for { + err := s.hookExecutor.Execute(postHook, deployment) + if err == nil { + glog.Info("Post hook finished successfully") + break + } + switch postHook.FailurePolicy { + case deployapi.LifecycleHookFailurePolicyIgnore, deployapi.LifecycleHookFailurePolicyAbort: + // Abort isn't supported here, so treat it like ignore. + glog.Infof("Post hook failed, ignoring: %s", err) + break postHookLoop + case deployapi.LifecycleHookFailurePolicyRetry: + glog.Infof("Post hook failed, retrying: %s", err) + time.Sleep(s.retryPeriod) + } + } + } + } + if !allProcessed { return fmt.Errorf("Failed to disable all prior deployments for new deployment %s", deployment.Name) } @@ -101,19 +168,87 @@ func (s *RecreateDeploymentStrategy) updateReplicas(namespace, name string, repl } } +// replicationControllerClient provides access to ReplicationControllers. type replicationControllerClient interface { getReplicationController(namespace, name string) (*kapi.ReplicationController, error) updateReplicationController(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) } -type realReplicationController struct { +// realReplicationControllerClient is a replicationControllerClient which uses +// a Kube client. +type realReplicationControllerClient struct { client kclient.Interface } -func (r realReplicationController) getReplicationController(namespace string, name string) (*kapi.ReplicationController, error) { +func (r *realReplicationControllerClient) getReplicationController(namespace string, name string) (*kapi.ReplicationController, error) { return r.client.ReplicationControllers(namespace).Get(name) } -func (r realReplicationController) updateReplicationController(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { +func (r *realReplicationControllerClient) updateReplicationController(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { return r.client.ReplicationControllers(namespace).Update(ctrl) } + +// hookExecutor knows how to execute a deployment lifecycle hook. +type hookExecutor interface { + Execute(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error +} + +// hookExecutorImpl is a pluggable hookExecutor. +type hookExecutorImpl struct { + executeFunc func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error +} + +func (i *hookExecutorImpl) Execute(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return i.executeFunc(hook, deployment) +} + +// podWatch provides watch semantics for a pod backed by a poller, since +// events aren't generated for pod status updates. +type podWatch struct { + result chan watch.Event + stop chan bool +} + +// newPodWatch makes a new podWatch. +func newPodWatch(client kclient.Interface, namespace, name string, period time.Duration) *podWatch { + pods := make(chan watch.Event) + stop := make(chan bool) + tick := time.NewTicker(period) + go func() { + for { + select { + case <-stop: + return + case <-tick.C: + pod, err := client.Pods(namespace).Get(name) + if err != nil { + pods <- watch.Event{ + Type: watch.Error, + Object: &kapi.Status{ + Status: "Failure", + Message: fmt.Sprintf("couldn't get pod %s/%s: %s", namespace, name, err), + }, + } + continue + } + pods <- watch.Event{ + Type: watch.Modified, + Object: pod, + } + } + } + }() + + return &podWatch{ + result: pods, + stop: stop, + } +} + +func (w *podWatch) Stop() { + w.stop <- true +} + +func (w *podWatch) ResultChan() <-chan watch.Event { + return w.result +} diff --git a/pkg/deploy/strategy/recreate/recreate_test.go b/pkg/deploy/strategy/recreate/recreate_test.go index 9b3af10b8034..6b385f8ca43b 100644 --- a/pkg/deploy/strategy/recreate/recreate_test.go +++ b/pkg/deploy/strategy/recreate/recreate_test.go @@ -8,11 +8,12 @@ import ( kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" 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 TestFirstDeployment(t *testing.T) { +func TestRecreate_initialDeployment(t *testing.T) { var updatedController *kapi.ReplicationController deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) @@ -29,6 +30,11 @@ func TestFirstDeployment(t *testing.T) { return ctrl, nil }, }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return nil + }, + }, } err := strategy.Deploy(deployment, []kapi.ObjectReference{}) @@ -46,7 +52,7 @@ func TestFirstDeployment(t *testing.T) { } } -func TestSecondDeploymentSuccessfulRetries(t *testing.T) { +func TestRecreate_secondDeploymentWithSuccessfulRetries(t *testing.T) { updatedControllers := make(map[string]*kapi.ReplicationController) oldDeployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) newConfig := deploytest.OkDeploymentConfig(2) @@ -79,6 +85,11 @@ func TestSecondDeploymentSuccessfulRetries(t *testing.T) { return ctrl, nil }, }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return nil + }, + }, } err := strategy.Deploy(newDeployment, []kapi.ObjectReference{ @@ -101,7 +112,7 @@ func TestSecondDeploymentSuccessfulRetries(t *testing.T) { } } -func TestSecondDeploymentFailedInitialRetries(t *testing.T) { +func TestRecreate_secondDeploymentScaleUpRetries(t *testing.T) { updatedControllers := make(map[string]*kapi.ReplicationController) oldDeployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) newConfig := deploytest.OkDeploymentConfig(2) @@ -127,6 +138,11 @@ func TestSecondDeploymentFailedInitialRetries(t *testing.T) { return nil, fmt.Errorf("update failure") }, }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return nil + }, + }, } err := strategy.Deploy(newDeployment, []kapi.ObjectReference{ @@ -145,7 +161,7 @@ func TestSecondDeploymentFailedInitialRetries(t *testing.T) { } } -func TestSecondDeploymentFailedDisableRetries(t *testing.T) { +func TestRecreate_secondDeploymentScaleDownRetries(t *testing.T) { updatedControllers := make(map[string]*kapi.ReplicationController) oldDeployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) newConfig := deploytest.OkDeploymentConfig(2) @@ -179,6 +195,11 @@ func TestSecondDeploymentFailedDisableRetries(t *testing.T) { } }, }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return nil + }, + }, } err := strategy.Deploy(newDeployment, []kapi.ObjectReference{ @@ -189,7 +210,7 @@ func TestSecondDeploymentFailedDisableRetries(t *testing.T) { }) if err == nil { - t.Fatalf("expcted a deploy error: %#v", err) + t.Fatalf("expected a deploy error: %#v", err) } if len(updatedControllers) > 0 { @@ -197,6 +218,358 @@ func TestSecondDeploymentFailedDisableRetries(t *testing.T) { } } +func TestRecreate_deploymentPreHookSuccess(t *testing.T) { + var updatedController *kapi.ReplicationController + config := deploytest.OkDeploymentConfig(1) + config.Template.Strategy.RecreateParams = recreateParams(deployapi.LifecycleHookFailurePolicyAbort, "") + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + strategy := &RecreateDeploymentStrategy{ + codec: api.Codec, + retryTimeout: 1 * time.Second, + retryPeriod: 1 * time.Millisecond, + client: &testControllerClient{ + getReplicationControllerFunc: func(namespace, name string) (*kapi.ReplicationController, error) { + return deployment, nil + }, + updateReplicationControllerFunc: func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { + updatedController = ctrl + return ctrl, nil + }, + }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return nil + }, + }, + } + + err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + + if err != nil { + t.Fatalf("unexpected deploy error: %#v", err) + } + + if updatedController == nil { + t.Fatalf("expected a ReplicationController") + } + + if e, a := 1, updatedController.Spec.Replicas; e != a { + t.Fatalf("expected controller replicas to be %d, got %d", e, a) + } +} + +func TestRecreate_deploymentPreHookFailAbort(t *testing.T) { + config := deploytest.OkDeploymentConfig(1) + config.Template.Strategy.RecreateParams = recreateParams(deployapi.LifecycleHookFailurePolicyAbort, "") + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + strategy := &RecreateDeploymentStrategy{ + codec: api.Codec, + retryTimeout: 1 * time.Second, + retryPeriod: 1 * time.Millisecond, + client: &testControllerClient{ + getReplicationControllerFunc: func(namespace, name string) (*kapi.ReplicationController, error) { + t.Fatalf("unexpected call to getReplicationController") + return deployment, nil + }, + updateReplicationControllerFunc: func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { + t.Fatalf("unexpected call to updateReplicationController") + return ctrl, nil + }, + }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return fmt.Errorf("hook execution failure") + }, + }, + } + + err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + if err == nil { + t.Fatalf("expected a deploy error") + } + t.Logf("got expected error: %s", err) +} + +func TestRecreate_deploymentPreHookFailureIgnored(t *testing.T) { + var updatedController *kapi.ReplicationController + config := deploytest.OkDeploymentConfig(1) + config.Template.Strategy.RecreateParams = recreateParams(deployapi.LifecycleHookFailurePolicyIgnore, "") + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + strategy := &RecreateDeploymentStrategy{ + codec: api.Codec, + retryTimeout: 1 * time.Second, + retryPeriod: 1 * time.Millisecond, + client: &testControllerClient{ + getReplicationControllerFunc: func(namespace, name string) (*kapi.ReplicationController, error) { + return deployment, nil + }, + updateReplicationControllerFunc: func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { + updatedController = ctrl + return ctrl, nil + }, + }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return fmt.Errorf("hook execution failure") + }, + }, + } + + err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + + if err != nil { + t.Fatalf("unexpected deploy error: %#v", err) + } + + if updatedController == nil { + t.Fatalf("expected a ReplicationController") + } + + if e, a := 1, updatedController.Spec.Replicas; e != a { + t.Fatalf("expected controller replicas to be %d, got %d", e, a) + } +} + +func TestRecreate_deploymentPreHookFailureRetried(t *testing.T) { + var updatedController *kapi.ReplicationController + config := deploytest.OkDeploymentConfig(1) + config.Template.Strategy.RecreateParams = recreateParams(deployapi.LifecycleHookFailurePolicyRetry, "") + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + errorCount := 2 + strategy := &RecreateDeploymentStrategy{ + codec: api.Codec, + retryTimeout: 1 * time.Second, + retryPeriod: 1 * time.Millisecond, + client: &testControllerClient{ + getReplicationControllerFunc: func(namespace, name string) (*kapi.ReplicationController, error) { + return deployment, nil + }, + updateReplicationControllerFunc: func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { + updatedController = ctrl + return ctrl, nil + }, + }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + if errorCount == 0 { + return nil + } + errorCount-- + return fmt.Errorf("hook execution failure") + }, + }, + } + + err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + + if err != nil { + t.Fatalf("unexpected deploy error: %#v", err) + } + + if updatedController == nil { + t.Fatalf("expected a ReplicationController") + } + + if e, a := 1, updatedController.Spec.Replicas; e != a { + t.Fatalf("expected controller replicas to be %d, got %d", e, a) + } +} + +func TestRecreate_deploymentPostHookSuccess(t *testing.T) { + var updatedController *kapi.ReplicationController + config := deploytest.OkDeploymentConfig(1) + config.Template.Strategy.RecreateParams = recreateParams("", deployapi.LifecycleHookFailurePolicyAbort) + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + strategy := &RecreateDeploymentStrategy{ + codec: api.Codec, + retryTimeout: 1 * time.Second, + retryPeriod: 1 * time.Millisecond, + client: &testControllerClient{ + getReplicationControllerFunc: func(namespace, name string) (*kapi.ReplicationController, error) { + return deployment, nil + }, + updateReplicationControllerFunc: func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { + updatedController = ctrl + return ctrl, nil + }, + }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return nil + }, + }, + } + + err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + + if err != nil { + t.Fatalf("unexpected deploy error: %#v", err) + } + + if updatedController == nil { + t.Fatalf("expected a ReplicationController") + } + + if e, a := 1, updatedController.Spec.Replicas; e != a { + t.Fatalf("expected controller replicas to be %d, got %d", e, a) + } +} + +func TestRecreate_deploymentPostHookAbortUnsupported(t *testing.T) { + var updatedController *kapi.ReplicationController + config := deploytest.OkDeploymentConfig(1) + config.Template.Strategy.RecreateParams = recreateParams("", deployapi.LifecycleHookFailurePolicyAbort) + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + strategy := &RecreateDeploymentStrategy{ + codec: api.Codec, + retryTimeout: 1 * time.Second, + retryPeriod: 1 * time.Millisecond, + client: &testControllerClient{ + getReplicationControllerFunc: func(namespace, name string) (*kapi.ReplicationController, error) { + return deployment, nil + }, + updateReplicationControllerFunc: func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { + updatedController = ctrl + return ctrl, nil + }, + }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return fmt.Errorf("hook execution failure") + }, + }, + } + + err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + + if err != nil { + t.Fatalf("unexpected deploy error: %#v", err) + } + + if updatedController == nil { + t.Fatalf("expected a ReplicationController") + } + + if e, a := 1, updatedController.Spec.Replicas; e != a { + t.Fatalf("expected controller replicas to be %d, got %d", e, a) + } +} + +func TestRecreate_deploymentPostHookFailIgnore(t *testing.T) { + var updatedController *kapi.ReplicationController + config := deploytest.OkDeploymentConfig(1) + config.Template.Strategy.RecreateParams = recreateParams("", deployapi.LifecycleHookFailurePolicyIgnore) + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + strategy := &RecreateDeploymentStrategy{ + codec: api.Codec, + retryTimeout: 1 * time.Second, + retryPeriod: 1 * time.Millisecond, + client: &testControllerClient{ + getReplicationControllerFunc: func(namespace, name string) (*kapi.ReplicationController, error) { + return deployment, nil + }, + updateReplicationControllerFunc: func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { + updatedController = ctrl + return ctrl, nil + }, + }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + return fmt.Errorf("hook execution failure") + }, + }, + } + + err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + + if err != nil { + t.Fatalf("unexpected deploy error: %#v", err) + } + + if updatedController == nil { + t.Fatalf("expected a ReplicationController") + } + + if e, a := 1, updatedController.Spec.Replicas; e != a { + t.Fatalf("expected controller replicas to be %d, got %d", e, a) + } +} + +func TestRecreate_deploymentPostHookFailureRetried(t *testing.T) { + var updatedController *kapi.ReplicationController + config := deploytest.OkDeploymentConfig(1) + config.Template.Strategy.RecreateParams = recreateParams("", deployapi.LifecycleHookFailurePolicyRetry) + deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) + + errorCount := 2 + strategy := &RecreateDeploymentStrategy{ + codec: api.Codec, + retryTimeout: 1 * time.Second, + retryPeriod: 1 * time.Millisecond, + client: &testControllerClient{ + getReplicationControllerFunc: func(namespace, name string) (*kapi.ReplicationController, error) { + return deployment, nil + }, + updateReplicationControllerFunc: func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) { + updatedController = ctrl + return ctrl, nil + }, + }, + hookExecutor: &hookExecutorImpl{ + executeFunc: func(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + if errorCount == 0 { + return nil + } + errorCount-- + return fmt.Errorf("hook execution failure") + }, + }, + } + + err := strategy.Deploy(deployment, []kapi.ObjectReference{}) + + if err != nil { + t.Fatalf("unexpected deploy error: %#v", err) + } + + if updatedController == nil { + t.Fatalf("expected a ReplicationController") + } + + if e, a := 1, updatedController.Spec.Replicas; e != a { + t.Fatalf("expected controller replicas to be %d, got %d", e, a) + } +} + +func recreateParams(preFailurePolicy, postFailurePolicy deployapi.LifecycleHookFailurePolicy) *deployapi.RecreateDeploymentStrategyParams { + var pre *deployapi.LifecycleHook + var post *deployapi.LifecycleHook + + if len(preFailurePolicy) > 0 { + pre = &deployapi.LifecycleHook{ + FailurePolicy: preFailurePolicy, + ExecNewPod: &deployapi.ExecNewPodHook{}, + } + } + if len(postFailurePolicy) > 0 { + post = &deployapi.LifecycleHook{ + FailurePolicy: postFailurePolicy, + ExecNewPod: &deployapi.ExecNewPodHook{}, + } + } + return &deployapi.RecreateDeploymentStrategyParams{ + Pre: pre, + Post: post, + } +} + type testControllerClient struct { getReplicationControllerFunc func(namespace, name string) (*kapi.ReplicationController, error) updateReplicationControllerFunc func(namespace string, ctrl *kapi.ReplicationController) (*kapi.ReplicationController, error) diff --git a/pkg/deploy/strategy/support/doc.go b/pkg/deploy/strategy/support/doc.go new file mode 100644 index 000000000000..f9539ad474e2 --- /dev/null +++ b/pkg/deploy/strategy/support/doc.go @@ -0,0 +1,2 @@ +// Package support is a library of code useful to any strategy. +package support diff --git a/pkg/deploy/strategy/support/lifecycle.go b/pkg/deploy/strategy/support/lifecycle.go new file mode 100644 index 000000000000..50983ea592ee --- /dev/null +++ b/pkg/deploy/strategy/support/lifecycle.go @@ -0,0 +1,135 @@ +package support + +import ( + "fmt" + "reflect" + + "github.com/golang/glog" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + deployapi "github.com/openshift/origin/pkg/deploy/api" +) + +// HookExecutor executes a deployment lifecycle hook. +type HookExecutor struct { + // PodClient provides access to pods. + PodClient HookExecutorPodClient +} + +// Execute executes hook in the context of deployment. +func (e *HookExecutor) Execute(hook *deployapi.LifecycleHook, deployment *kapi.ReplicationController) error { + if hook.ExecNewPod != nil { + return e.executeExecNewPod(hook.ExecNewPod, deployment) + } + return nil +} + +// executeExecNewPod executes a ExecNewPod hook by creating a new pod based on +// the hook parameters and deployment. The pod is then synchronously watched +// until the pod completes, and if the pod failed, an error is returned. +func (e *HookExecutor) executeExecNewPod(hook *deployapi.ExecNewPodHook, deployment *kapi.ReplicationController) error { + // Build a pod spec from the hook config and deployment + var image string + for _, container := range deployment.Spec.Template.Spec.Containers { + if container.Name != hook.ContainerName { + continue + } + image = container.Image + } + if len(image) == 0 { + return fmt.Errorf("no container named '%s' found in deployment template", hook.ContainerName) + } + + podName := kapi.SimpleNameGenerator.GenerateName(fmt.Sprintf("deployment-%s-hook-", deployment.Name)) + podSpec := &kapi.Pod{ + ObjectMeta: kapi.ObjectMeta{ + Name: podName, + Annotations: map[string]string{ + deployapi.DeploymentAnnotation: deployment.Name, + }, + }, + Spec: kapi.PodSpec{ + Containers: []kapi.Container{ + { + Name: "lifecycle", + Image: image, + Command: hook.Command, + Env: hook.Env, + }, + }, + RestartPolicy: kapi.RestartPolicyNever, + }, + } + + // Set up a watch for the pod + podWatch, err := e.PodClient.WatchPod(deployment.Namespace, podName) + if err != nil { + return fmt.Errorf("couldn't create watch for pod %s/%s: %s", deployment.Namespace, podName, err) + } + defer podWatch.Stop() + + // Try to create the pod + pod, err := e.PodClient.CreatePod(deployment.Namespace, podSpec) + if err != nil { + if !kerrors.IsAlreadyExists(err) { + return fmt.Errorf("couldn't create lifecycle pod for %s: %v", labelForDeployment(deployment), err) + } + } else { + glog.V(0).Infof("Created lifecycle pod %s for deployment %s", pod.Name, labelForDeployment(deployment)) + } + + // Wait for the pod to finish. + // TODO: Delete pod before returning? + glog.V(0).Infof("Waiting for hook pod %s/%s to complete", pod.Namespace, pod.Name) + for { + select { + case event, ok := <-podWatch.ResultChan(): + if !ok { + return fmt.Errorf("couldn't watch pod %s/%s", pod.Namespace, pod.Name) + } + if event.Type == watch.Error { + return kerrors.FromObject(event.Object) + } + pod, podOk := event.Object.(*kapi.Pod) + if !podOk { + return fmt.Errorf("expected a pod event, got a %s", reflect.TypeOf(event.Object)) + } + glog.V(0).Infof("Lifecycle pod %s/%s in phase %s", pod.Namespace, pod.Name, pod.Status.Phase) + switch pod.Status.Phase { + case kapi.PodSucceeded: + return nil + case kapi.PodFailed: + // TODO: Add context + return fmt.Errorf("pod failed") + } + } + } +} + +// labelForDeployment builds a string identifier for a deployment. +func labelForDeployment(deployment *kapi.ReplicationController) string { + return fmt.Sprintf("%s/%s", deployment.Namespace, deployment.Name) +} + +// HookExecutorPodClient abstracts access to pods. +type HookExecutorPodClient interface { + CreatePod(namespace string, pod *kapi.Pod) (*kapi.Pod, error) + WatchPod(namespace, name string) (watch.Interface, error) +} + +// HookExecutorPodClientImpl is a pluggable HookExecutorPodClient. +type HookExecutorPodClientImpl struct { + CreatePodFunc func(namespace string, pod *kapi.Pod) (*kapi.Pod, error) + WatchPodFunc func(namespace, name string) (watch.Interface, error) +} + +func (i *HookExecutorPodClientImpl) CreatePod(namespace string, pod *kapi.Pod) (*kapi.Pod, error) { + return i.CreatePodFunc(namespace, pod) +} + +func (i *HookExecutorPodClientImpl) WatchPod(namespace, name string) (watch.Interface, error) { + return i.WatchPodFunc(namespace, name) +} diff --git a/pkg/deploy/strategy/support/lifecycle_test.go b/pkg/deploy/strategy/support/lifecycle_test.go new file mode 100644 index 000000000000..34b2213ffbd3 --- /dev/null +++ b/pkg/deploy/strategy/support/lifecycle_test.go @@ -0,0 +1,223 @@ +package support + +import ( + "fmt" + "testing" + //"time" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + + //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 TestHookExecutor_executeExecNewPodInvalidContainerRef(t *testing.T) { + hook := &deployapi.LifecycleHook{ + ExecNewPod: &deployapi.ExecNewPodHook{ + ContainerName: "undefined", + }, + } + + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) + + executor := &HookExecutor{ + PodClient: &HookExecutorPodClientImpl{ + CreatePodFunc: func(namespace string, pod *kapi.Pod) (*kapi.Pod, error) { + t.Fatalf("unexpected call to CreatePod") + return nil, nil + }, + WatchPodFunc: func(namespace, name string) (watch.Interface, error) { + t.Fatalf("unexpected call to WatchPod") + return nil, nil + }, + }, + } + + err := executor.Execute(hook, deployment) + + if err == nil { + t.Fatalf("expected an error") + } + t.Logf("got expected error: %s", err) +} + +func TestHookExecutor_executeExecNewWatchFailure(t *testing.T) { + hook := &deployapi.LifecycleHook{ + ExecNewPod: &deployapi.ExecNewPodHook{ + ContainerName: "undefined", + }, + } + + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) + + executor := &HookExecutor{ + PodClient: &HookExecutorPodClientImpl{ + CreatePodFunc: func(namespace string, pod *kapi.Pod) (*kapi.Pod, error) { + t.Fatalf("unexpected call to CreatePod") + return nil, nil + }, + WatchPodFunc: func(namespace, name string) (watch.Interface, error) { + return nil, fmt.Errorf("couldn't make watch") + }, + }, + } + + err := executor.Execute(hook, deployment) + + if err == nil { + t.Fatalf("expected an error") + } + t.Logf("got expected error: %s", err) +} + +func TestHookExecutor_executeExecNewCreatePodFailure(t *testing.T) { + hook := &deployapi.LifecycleHook{ + ExecNewPod: &deployapi.ExecNewPodHook{ + ContainerName: "container1", + }, + } + + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) + + podWatch := newTestWatch() + + executor := &HookExecutor{ + PodClient: &HookExecutorPodClientImpl{ + CreatePodFunc: func(namespace string, pod *kapi.Pod) (*kapi.Pod, error) { + return nil, fmt.Errorf("couldn't create pod") + }, + WatchPodFunc: func(namespace, name string) (watch.Interface, error) { + return podWatch, nil + }, + }, + } + + err := executor.Execute(hook, deployment) + + if err == nil { + t.Fatalf("expected an error") + } + t.Logf("got expected error: %s", err) +} + +func TestHookExecutor_executeExecNewPodSucceeded(t *testing.T) { + hook := &deployapi.LifecycleHook{ + ExecNewPod: &deployapi.ExecNewPodHook{ + ContainerName: "container1", + Command: []string{"overridden"}, + Env: []kapi.EnvVar{ + { + Name: "name", + Value: "value", + }, + }, + }, + } + + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) + + podWatch := newTestWatch() + + var createdPod *kapi.Pod + executor := &HookExecutor{ + PodClient: &HookExecutorPodClientImpl{ + CreatePodFunc: func(namespace string, pod *kapi.Pod) (*kapi.Pod, error) { + go func() { + obj, _ := kapi.Scheme.Copy(pod) + cp := obj.(*kapi.Pod) + cp.Status.Phase = kapi.PodSucceeded + podWatch.events <- watch.Event{ + Type: watch.Modified, + Object: cp, + } + }() + createdPod = pod + return createdPod, nil + }, + WatchPodFunc: func(namespace, name string) (watch.Interface, error) { + return podWatch, nil + }, + }, + } + + err := executor.Execute(hook, deployment) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if e, a := deployment.Spec.Template.Spec.Containers[0].Image, createdPod.Spec.Containers[0].Image; e != a { + t.Fatalf("expected container image %s, got %s", e, a) + } + + if e, a := "overridden", createdPod.Spec.Containers[0].Command[0]; e != a { + t.Fatalf("expected container command %s, got %s", e, a) + } + + if e, a := "value", createdPod.Spec.Containers[0].Env[0].Value; e != a { + t.Fatalf("expected env value %s, got %s", e, a) + } + + if e, a := kapi.RestartPolicyNever, createdPod.Spec.RestartPolicy; e != a { + t.Fatalf("expected restart policy %s, got %s", e, a) + } +} + +func TestHookExecutor_executeExecNewPodFailed(t *testing.T) { + hook := &deployapi.LifecycleHook{ + ExecNewPod: &deployapi.ExecNewPodHook{ + ContainerName: "container1", + }, + } + + deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codec) + + podWatch := newTestWatch() + + var createdPod *kapi.Pod + executor := &HookExecutor{ + PodClient: &HookExecutorPodClientImpl{ + CreatePodFunc: func(namespace string, pod *kapi.Pod) (*kapi.Pod, error) { + go func() { + obj, _ := kapi.Scheme.Copy(pod) + cp := obj.(*kapi.Pod) + cp.Status.Phase = kapi.PodFailed + podWatch.events <- watch.Event{ + Type: watch.Modified, + Object: cp, + } + }() + createdPod = pod + return createdPod, nil + }, + WatchPodFunc: func(namespace, name string) (watch.Interface, error) { + return podWatch, nil + }, + }, + } + + err := executor.Execute(hook, deployment) + + if err == nil { + t.Fatalf("expected an error", err) + } + t.Logf("got expected error: %s", err) +} + +type testWatch struct { + events chan watch.Event +} + +func newTestWatch() *testWatch { + return &testWatch{make(chan watch.Event)} +} + +func (w *testWatch) Stop() { +} + +func (w *testWatch) ResultChan() <-chan watch.Event { + return w.events +} From 1dc8f857fd7605e3845b71b997e4dd06e297a94c Mon Sep 17 00:00:00 2001 From: Dan Mace Date: Mon, 13 Apr 2015 12:32:29 -0400 Subject: [PATCH 2/4] WIP --- pkg/deploy/api/validation/validation.go | 68 ++++++++++++++++ pkg/deploy/api/validation/validation_test.go | 81 ++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/pkg/deploy/api/validation/validation.go b/pkg/deploy/api/validation/validation.go index 3cd2353155ca..3578e51389f0 100644 --- a/pkg/deploy/api/validation/validation.go +++ b/pkg/deploy/api/validation/validation.go @@ -1,6 +1,7 @@ package validation import ( + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" @@ -77,6 +78,10 @@ func validateDeploymentStrategy(strategy *deployapi.DeploymentStrategy) fielderr } switch strategy.Type { + case deployapi.DeploymentStrategyTypeRecreate: + if strategy.RecreateParams != nil { + errs = append(errs, validateRecreateParams(strategy.RecreateParams).Prefix("recreateParams")...) + } case deployapi.DeploymentStrategyTypeCustom: if strategy.CustomParams == nil { errs = append(errs, fielderrors.NewFieldRequired("customParams")) @@ -98,6 +103,69 @@ func validateCustomParams(params *deployapi.CustomDeploymentStrategyParams) fiel return errs } +func validateRecreateParams(params *deployapi.RecreateDeploymentStrategyParams) fielderrors.ValidationErrorList { + errs := fielderrors.ValidationErrorList{} + + if params.Pre != nil { + errs = append(errs, validateLifecycleHook(params.Pre).Prefix("pre")...) + } + if params.Post != nil { + errs = append(errs, validateLifecycleHook(params.Post).Prefix("post")...) + } + + return errs +} + +func validateLifecycleHook(hook *deployapi.LifecycleHook) fielderrors.ValidationErrorList { + errs := fielderrors.ValidationErrorList{} + + if len(hook.FailurePolicy) == 0 { + errs = append(errs, fielderrors.NewFieldRequired("failurePolicy")) + } + + if hook.ExecNewPod == nil { + errs = append(errs, fielderrors.NewFieldRequired("execNewPod")) + } else { + errs = append(errs, validateExecNewPod(hook.ExecNewPod).Prefix("execNewPod")...) + } + + return errs +} + +func validateExecNewPod(hook *deployapi.ExecNewPodHook) fielderrors.ValidationErrorList { + errs := fielderrors.ValidationErrorList{} + + if len(hook.Command) == 0 { + errs = append(errs, fielderrors.NewFieldRequired("command")) + } + + if len(hook.ContainerName) == 0 { + errs = append(errs, fielderrors.NewFieldRequired("containerName")) + } + + if len(hook.Env) > 0 { + errs = append(errs, validateEnv(hook.Env).Prefix("env")...) + } + + return errs +} + +func validateEnv(vars []kapi.EnvVar) fielderrors.ValidationErrorList { + allErrs := fielderrors.ValidationErrorList{} + + for i, ev := range vars { + vErrs := fielderrors.ValidationErrorList{} + if len(ev.Name) == 0 { + vErrs = append(vErrs, fielderrors.NewFieldRequired("name")) + } + if !util.IsCIdentifier(ev.Name) { + vErrs = append(vErrs, fielderrors.NewFieldInvalid("name", ev.Name, "must match regex "+util.CIdentifierFmt)) + } + allErrs = append(allErrs, vErrs.PrefixIndex(i)...) + } + return allErrs +} + 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 036632b73b03..1a235d24e8b9 100644 --- a/pkg/deploy/api/validation/validation_test.go +++ b/pkg/deploy/api/validation/validation_test.go @@ -227,6 +227,87 @@ func TestValidateDeploymentConfigMissingFields(t *testing.T) { fielderrors.ValidationErrorTypeRequired, "template.strategy.customParams.image", }, + "missing template.strategy.recreateParams.pre.failurePolicy": { + api.DeploymentConfig{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"}, + Template: api.DeploymentTemplate{ + Strategy: api.DeploymentStrategy{ + Type: api.DeploymentStrategyTypeRecreate, + RecreateParams: &api.RecreateDeploymentStrategyParams{ + Pre: &api.LifecycleHook{ + ExecNewPod: &api.ExecNewPodHook{ + Command: []string{"cmd"}, + ContainerName: "container", + }, + }, + }, + }, + ControllerTemplate: test.OkControllerTemplate(), + }, + }, + fielderrors.ValidationErrorTypeRequired, + "template.strategy.recreateParams.pre.failurePolicy", + }, + "missing template.strategy.recreateParams.pre.execNewPod": { + api.DeploymentConfig{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"}, + Template: api.DeploymentTemplate{ + Strategy: api.DeploymentStrategy{ + Type: api.DeploymentStrategyTypeRecreate, + RecreateParams: &api.RecreateDeploymentStrategyParams{ + Pre: &api.LifecycleHook{ + FailurePolicy: api.LifecycleHookFailurePolicyRetry, + }, + }, + }, + ControllerTemplate: test.OkControllerTemplate(), + }, + }, + fielderrors.ValidationErrorTypeRequired, + "template.strategy.recreateParams.pre.execNewPod", + }, + "missing template.strategy.recreateParams.pre.execNewPod.command": { + api.DeploymentConfig{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"}, + Template: api.DeploymentTemplate{ + Strategy: api.DeploymentStrategy{ + Type: api.DeploymentStrategyTypeRecreate, + RecreateParams: &api.RecreateDeploymentStrategyParams{ + Pre: &api.LifecycleHook{ + FailurePolicy: api.LifecycleHookFailurePolicyRetry, + ExecNewPod: &api.ExecNewPodHook{ + ContainerName: "container", + }, + }, + }, + }, + ControllerTemplate: test.OkControllerTemplate(), + }, + }, + fielderrors.ValidationErrorTypeRequired, + "template.strategy.recreateParams.pre.execNewPod.command", + }, + "missing template.strategy.recreateParams.pre.execNewPod.containerName": { + api.DeploymentConfig{ + ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "bar"}, + Template: api.DeploymentTemplate{ + Strategy: api.DeploymentStrategy{ + Type: api.DeploymentStrategyTypeRecreate, + RecreateParams: &api.RecreateDeploymentStrategyParams{ + Pre: &api.LifecycleHook{ + FailurePolicy: api.LifecycleHookFailurePolicyRetry, + ExecNewPod: &api.ExecNewPodHook{ + Command: []string{"cmd"}, + }, + }, + }, + }, + ControllerTemplate: test.OkControllerTemplate(), + }, + }, + fielderrors.ValidationErrorTypeRequired, + "template.strategy.recreateParams.pre.execNewPod.containerName", + }, } for k, v := range errorCases { From 72f03e10b189dac8ae6d6a5306601bfe950f8394 Mon Sep 17 00:00:00 2001 From: Dan Mace Date: Mon, 13 Apr 2015 13:11:43 -0400 Subject: [PATCH 3/4] Add e2e test coverage --- .../application-template-stibuild.json | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/examples/sample-app/application-template-stibuild.json b/examples/sample-app/application-template-stibuild.json index e4bccde8a268..ac2575dd4ee7 100644 --- a/examples/sample-app/application-template-stibuild.json +++ b/examples/sample-app/application-template-stibuild.json @@ -154,7 +154,39 @@ "replicas": 1 }, "strategy": { - "type": "Recreate" + "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" + } + ] + } + } + } } }, "triggers": [ From 1edbe520180286c4bfed8764504cfdc529a82f05 Mon Sep 17 00:00:00 2001 From: Dan Mace Date: Mon, 13 Apr 2015 16:44:27 -0400 Subject: [PATCH 4/4] Add missing omitempty tags --- pkg/deploy/api/types.go | 4 ++-- pkg/deploy/api/v1beta1/types.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/deploy/api/types.go b/pkg/deploy/api/types.go index 5cec3a54d68e..4198907beb7a 100644 --- a/pkg/deploy/api/types.go +++ b/pkg/deploy/api/types.go @@ -81,11 +81,11 @@ type CustomDeploymentStrategyParams struct { type RecreateDeploymentStrategyParams struct { // Pre is a lifecycle hook which is executed before the strategy manipulates // the deployment. All LifecycleHookFailurePolicy values are supported. - Pre *LifecycleHook `json:"pre"` + Pre *LifecycleHook `json:"pre,omitempty"` // Post is a lifecycle hook which is executed after the strategy has // finished all deployment logic. The LifecycleHookFailurePolicyAbort policy // is NOT supported. - Post *LifecycleHook `json:"post"` + Post *LifecycleHook `json:"post,omitempty"` } // Handler defines a specific deployment lifecycle action. diff --git a/pkg/deploy/api/v1beta1/types.go b/pkg/deploy/api/v1beta1/types.go index 02d44474bea4..79e4ecbeb9fc 100644 --- a/pkg/deploy/api/v1beta1/types.go +++ b/pkg/deploy/api/v1beta1/types.go @@ -82,11 +82,11 @@ type CustomDeploymentStrategyParams struct { type RecreateDeploymentStrategyParams struct { // Pre is a lifecycle hook which is executed before the strategy manipulates // the deployment. All LifecycleHookFailurePolicy values are supported. - Pre *LifecycleHook `json:"pre"` + Pre *LifecycleHook `json:"pre,omitempty"` // Post is a lifecycle hook which is executed after the strategy has // finished all deployment logic. The LifecycleHookFailurePolicyAbort policy // is NOT supported. - Post *LifecycleHook `json:"post"` + Post *LifecycleHook `json:"post,omitempty"` } // Handler defines a specific deployment lifecycle action.