diff --git a/pkg/api/resource_helpers.go b/pkg/api/resource_helpers.go index 1e3b3698b54..65bc1b85f08 100644 --- a/pkg/api/resource_helpers.go +++ b/pkg/api/resource_helpers.go @@ -131,6 +131,17 @@ func UpdatePodCondition(status *PodStatus, condition *PodCondition) bool { } } +// GetPodCondition extracts the provided condition from the given status and returns that. +// Returns nil if the condition is not present. +func GetPodCondition(status PodStatus, t PodConditionType) *PodCondition { + for i, c := range status.Conditions { + if c.Type == t { + return &status.Conditions[i] + } + } + return nil +} + // IsNodeReady returns true if a node is ready; false otherwise. func IsNodeReady(node *Node) bool { for _, c := range node.Status.Conditions { diff --git a/pkg/api/serialization_proto_test.go b/pkg/api/serialization_proto_test.go index e35708340b5..a108a98308c 100644 --- a/pkg/api/serialization_proto_test.go +++ b/pkg/api/serialization_proto_test.go @@ -67,6 +67,9 @@ func TestUniversalDeserializer(t *testing.T) { func TestProtobufRoundTrip(t *testing.T) { obj := &v1.Pod{} apitesting.FuzzerFor(t, v1.SchemeGroupVersion, rand.NewSource(benchmarkSeed)).Fuzz(obj) + // InitContainers are turned into annotations by conversion. + obj.Spec.InitContainers = nil + obj.Status.InitContainerStatuses = nil data, err := obj.Marshal() if err != nil { t.Fatal(err) @@ -77,7 +80,7 @@ func TestProtobufRoundTrip(t *testing.T) { } if !api.Semantic.Equalities.DeepEqual(out, obj) { t.Logf("marshal\n%s", hex.Dump(data)) - t.Fatalf("Unmarshal is unequal\n%s", diff.ObjectGoPrintSideBySide(out, obj)) + t.Fatalf("Unmarshal is unequal\n%s", diff.ObjectGoPrintDiff(out, obj)) } } diff --git a/pkg/api/types.go b/pkg/api/types.go index 911ad13ad71..c694cc09a45 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1076,6 +1076,8 @@ const ( // PodReady means the pod is able to service requests and should be added to the // load balancing pools of all matching services. PodReady PodConditionType = "Ready" + // PodInitialized means that all init containers in the pod have started successfully. + PodInitialized PodConditionType = "Initialized" ) type PodCondition struct { @@ -1309,7 +1311,9 @@ type PreferredSchedulingTerm struct { // PodSpec is a description of a pod type PodSpec struct { Volumes []Volume `json:"volumes"` - // Required: there must be at least one container in a pod. + // List of initialization containers belonging to the pod. + InitContainers []Container `json:"-"` + // List of containers belonging to the pod. Containers []Container `json:"containers"` RestartPolicy RestartPolicy `json:"restartPolicy,omitempty"` // Optional duration in seconds the pod needs to terminate gracefully. May be decreased in delete request. @@ -1416,6 +1420,11 @@ type PodStatus struct { // This is before the Kubelet pulled the container image(s) for the pod. StartTime *unversioned.Time `json:"startTime,omitempty"` + // The list has one entry per init container in the manifest. The most recent successful + // init container will have ready = true, the most recently started container will have + // startTime set. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-statuses + InitContainerStatuses []ContainerStatus `json:"-"` // The list has one entry per container in the manifest. Each entry is // currently the output of `docker inspect`. This output format is *not* // final and should not be relied upon. diff --git a/pkg/api/v1/conversion.go b/pkg/api/v1/conversion.go index a3631d00c75..7dc8f1e84da 100644 --- a/pkg/api/v1/conversion.go +++ b/pkg/api/v1/conversion.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + "encoding/json" "fmt" inf "gopkg.in/inf.v0" @@ -258,6 +259,75 @@ func Convert_v1_ReplicationControllerSpec_To_api_ReplicationControllerSpec(in *R return nil } +func Convert_api_PodStatusResult_To_v1_PodStatusResult(in *api.PodStatusResult, out *PodStatusResult, s conversion.Scope) error { + if err := autoConvert_api_PodStatusResult_To_v1_PodStatusResult(in, out, s); err != nil { + return err + } + + if len(out.Status.InitContainerStatuses) > 0 { + if out.Annotations == nil { + out.Annotations = make(map[string]string) + } + value, err := json.Marshal(out.Status.InitContainerStatuses) + if err != nil { + return err + } + out.Annotations[PodInitContainerStatusesAnnotationKey] = string(value) + } else { + delete(in.Annotations, PodInitContainerStatusesAnnotationKey) + } + return nil +} + +func Convert_v1_PodStatusResult_To_api_PodStatusResult(in *PodStatusResult, out *api.PodStatusResult, s conversion.Scope) error { + // TODO: when we move init container to beta, remove these conversions + if value := in.Annotations[PodInitContainerStatusesAnnotationKey]; len(value) > 0 { + delete(in.Annotations, PodInitContainerStatusesAnnotationKey) + var values []ContainerStatus + if err := json.Unmarshal([]byte(value), &values); err != nil { + return err + } + in.Status.InitContainerStatuses = values + } + + return autoConvert_v1_PodStatusResult_To_api_PodStatusResult(in, out, s) +} + +func Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(in *api.PodTemplateSpec, out *PodTemplateSpec, s conversion.Scope) error { + if err := autoConvert_api_PodTemplateSpec_To_v1_PodTemplateSpec(in, out, s); err != nil { + return err + } + + // TODO: when we move init container to beta, remove these conversions + if len(out.Spec.InitContainers) > 0 { + if out.Annotations == nil { + out.Annotations = make(map[string]string) + } + value, err := json.Marshal(out.Spec.InitContainers) + if err != nil { + return err + } + out.Annotations[PodInitContainersAnnotationKey] = string(value) + } else { + delete(out.Annotations, PodInitContainersAnnotationKey) + } + return nil +} + +func Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(in *PodTemplateSpec, out *api.PodTemplateSpec, s conversion.Scope) error { + // TODO: when we move init container to beta, remove these conversions + if value := in.Annotations[PodInitContainersAnnotationKey]; len(value) > 0 { + delete(in.Annotations, PodInitContainersAnnotationKey) + var values []Container + if err := json.Unmarshal([]byte(value), &values); err != nil { + return err + } + in.Spec.InitContainers = values + } + + return autoConvert_v1_PodTemplateSpec_To_api_PodTemplateSpec(in, out, s) +} + // The following two PodSpec conversions are done here to support ServiceAccount // as an alias for ServiceAccountName. func Convert_api_PodSpec_To_v1_PodSpec(in *api.PodSpec, out *PodSpec, s conversion.Scope) error { @@ -271,6 +341,16 @@ func Convert_api_PodSpec_To_v1_PodSpec(in *api.PodSpec, out *PodSpec, s conversi } else { out.Volumes = nil } + if in.InitContainers != nil { + out.InitContainers = make([]Container, len(in.InitContainers)) + for i := range in.InitContainers { + if err := Convert_api_Container_To_v1_Container(&in.InitContainers[i], &out.InitContainers[i], s); err != nil { + return err + } + } + } else { + out.InitContainers = nil + } if in.Containers != nil { out.Containers = make([]Container, len(in.Containers)) for i := range in.Containers { @@ -346,6 +426,16 @@ func Convert_v1_PodSpec_To_api_PodSpec(in *PodSpec, out *api.PodSpec, s conversi } else { out.Volumes = nil } + if in.InitContainers != nil { + out.InitContainers = make([]api.Container, len(in.InitContainers)) + for i := range in.InitContainers { + if err := Convert_v1_Container_To_api_Container(&in.InitContainers[i], &out.InitContainers[i], s); err != nil { + return err + } + } + } else { + out.InitContainers = nil + } if in.Containers != nil { out.Containers = make([]api.Container, len(in.Containers)) for i := range in.Containers { @@ -419,6 +509,33 @@ func Convert_api_Pod_To_v1_Pod(in *api.Pod, out *Pod, s conversion.Scope) error if err := autoConvert_api_Pod_To_v1_Pod(in, out, s); err != nil { return err } + + // TODO: when we move init container to beta, remove these conversions + if len(out.Spec.InitContainers) > 0 { + if out.Annotations == nil { + out.Annotations = make(map[string]string) + } + value, err := json.Marshal(out.Spec.InitContainers) + if err != nil { + return err + } + out.Annotations[PodInitContainersAnnotationKey] = string(value) + } else { + delete(out.Annotations, PodInitContainersAnnotationKey) + } + if len(out.Status.InitContainerStatuses) > 0 { + if out.Annotations == nil { + out.Annotations = make(map[string]string) + } + value, err := json.Marshal(out.Status.InitContainerStatuses) + if err != nil { + return err + } + out.Annotations[PodInitContainerStatusesAnnotationKey] = string(value) + } else { + delete(in.Annotations, PodInitContainerStatusesAnnotationKey) + } + // We need to reset certain fields for mirror pods from pre-v1.1 kubelet // (#15960). // TODO: Remove this code after we drop support for v1.0 kubelets. @@ -434,6 +551,24 @@ func Convert_api_Pod_To_v1_Pod(in *api.Pod, out *Pod, s conversion.Scope) error } func Convert_v1_Pod_To_api_Pod(in *Pod, out *api.Pod, s conversion.Scope) error { + // TODO: when we move init container to beta, remove these conversions + if value := in.Annotations[PodInitContainersAnnotationKey]; len(value) > 0 { + delete(in.Annotations, PodInitContainersAnnotationKey) + var values []Container + if err := json.Unmarshal([]byte(value), &values); err != nil { + return err + } + in.Spec.InitContainers = values + } + if value := in.Annotations[PodInitContainerStatusesAnnotationKey]; len(value) > 0 { + delete(in.Annotations, PodInitContainerStatusesAnnotationKey) + var values []ContainerStatus + if err := json.Unmarshal([]byte(value), &values); err != nil { + return err + } + in.Status.InitContainerStatuses = values + } + return autoConvert_v1_Pod_To_api_Pod(in, out, s) } diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index b1fa56f0578..315c9193fd9 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -1106,7 +1106,7 @@ type Container struct { // Cannot be updated. // More info: http://releases.k8s.io/HEAD/docs/user-guide/persistent-volumes.md#resources Resources ResourceRequirements `json:"resources,omitempty" protobuf:"bytes,8,opt,name=resources"` - // Pod volumes to mount into the container's filesyste. + // Pod volumes to mount into the container's filesystem. // Cannot be updated. VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,9,rep,name=volumeMounts"` // Periodic probe of container liveness. @@ -1541,11 +1541,36 @@ type PreferredSchedulingTerm struct { Preference NodeSelectorTerm `json:"preference" protobuf:"bytes,2,opt,name=preference"` } +const ( + // This annotation key will be used to contain an array of v1 JSON encoded Containers + // for init containers. The annotation will be placed into the internal type and cleared. + PodInitContainersAnnotationKey = "pod.alpha.kubernetes.io/init-containers" + // This annotation key will be used to contain an array of v1 JSON encoded + // ContainerStatuses for init containers. The annotation will be placed into the internal + // type and cleared. + PodInitContainerStatusesAnnotationKey = "pod.alpha.kubernetes.io/init-container-statuses" +) + // PodSpec is a description of a pod. type PodSpec struct { // List of volumes that can be mounted by containers belonging to the pod. // More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md Volumes []Volume `json:"volumes,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,1,rep,name=volumes"` + // List of initialization containers belonging to the pod. + // Init containers are executed in order prior to containers being started. If any + // init container fails, the pod is considered to have failed and is handled according + // to its restartPolicy. The name for an init container or normal container must be + // unique among all containers. + // Init containers may not have Lifecycle actions, Readiness probes, or Liveness probes. + // The resourceRequirements of an init container are taken into account during scheduling + // by finding the highest request/limit for each resource type, and then using the max of + // of that value or the sum of the normal containers. Limits are applied to init containers + // in a similar fashion. + // Init containers cannot currently be added or removed. + // Init containers are in alpha state and may change without notice. + // Cannot be updated. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/containers.md + InitContainers []Container `json:"-" patchStrategy:"merge" patchMergeKey:"name"` // List of containers belonging to the pod. // Containers cannot currently be added or removed. // There must be at least one container in a Pod. @@ -1679,6 +1704,12 @@ type PodStatus struct { // This is before the Kubelet pulled the container image(s) for the pod. StartTime *unversioned.Time `json:"startTime,omitempty" protobuf:"bytes,7,opt,name=startTime"` + // The list has one entry per init container in the manifest. The most recent successful + // init container will have ready = true, the most recently started container will have + // startTime set. + // Init containers are in alpha state and may change without notice. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-statuses + InitContainerStatuses []ContainerStatus `json:"-"` // The list has one entry per container in the manifest. Each entry is currently the output // of `docker inspect`. // More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-statuses diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index ad55b55c128..ce5ba70d90f 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1326,6 +1326,37 @@ func validatePullPolicy(policy api.PullPolicy, fldPath *field.Path) field.ErrorL return allErrors } +func validateInitContainers(containers, otherContainers []api.Container, volumes sets.String, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if len(containers) > 0 { + allErrs = append(allErrs, validateContainers(containers, volumes, fldPath)...) + } + + allNames := sets.String{} + for _, ctr := range otherContainers { + allNames.Insert(ctr.Name) + } + for i, ctr := range containers { + idxPath := fldPath.Index(i) + if allNames.Has(ctr.Name) { + allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), ctr.Name)) + } + if len(ctr.Name) > 0 { + allNames.Insert(ctr.Name) + } + if ctr.Lifecycle != nil { + allErrs = append(allErrs, field.Invalid(idxPath.Child("lifecycle"), ctr.Lifecycle, "must not be set for init containers")) + } + if ctr.LivenessProbe != nil { + allErrs = append(allErrs, field.Invalid(idxPath.Child("livenessProbe"), ctr.LivenessProbe, "must not be set for init containers")) + } + if ctr.ReadinessProbe != nil { + allErrs = append(allErrs, field.Invalid(idxPath.Child("readinessProbe"), ctr.ReadinessProbe, "must not be set for init containers")) + } + } + return allErrs +} + func validateContainers(containers []api.Container, volumes sets.String, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -1451,6 +1482,7 @@ func ValidatePodSpec(spec *api.PodSpec, fldPath *field.Path) field.ErrorList { allVolumes, vErrs := validateVolumes(spec.Volumes, fldPath.Child("volumes")) allErrs = append(allErrs, vErrs...) allErrs = append(allErrs, validateContainers(spec.Containers, allVolumes, fldPath.Child("containers"))...) + allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.Containers, allVolumes, fldPath.Child("initContainers"))...) allErrs = append(allErrs, validateRestartPolicy(&spec.RestartPolicy, fldPath.Child("restartPolicy"))...) allErrs = append(allErrs, validateDNSPolicy(&spec.DNSPolicy, fldPath.Child("dnsPolicy"))...) allErrs = append(allErrs, unversionedvalidation.ValidateLabels(spec.NodeSelector, fldPath.Child("nodeSelector"))...)