package controller import ( "github.com/golang/glog" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" deployapi "github.com/openshift/origin/pkg/deploy/api" deployutil "github.com/openshift/origin/pkg/deploy/util" ) // DeploymentController performs a deployment by creating a pod which is defined by a strategy. // The status of the resulting deployment will follow the status of the corresponding pod. // // Deployments are represented by a ReplicationController. type DeploymentController struct { // ContainerCreator makes the container for the deployment pod based on the strategy. ContainerCreator DeploymentContainerCreator // DeploymentInterface provides access to deployments. DeploymentInterface dcDeploymentInterface // PodInterface provides access to pods. PodInterface dcPodInterface // NextDeployment blocks until the next deployment is available. NextDeployment func() *kapi.ReplicationController // NextPod blocks until the next pod is available. NextPod func() *kapi.Pod // DeploymentStore is a cache of deployments. DeploymentStore cache.Store // Environment is a set of environment which should be injected into all deployment pod // containers, in addition to whatever environment is specified by the ContainerCreator. Environment []kapi.EnvVar // UseLocalImages configures the ImagePullPolicy for containers in the deployment pod. UseLocalImages bool // Codec is used to decode DeploymentConfigs. Codec runtime.Codec // Stop is an optional channel that controls when the controller exits. Stop <-chan struct{} } // DeploymentContainerCreator knows how to create a deployment pod's container based on // the deployment's strategy. type DeploymentContainerCreator interface { CreateContainer(*deployapi.DeploymentStrategy) *kapi.Container } type dcDeploymentInterface interface { UpdateDeployment(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) } type dcPodInterface interface { CreatePod(namespace string, pod *kapi.Pod) (*kapi.Pod, error) DeletePod(namespace, id string) error } // Run begins watching and synchronizing deployment states. func (dc *DeploymentController) Run() { go util.Until(func() { dc.HandleDeployment() }, 0, dc.Stop) go util.Until(func() { dc.HandlePod() }, 0, dc.Stop) } // HandleDeployment processes a new deployment and creates a new Pod which implements the specific // deployment behavior. The deployment and pod are correlated with annotations. If the pod was // successfully created, the deployment's status is transitioned to pending; otherwise, the status // is transitioned to failed. func (dc *DeploymentController) HandleDeployment() { deployment := dc.NextDeployment() if deployment.Annotations[deployapi.DeploymentStatusAnnotation] != string(deployapi.DeploymentStatusNew) { glog.V(4).Infof("Ignoring deployment %s with non-New status", deployment.Name) return } // TODO: transition to a failed state? seems like yes since this is probably not recoverable var deploymentPod *kapi.Pod var deploymentPodError error if deploymentPod, deploymentPodError = dc.makeDeploymentPod(deployment); deploymentPodError != nil { glog.V(0).Infof("Failed to make deployment pod for %s: %v", deployment.Name, deploymentPodError) return } nextStatus := deployment.Annotations[deployapi.DeploymentStatusAnnotation] if pod, err := dc.PodInterface.CreatePod(deployment.Namespace, deploymentPod); err != nil { // If the pod already exists, it's possible that a previous CreatePod succeeded but // the deployment state update failed and now we're re-entering. if kerrors.IsAlreadyExists(err) { nextStatus = string(deployapi.DeploymentStatusPending) } else { glog.Infof("Error creating pod for deployment %s: %v", deployment.Name, err) nextStatus = string(deployapi.DeploymentStatusFailed) } } else { glog.V(2).Infof("Created pod %s for deployment %s", pod.Name, deployment.Name) deployment.Annotations[deployapi.DeploymentPodAnnotation] = pod.Name nextStatus = string(deployapi.DeploymentStatusPending) } deployment.Annotations[deployapi.DeploymentStatusAnnotation] = nextStatus glog.V(2).Infof("Updating deployment %s status %s -> %s", deployment.Name, deployment.Status, nextStatus) if _, err := dc.DeploymentInterface.UpdateDeployment(deployment.Namespace, deployment); err != nil { glog.V(2).Infof("Failed to update deployment %s: %v", deployment.Name, err) } } // HandlePod reconciles a pod's current state with its associated deployment and updates the // deployment appropriately. func (dc *DeploymentController) HandlePod() { pod := dc.NextPod() // Verify the assumption that we'll be given only pods correlated to a deployment deploymentID, hasDeploymentID := pod.Annotations[deployapi.DeploymentAnnotation] if !hasDeploymentID { glog.V(2).Infof("Unexpected state: Pod %s has no deployment annotation; skipping", pod.Name) return } deploymentObj, deploymentExists := dc.DeploymentStore.Get(deploymentID) if !deploymentExists { glog.V(2).Infof("Couldn't find deployment %s associated with pod %s", deploymentID, pod.Name) return } deployment := deploymentObj.(*kapi.ReplicationController) nextDeploymentStatus := deployment.Annotations[deployapi.DeploymentStatusAnnotation] switch pod.Status.Phase { case kapi.PodRunning: nextDeploymentStatus = string(deployapi.DeploymentStatusRunning) case kapi.PodSucceeded, kapi.PodFailed: nextDeploymentStatus = string(deployapi.DeploymentStatusComplete) // Detect failure based on the container state for _, info := range pod.Status.Info { if info.State.Termination != nil && info.State.Termination.ExitCode != 0 { nextDeploymentStatus = string(deployapi.DeploymentStatusFailed) } } // Automatically clean up successful pods if nextDeploymentStatus == string(deployapi.DeploymentStatusComplete) { if err := dc.PodInterface.DeletePod(deployment.Namespace, pod.Name); err != nil { glog.V(4).Infof("Couldn't delete completed pod %s for deployment %s: %#v", pod.Name, deployment.Name, err) } else { glog.V(4).Infof("Deleted completed pod %s for deployment %s", pod.Name, deployment.Name) } } } if deployment.Annotations[deployapi.DeploymentStatusAnnotation] != nextDeploymentStatus { glog.V(2).Infof("Updating deployment %s status %s -> %s", deployment.Name, deployment.Annotations[deployapi.DeploymentStatusAnnotation], nextDeploymentStatus) deployment.Annotations[deployapi.DeploymentStatusAnnotation] = nextDeploymentStatus if _, err := dc.DeploymentInterface.UpdateDeployment(pod.Namespace, deployment); err != nil { glog.V(2).Infof("Failed to update deployment %v: %v", deployment.Name, err) } } } // makeDeploymentPod creates a pod which implements deployment behavior. The pod is correlated to // the deployment with an annotation. func (dc *DeploymentController) makeDeploymentPod(deployment *kapi.ReplicationController) (*kapi.Pod, error) { var deploymentConfig *deployapi.DeploymentConfig var decodeError error if deploymentConfig, decodeError = deployutil.DecodeDeploymentConfig(deployment, dc.Codec); decodeError != nil { return nil, decodeError } container := dc.ContainerCreator.CreateContainer(&deploymentConfig.Template.Strategy) // Combine the container environment, controller environment, and deployment values into // the pod's environment. envVars := container.Env envVars = append(envVars, kapi.EnvVar{Name: "OPENSHIFT_DEPLOYMENT_NAME", Value: deployment.Name}) envVars = append(envVars, kapi.EnvVar{Name: "OPENSHIFT_DEPLOYMENT_NAMESPACE", Value: deployment.Namespace}) for _, env := range dc.Environment { envVars = append(envVars, env) } pod := &kapi.Pod{ ObjectMeta: kapi.ObjectMeta{ Annotations: map[string]string{ deployapi.DeploymentAnnotation: deployment.Name, }, }, Spec: kapi.PodSpec{ Containers: []kapi.Container{ { Name: "deployment", Command: container.Command, Image: container.Image, Env: envVars, }, }, RestartPolicy: kapi.RestartPolicy{ Never: &kapi.RestartPolicyNever{}, }, }, } if dc.UseLocalImages { pod.Spec.Containers[0].ImagePullPolicy = kapi.PullIfNotPresent } return pod, nil }