diff --git a/pkg/expapi/deep_copy_generated.go b/pkg/expapi/deep_copy_generated.go index 1879cc8e3ac..5856e7c9a21 100644 --- a/pkg/expapi/deep_copy_generated.go +++ b/pkg/expapi/deep_copy_generated.go @@ -986,6 +986,121 @@ func deepCopy_expapi_HorizontalPodAutoscalerStatus(in HorizontalPodAutoscalerSta return nil } +func deepCopy_expapi_Job(in Job, out *Job, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + if err := deepCopy_expapi_JobSpec(in.Spec, &out.Spec, c); err != nil { + return err + } + if err := deepCopy_expapi_JobStatus(in.Status, &out.Status, c); err != nil { + return err + } + return nil +} + +func deepCopy_expapi_JobCondition(in JobCondition, out *JobCondition, c *conversion.Cloner) error { + out.Type = in.Type + out.Status = in.Status + if err := deepCopy_util_Time(in.LastProbeTime, &out.LastProbeTime, c); err != nil { + return err + } + if err := deepCopy_util_Time(in.LastTransitionTime, &out.LastTransitionTime, c); err != nil { + return err + } + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +func deepCopy_expapi_JobList(in JobList, out *JobList, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ListMeta(in.ListMeta, &out.ListMeta, c); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]Job, len(in.Items)) + for i := range in.Items { + if err := deepCopy_expapi_Job(in.Items[i], &out.Items[i], c); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func deepCopy_expapi_JobSpec(in JobSpec, out *JobSpec, c *conversion.Cloner) error { + if in.Parallelism != nil { + out.Parallelism = new(int) + *out.Parallelism = *in.Parallelism + } else { + out.Parallelism = nil + } + if in.Completions != nil { + out.Completions = new(int) + *out.Completions = *in.Completions + } else { + out.Completions = nil + } + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(api.PodTemplateSpec) + if err := deepCopy_api_PodTemplateSpec(*in.Template, out.Template, c); err != nil { + return err + } + } else { + out.Template = nil + } + return nil +} + +func deepCopy_expapi_JobStatus(in JobStatus, out *JobStatus, c *conversion.Cloner) error { + if in.Conditions != nil { + out.Conditions = make([]JobCondition, len(in.Conditions)) + for i := range in.Conditions { + if err := deepCopy_expapi_JobCondition(in.Conditions[i], &out.Conditions[i], c); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + if in.StartTime != nil { + out.StartTime = new(util.Time) + if err := deepCopy_util_Time(*in.StartTime, out.StartTime, c); err != nil { + return err + } + } else { + out.StartTime = nil + } + if in.CompletionTime != nil { + out.CompletionTime = new(util.Time) + if err := deepCopy_util_Time(*in.CompletionTime, out.CompletionTime, c); err != nil { + return err + } + } else { + out.CompletionTime = nil + } + out.Active = in.Active + out.Successful = in.Successful + out.Unsuccessful = in.Unsuccessful + return nil +} + func deepCopy_expapi_ReplicationControllerDummy(in ReplicationControllerDummy, out *ReplicationControllerDummy, c *conversion.Cloner) error { if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { return err @@ -1206,6 +1321,11 @@ func init() { deepCopy_expapi_HorizontalPodAutoscalerList, deepCopy_expapi_HorizontalPodAutoscalerSpec, deepCopy_expapi_HorizontalPodAutoscalerStatus, + deepCopy_expapi_Job, + deepCopy_expapi_JobCondition, + deepCopy_expapi_JobList, + deepCopy_expapi_JobSpec, + deepCopy_expapi_JobStatus, deepCopy_expapi_ReplicationControllerDummy, deepCopy_expapi_ResourceConsumption, deepCopy_expapi_RollingUpdateDeployment, diff --git a/pkg/expapi/register.go b/pkg/expapi/register.go index 10e656da2cb..7c88517f833 100644 --- a/pkg/expapi/register.go +++ b/pkg/expapi/register.go @@ -32,6 +32,8 @@ func addKnownTypes() { &DeploymentList{}, &HorizontalPodAutoscaler{}, &HorizontalPodAutoscalerList{}, + &Job{}, + &JobList{}, &ReplicationControllerDummy{}, &Scale{}, &ThirdPartyResource{}, @@ -47,6 +49,8 @@ func (*Deployment) IsAnAPIObject() {} func (*DeploymentList) IsAnAPIObject() {} func (*HorizontalPodAutoscaler) IsAnAPIObject() {} func (*HorizontalPodAutoscalerList) IsAnAPIObject() {} +func (*Job) IsAnAPIObject() {} +func (*JobList) IsAnAPIObject() {} func (*ReplicationControllerDummy) IsAnAPIObject() {} func (*Scale) IsAnAPIObject() {} func (*ThirdPartyResource) IsAnAPIObject() {} diff --git a/pkg/expapi/types.go b/pkg/expapi/types.go index e136b93a890..e1c238e86fc 100644 --- a/pkg/expapi/types.go +++ b/pkg/expapi/types.go @@ -362,3 +362,102 @@ type ThirdPartyResourceDataList struct { // Items is a list of third party objects Items []ThirdPartyResourceData `json:"items"` } + +// Job represents the configuration of a single job. +type Job struct { + api.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + api.ObjectMeta `json:"metadata,omitempty"` + + // Spec is a structure defining the expected behavior of a job. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status + Spec JobSpec `json:"spec,omitempty"` + + // Status is a structure describing current status of a job. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status + Status JobStatus `json:"status,omitempty"` +} + +// JobList is a collection of jobs. +type JobList struct { + api.TypeMeta `json:",inline"` + // Standard list metadata + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + api.ListMeta `json:"metadata,omitempty"` + + // Items is the list of Job. + Items []Job `json:"items"` +} + +// JobSpec describes how the job execution will look like. +type JobSpec struct { + + // Parallelism specifies the maximum desired number of pods the job should + // run at any given time. The actual number of pods running in steady state will + // be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), + // i.e. when the work left to do is less than max parallelism. + Parallelism *int `json:"parallelism,omitempty"` + + // Completions specifies the desired number of successfully finished pods the + // job should be run with. Defaults to 1. + Completions *int `json:"completions,omitempty"` + + // Selector is a label query over pods that should match the pod count. + Selector map[string]string `json:"selector"` + + // Template is the object that describes the pod that will be created when + // executing a job. + Template *api.PodTemplateSpec `json:"template"` +} + +// JobStatus represents the current state of a Job. +type JobStatus struct { + + // Conditions represent the latest available observations of an object's current state. + Conditions []JobCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // StartTime represents time when the job was acknowledged by the Job Manager. + // It is not guaranteed to be set in happens-before order across separate operations. + // It is represented in RFC3339 form and is in UTC. + StartTime *util.Time `json:"startTime,omitempty"` + + // CompletionTime represents time when the job was completed. It is not guaranteed to + // be set in happens-before order across separate operations. + // It is represented in RFC3339 form and is in UTC. + CompletionTime *util.Time `json:"completionTime,omitempty"` + + // Active is the number of actively running pods. + Active int `json:"active,omitempty"` + + // Successful is the number of pods which reached Phase Succeeded. + Successful int `json:"successful,omitempty"` + + // Unsuccessful is the number of pods failures, this applies only to jobs + // created with RestartPolicyNever, otherwise this value will always be 0. + Unsuccessful int `json:"unsuccessful,omitempty"` +} + +type JobConditionType string + +// These are valid conditions of a job. +const ( + // JobComplete means the job has completed its execution. + JobComplete JobConditionType = "Complete" +) + +// JobCondition describes current state of a job. +type JobCondition struct { + // Type of job condition, currently only Complete. + Type JobConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status api.ConditionStatus `json:"status"` + // Last time the condition was checked. + LastProbeTime util.Time `json:"lastProbeTime,omitempty"` + // Last time the condition transit from one status to another. + LastTransitionTime util.Time `json:"lastTransitionTime,omitempty"` + // (brief) reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + // Human readable message indicating details about last transition. + Message string `json:"message,omitempty"` +} diff --git a/pkg/expapi/v1/conversion_generated.go b/pkg/expapi/v1/conversion_generated.go index 12289ef2363..7f0b1ed69b0 100644 --- a/pkg/expapi/v1/conversion_generated.go +++ b/pkg/expapi/v1/conversion_generated.go @@ -1788,6 +1788,134 @@ func convert_expapi_HorizontalPodAutoscalerStatus_To_v1_HorizontalPodAutoscalerS return nil } +func convert_expapi_Job_To_v1_Job(in *expapi.Job, out *Job, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*expapi.Job))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ObjectMeta_To_v1_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := convert_expapi_JobSpec_To_v1_JobSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := convert_expapi_JobStatus_To_v1_JobStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func convert_expapi_JobCondition_To_v1_JobCondition(in *expapi.JobCondition, out *JobCondition, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*expapi.JobCondition))(in) + } + out.Type = JobConditionType(in.Type) + out.Status = v1.ConditionStatus(in.Status) + if err := s.Convert(&in.LastProbeTime, &out.LastProbeTime, 0); err != nil { + return err + } + if err := s.Convert(&in.LastTransitionTime, &out.LastTransitionTime, 0); err != nil { + return err + } + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +func convert_expapi_JobList_To_v1_JobList(in *expapi.JobList, out *JobList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*expapi.JobList))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ListMeta_To_v1_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]Job, len(in.Items)) + for i := range in.Items { + if err := convert_expapi_Job_To_v1_Job(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func convert_expapi_JobSpec_To_v1_JobSpec(in *expapi.JobSpec, out *JobSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*expapi.JobSpec))(in) + } + if in.Parallelism != nil { + out.Parallelism = new(int) + *out.Parallelism = *in.Parallelism + } else { + out.Parallelism = nil + } + if in.Completions != nil { + out.Completions = new(int) + *out.Completions = *in.Completions + } else { + out.Completions = nil + } + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(v1.PodTemplateSpec) + if err := convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(in.Template, out.Template, s); err != nil { + return err + } + } else { + out.Template = nil + } + return nil +} + +func convert_expapi_JobStatus_To_v1_JobStatus(in *expapi.JobStatus, out *JobStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*expapi.JobStatus))(in) + } + if in.Conditions != nil { + out.Conditions = make([]JobCondition, len(in.Conditions)) + for i := range in.Conditions { + if err := convert_expapi_JobCondition_To_v1_JobCondition(&in.Conditions[i], &out.Conditions[i], s); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + if in.StartTime != nil { + if err := s.Convert(&in.StartTime, &out.StartTime, 0); err != nil { + return err + } + } else { + out.StartTime = nil + } + if in.CompletionTime != nil { + if err := s.Convert(&in.CompletionTime, &out.CompletionTime, 0); err != nil { + return err + } + } else { + out.CompletionTime = nil + } + out.Active = in.Active + out.Successful = in.Successful + out.Unsuccessful = in.Unsuccessful + return nil +} + func convert_expapi_ReplicationControllerDummy_To_v1_ReplicationControllerDummy(in *expapi.ReplicationControllerDummy, out *ReplicationControllerDummy, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*expapi.ReplicationControllerDummy))(in) @@ -2176,6 +2304,134 @@ func convert_v1_HorizontalPodAutoscalerStatus_To_expapi_HorizontalPodAutoscalerS return nil } +func convert_v1_Job_To_expapi_Job(in *Job, out *expapi.Job, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*Job))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := convert_v1_JobSpec_To_expapi_JobSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := convert_v1_JobStatus_To_expapi_JobStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func convert_v1_JobCondition_To_expapi_JobCondition(in *JobCondition, out *expapi.JobCondition, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*JobCondition))(in) + } + out.Type = expapi.JobConditionType(in.Type) + out.Status = api.ConditionStatus(in.Status) + if err := s.Convert(&in.LastProbeTime, &out.LastProbeTime, 0); err != nil { + return err + } + if err := s.Convert(&in.LastTransitionTime, &out.LastTransitionTime, 0); err != nil { + return err + } + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +func convert_v1_JobList_To_expapi_JobList(in *JobList, out *expapi.JobList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*JobList))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ListMeta_To_api_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]expapi.Job, len(in.Items)) + for i := range in.Items { + if err := convert_v1_Job_To_expapi_Job(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func convert_v1_JobSpec_To_expapi_JobSpec(in *JobSpec, out *expapi.JobSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*JobSpec))(in) + } + if in.Parallelism != nil { + out.Parallelism = new(int) + *out.Parallelism = *in.Parallelism + } else { + out.Parallelism = nil + } + if in.Completions != nil { + out.Completions = new(int) + *out.Completions = *in.Completions + } else { + out.Completions = nil + } + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(api.PodTemplateSpec) + if err := convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(in.Template, out.Template, s); err != nil { + return err + } + } else { + out.Template = nil + } + return nil +} + +func convert_v1_JobStatus_To_expapi_JobStatus(in *JobStatus, out *expapi.JobStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*JobStatus))(in) + } + if in.Conditions != nil { + out.Conditions = make([]expapi.JobCondition, len(in.Conditions)) + for i := range in.Conditions { + if err := convert_v1_JobCondition_To_expapi_JobCondition(&in.Conditions[i], &out.Conditions[i], s); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + if in.StartTime != nil { + if err := s.Convert(&in.StartTime, &out.StartTime, 0); err != nil { + return err + } + } else { + out.StartTime = nil + } + if in.CompletionTime != nil { + if err := s.Convert(&in.CompletionTime, &out.CompletionTime, 0); err != nil { + return err + } + } else { + out.CompletionTime = nil + } + out.Active = in.Active + out.Successful = in.Successful + out.Unsuccessful = in.Unsuccessful + return nil +} + func convert_v1_ReplicationControllerDummy_To_expapi_ReplicationControllerDummy(in *ReplicationControllerDummy, out *expapi.ReplicationControllerDummy, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*ReplicationControllerDummy))(in) @@ -2390,6 +2646,11 @@ func init() { convert_expapi_HorizontalPodAutoscalerSpec_To_v1_HorizontalPodAutoscalerSpec, convert_expapi_HorizontalPodAutoscalerStatus_To_v1_HorizontalPodAutoscalerStatus, convert_expapi_HorizontalPodAutoscaler_To_v1_HorizontalPodAutoscaler, + convert_expapi_JobCondition_To_v1_JobCondition, + convert_expapi_JobList_To_v1_JobList, + convert_expapi_JobSpec_To_v1_JobSpec, + convert_expapi_JobStatus_To_v1_JobStatus, + convert_expapi_Job_To_v1_Job, convert_expapi_ReplicationControllerDummy_To_v1_ReplicationControllerDummy, convert_expapi_ResourceConsumption_To_v1_ResourceConsumption, convert_expapi_ScaleSpec_To_v1_ScaleSpec, @@ -2431,6 +2692,11 @@ func init() { convert_v1_HorizontalPodAutoscaler_To_expapi_HorizontalPodAutoscaler, convert_v1_HostPathVolumeSource_To_api_HostPathVolumeSource, convert_v1_ISCSIVolumeSource_To_api_ISCSIVolumeSource, + convert_v1_JobCondition_To_expapi_JobCondition, + convert_v1_JobList_To_expapi_JobList, + convert_v1_JobSpec_To_expapi_JobSpec, + convert_v1_JobStatus_To_expapi_JobStatus, + convert_v1_Job_To_expapi_Job, convert_v1_Lifecycle_To_api_Lifecycle, convert_v1_ListMeta_To_api_ListMeta, convert_v1_LocalObjectReference_To_api_LocalObjectReference, diff --git a/pkg/expapi/v1/deep_copy_generated.go b/pkg/expapi/v1/deep_copy_generated.go index 0e5ba1cb976..bc58c0de5bb 100644 --- a/pkg/expapi/v1/deep_copy_generated.go +++ b/pkg/expapi/v1/deep_copy_generated.go @@ -998,6 +998,121 @@ func deepCopy_v1_HorizontalPodAutoscalerStatus(in HorizontalPodAutoscalerStatus, return nil } +func deepCopy_v1_Job(in Job, out *Job, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + if err := deepCopy_v1_JobSpec(in.Spec, &out.Spec, c); err != nil { + return err + } + if err := deepCopy_v1_JobStatus(in.Status, &out.Status, c); err != nil { + return err + } + return nil +} + +func deepCopy_v1_JobCondition(in JobCondition, out *JobCondition, c *conversion.Cloner) error { + out.Type = in.Type + out.Status = in.Status + if err := deepCopy_util_Time(in.LastProbeTime, &out.LastProbeTime, c); err != nil { + return err + } + if err := deepCopy_util_Time(in.LastTransitionTime, &out.LastTransitionTime, c); err != nil { + return err + } + out.Reason = in.Reason + out.Message = in.Message + return nil +} + +func deepCopy_v1_JobList(in JobList, out *JobList, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ListMeta(in.ListMeta, &out.ListMeta, c); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]Job, len(in.Items)) + for i := range in.Items { + if err := deepCopy_v1_Job(in.Items[i], &out.Items[i], c); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func deepCopy_v1_JobSpec(in JobSpec, out *JobSpec, c *conversion.Cloner) error { + if in.Parallelism != nil { + out.Parallelism = new(int) + *out.Parallelism = *in.Parallelism + } else { + out.Parallelism = nil + } + if in.Completions != nil { + out.Completions = new(int) + *out.Completions = *in.Completions + } else { + out.Completions = nil + } + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(v1.PodTemplateSpec) + if err := deepCopy_v1_PodTemplateSpec(*in.Template, out.Template, c); err != nil { + return err + } + } else { + out.Template = nil + } + return nil +} + +func deepCopy_v1_JobStatus(in JobStatus, out *JobStatus, c *conversion.Cloner) error { + if in.Conditions != nil { + out.Conditions = make([]JobCondition, len(in.Conditions)) + for i := range in.Conditions { + if err := deepCopy_v1_JobCondition(in.Conditions[i], &out.Conditions[i], c); err != nil { + return err + } + } + } else { + out.Conditions = nil + } + if in.StartTime != nil { + out.StartTime = new(util.Time) + if err := deepCopy_util_Time(*in.StartTime, out.StartTime, c); err != nil { + return err + } + } else { + out.StartTime = nil + } + if in.CompletionTime != nil { + out.CompletionTime = new(util.Time) + if err := deepCopy_util_Time(*in.CompletionTime, out.CompletionTime, c); err != nil { + return err + } + } else { + out.CompletionTime = nil + } + out.Active = in.Active + out.Successful = in.Successful + out.Unsuccessful = in.Unsuccessful + return nil +} + func deepCopy_v1_ReplicationControllerDummy(in ReplicationControllerDummy, out *ReplicationControllerDummy, c *conversion.Cloner) error { if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { return err @@ -1228,6 +1343,11 @@ func init() { deepCopy_v1_HorizontalPodAutoscalerList, deepCopy_v1_HorizontalPodAutoscalerSpec, deepCopy_v1_HorizontalPodAutoscalerStatus, + deepCopy_v1_Job, + deepCopy_v1_JobCondition, + deepCopy_v1_JobList, + deepCopy_v1_JobSpec, + deepCopy_v1_JobStatus, deepCopy_v1_ReplicationControllerDummy, deepCopy_v1_ResourceConsumption, deepCopy_v1_RollingUpdateDeployment, diff --git a/pkg/expapi/v1/register.go b/pkg/expapi/v1/register.go index 30bb3d923d4..88d0514b5a1 100644 --- a/pkg/expapi/v1/register.go +++ b/pkg/expapi/v1/register.go @@ -36,6 +36,8 @@ func addKnownTypes() { &DeploymentList{}, &HorizontalPodAutoscaler{}, &HorizontalPodAutoscalerList{}, + &Job{}, + &JobList{}, &ReplicationControllerDummy{}, &Scale{}, &ThirdPartyResource{}, @@ -51,6 +53,8 @@ func (*Deployment) IsAnAPIObject() {} func (*DeploymentList) IsAnAPIObject() {} func (*HorizontalPodAutoscaler) IsAnAPIObject() {} func (*HorizontalPodAutoscalerList) IsAnAPIObject() {} +func (*Job) IsAnAPIObject() {} +func (*JobList) IsAnAPIObject() {} func (*ReplicationControllerDummy) IsAnAPIObject() {} func (*Scale) IsAnAPIObject() {} func (*ThirdPartyResource) IsAnAPIObject() {} diff --git a/pkg/expapi/v1/types.go b/pkg/expapi/v1/types.go index e3d120f9e12..3be346140f4 100644 --- a/pkg/expapi/v1/types.go +++ b/pkg/expapi/v1/types.go @@ -363,3 +363,102 @@ type ThirdPartyResourceDataList struct { // Items is the list of ThirdpartyResourceData. Items []ThirdPartyResourceData `json:"items"` } + +// Job represents the configuration of a single job. +type Job struct { + v1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + v1.ObjectMeta `json:"metadata,omitempty"` + + // Spec is a structure defining the expected behavior of a job. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status + Spec JobSpec `json:"spec,omitempty"` + + // Status is a structure describing current status of a job. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status + Status JobStatus `json:"status,omitempty"` +} + +// JobList is a collection of jobs. +type JobList struct { + v1.TypeMeta `json:",inline"` + // Standard list metadata + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + v1.ListMeta `json:"metadata,omitempty"` + + // Items is the list of Job. + Items []Job `json:"items"` +} + +// JobSpec describes how the job execution will look like. +type JobSpec struct { + + // Parallelism specifies the maximum desired number of pods the job should + // run at any given time. The actual number of pods running in steady state will + // be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), + // i.e. when the work left to do is less than max parallelism. + Parallelism *int `json:"parallelism,omitempty"` + + // Completions specifies the desired number of successfully finished pods the + // job should be run with. Defaults to 1. + Completions *int `json:"completions,omitempty"` + + // Selector is a label query over pods that should match the pod count. + Selector map[string]string `json:"selector"` + + // Template is the object that describes the pod that will be created when + // executing a job. + Template *v1.PodTemplateSpec `json:"template"` +} + +// JobStatus represents the current state of a Job. +type JobStatus struct { + + // Conditions represent the latest available observations of an object's current state. + Conditions []JobCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + + // StartTime represents time when the job was acknowledged by the Job Manager. + // It is not guaranteed to be set in happens-before order across separate operations. + // It is represented in RFC3339 form and is in UTC. + StartTime *util.Time `json:"startTime,omitempty"` + + // CompletionTime represents time when the job was completed. It is not guaranteed to + // be set in happens-before order across separate operations. + // It is represented in RFC3339 form and is in UTC. + CompletionTime *util.Time `json:"completionTime,omitempty"` + + // Active is the number of actively running pods. + Active int `json:"active,omitempty"` + + // Successful is the number of pods which reached Phase Succeeded. + Successful int `json:"successful,omitempty"` + + // Unsuccessful is the number of pods failures, this applies only to jobs + // created with RestartPolicyNever, otherwise this value will always be 0. + Unsuccessful int `json:"unsuccessful,omitempty"` +} + +type JobConditionType string + +// These are valid conditions of a job. +const ( + // JobComplete means the job has completed its execution. + JobComplete JobConditionType = "Complete" +) + +// JobCondition describes current state of a job. +type JobCondition struct { + // Type of job condition, currently only Complete. + Type JobConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status v1.ConditionStatus `json:"status"` + // Last time the condition was checked. + LastProbeTime util.Time `json:"lastProbeTime,omitempty"` + // Last time the condition transit from one status to another. + LastTransitionTime util.Time `json:"lastTransitionTime,omitempty"` + // (brief) reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + // Human readable message indicating details about last transition. + Message string `json:"message,omitempty"` +} diff --git a/pkg/expapi/validation/validation.go b/pkg/expapi/validation/validation.go index 2e1c653808d..c77d81c1284 100644 --- a/pkg/expapi/validation/validation.go +++ b/pkg/expapi/validation/validation.go @@ -28,6 +28,8 @@ import ( "k8s.io/kubernetes/pkg/util/sets" ) +const isNegativeErrorMsg string = `must be non-negative` + // ValidateHorizontalPodAutoscaler can be used to check whether the given autoscaler name is valid. // Prefix indicates this name will be used as part of generation, in which case trailing dashes are allowed. func ValidateHorizontalPodAutoscalerName(name string, prefix bool) (bool, string) { @@ -268,3 +270,50 @@ func ValidateThirdPartyResourceData(obj *expapi.ThirdPartyResourceData) errs.Val } return allErrs } + +func ValidateJob(job *expapi.Job) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + // Jobs and rcs have the same name validation + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName).Prefix("metadata")...) + allErrs = append(allErrs, ValidateJobSpec(&job.Spec).Prefix("spec")...) + return allErrs +} + +func ValidateJobSpec(spec *expapi.JobSpec) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + + if spec.Parallelism != nil && *spec.Parallelism < 0 { + allErrs = append(allErrs, errs.NewFieldInvalid("parallelism", spec.Parallelism, isNegativeErrorMsg)) + } + if spec.Completions != nil && *spec.Completions < 0 { + allErrs = append(allErrs, errs.NewFieldInvalid("completions", spec.Completions, isNegativeErrorMsg)) + } + + selector := labels.Set(spec.Selector).AsSelector() + if selector.Empty() { + allErrs = append(allErrs, errs.NewFieldRequired("selector")) + } + + if spec.Template == nil { + allErrs = append(allErrs, errs.NewFieldRequired("template")) + } else { + labels := labels.Set(spec.Template.Labels) + if !selector.Matches(labels) { + allErrs = append(allErrs, errs.NewFieldInvalid("template.labels", spec.Template.Labels, "selector does not match template")) + } + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(spec.Template).Prefix("template")...) + if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && + spec.Template.Spec.RestartPolicy != api.RestartPolicyNever { + allErrs = append(allErrs, errs.NewFieldValueNotSupported("template.spec.restartPolicy", + spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyOnFailure), string(api.RestartPolicyNever)})) + } + } + return allErrs +} + +func ValidateJobUpdate(oldJob, job *expapi.Job) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&oldJob.ObjectMeta, &job.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateJobSpec(&job.Spec).Prefix("spec")...) + return allErrs +} diff --git a/pkg/expapi/validation/validation_test.go b/pkg/expapi/validation/validation_test.go index 91c562feb71..3f136a43edf 100644 --- a/pkg/expapi/validation/validation_test.go +++ b/pkg/expapi/validation/validation_test.go @@ -658,3 +658,129 @@ func TestValidateDeployment(t *testing.T) { } } } + +func TestValidateJob(t *testing.T) { + validSelector := map[string]string{"a": "b"} + validPodTemplateSpec := api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + } + successCases := []expapi.Job{ + { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + }, + Spec: expapi.JobSpec{ + Selector: validSelector, + Template: &validPodTemplateSpec, + }, + }, + } + for _, successCase := range successCases { + if errs := ValidateJob(&successCase); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + negative := -1 + errorCases := map[string]expapi.Job{ + "spec.parallelism:must be non-negative": { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + }, + Spec: expapi.JobSpec{ + Parallelism: &negative, + Selector: validSelector, + Template: &validPodTemplateSpec, + }, + }, + "spec.completions:must be non-negative": { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + }, + Spec: expapi.JobSpec{ + Completions: &negative, + Selector: validSelector, + Template: &validPodTemplateSpec, + }, + }, + "spec.selector:required value": { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + }, + Spec: expapi.JobSpec{ + Selector: map[string]string{}, + Template: &validPodTemplateSpec, + }, + }, + "spec.template:required value": { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + }, + Spec: expapi.JobSpec{ + Selector: validSelector, + }, + }, + "spec.template.labels:selector does not match template": { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + }, + Spec: expapi.JobSpec{ + Selector: validSelector, + Template: &api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"y": "z"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + }, + }, + "spec.template.spec.restartPolicy:unsupported value": { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + }, + Spec: expapi.JobSpec{ + Selector: validSelector, + Template: &api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + }, + }, + } + + for k, v := range errorCases { + errs := ValidateJob(&v) + if len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } else { + s := strings.Split(k, ":") + err := errs[0].(*errors.ValidationError) + if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) { + t.Errorf("unexpected error: %v, expected: %s", errs[0], k) + } + } + } +} diff --git a/pkg/master/master.go b/pkg/master/master.go index e3344954838..f3e16f532ce 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -55,6 +55,7 @@ import ( endpointsetcd "k8s.io/kubernetes/pkg/registry/endpoint/etcd" eventetcd "k8s.io/kubernetes/pkg/registry/event/etcd" expcontrolleretcd "k8s.io/kubernetes/pkg/registry/experimental/controller/etcd" + jobetcd "k8s.io/kubernetes/pkg/registry/job/etcd" limitrangeetcd "k8s.io/kubernetes/pkg/registry/limitrange/etcd" "k8s.io/kubernetes/pkg/registry/namespace" namespaceetcd "k8s.io/kubernetes/pkg/registry/namespace/etcd" @@ -827,6 +828,7 @@ func (m *Master) expapi(c *Config) *apiserver.APIGroupVersion { thirdPartyResourceStorage := thirdpartyresourceetcd.NewREST(c.ExpDatabaseStorage) daemonSetStorage := daemonetcd.NewREST(c.ExpDatabaseStorage) deploymentStorage := deploymentetcd.NewREST(c.ExpDatabaseStorage) + jobStorage := jobetcd.NewREST(c.ExpDatabaseStorage) storage := map[string]rest.Storage{ strings.ToLower("replicationControllers"): controllerStorage.ReplicationController, @@ -835,6 +837,7 @@ func (m *Master) expapi(c *Config) *apiserver.APIGroupVersion { strings.ToLower("thirdpartyresources"): thirdPartyResourceStorage, strings.ToLower("daemonsets"): daemonSetStorage, strings.ToLower("deployments"): deploymentStorage, + strings.ToLower("jobs"): jobStorage, } return &apiserver.APIGroupVersion{ diff --git a/pkg/registry/job/doc.go b/pkg/registry/job/doc.go new file mode 100644 index 00000000000..a76a224cbff --- /dev/null +++ b/pkg/registry/job/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package controller provides Registry interface and it's RESTStorage +// implementation for storing Job api objects. +package job diff --git a/pkg/registry/job/etcd/etcd.go b/pkg/registry/job/etcd/etcd.go new file mode 100644 index 00000000000..d9f44da9ad6 --- /dev/null +++ b/pkg/registry/job/etcd/etcd.go @@ -0,0 +1,77 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package etcd + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + etcdgeneric "k8s.io/kubernetes/pkg/registry/generic/etcd" + "k8s.io/kubernetes/pkg/registry/job" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/storage" +) + +// rest implements a RESTStorage for jobs against etcd +type REST struct { + *etcdgeneric.Etcd +} + +// jobPrefix is the location for jobs in etcd, only exposed +// for testing +var jobPrefix = "/jobs" + +// NewREST returns a RESTStorage object that will work against Jobs. +func NewREST(s storage.Interface) *REST { + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &expapi.Job{} }, + + // NewListFunc returns an object capable of storing results of an etcd list. + NewListFunc: func() runtime.Object { return &expapi.JobList{} }, + // Produces a path that etcd understands, to the root of the resource + // by combining the namespace in the context with the given prefix + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, jobPrefix) + }, + // Produces a path that etcd understands, to the resource by combining + // the namespace in the context with the given prefix + KeyFunc: func(ctx api.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, jobPrefix, name) + }, + // Retrieve the name field of a job + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*expapi.Job).Name, nil + }, + // Used to match objects based on labels/fields for list and watch + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return job.MatchJob(label, field) + }, + EndpointName: "jobs", + + // Used to validate job creation + CreateStrategy: job.Strategy, + + // Used to validate job updates + UpdateStrategy: job.Strategy, + + Storage: s, + } + + return &REST{store} +} diff --git a/pkg/registry/job/etcd/etcd_test.go b/pkg/registry/job/etcd/etcd_test.go new file mode 100644 index 00000000000..5839fe9181d --- /dev/null +++ b/pkg/registry/job/etcd/etcd_test.go @@ -0,0 +1,148 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package etcd + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/expapi" + // Ensure that expapi/v1 package is initialized. + _ "k8s.io/kubernetes/pkg/expapi/v1" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/registrytest" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/tools" +) + +func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient) { + etcdStorage, fakeClient := registrytest.NewEtcdStorage(t, "experimental") + return NewREST(etcdStorage), fakeClient +} + +func validNewJob() *expapi.Job { + completions := 1 + parallelism := 1 + return &expapi.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + Spec: expapi.JobSpec{ + Completions: &completions, + Parallelism: ¶llelism, + Selector: map[string]string{"a": "b"}, + Template: &api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "test", + Image: "test_image", + ImagePullPolicy: api.PullIfNotPresent, + }, + }, + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + }, + }, + }, + } +} + +func TestCreate(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + validJob := validNewJob() + validJob.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + validJob, + // invalid (empty selector) + &expapi.Job{ + Spec: expapi.JobSpec{ + Completions: validJob.Spec.Completions, + Selector: map[string]string{}, + Template: validJob.Spec.Template, + }, + }, + ) +} + +func TestUpdate(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + completions := 2 + test.TestUpdate( + // valid + validNewJob(), + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*expapi.Job) + object.Spec.Completions = &completions + return object + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*expapi.Job) + object.Spec.Selector = map[string]string{} + return object + }, + ) +} + +func TestDelete(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestDelete(validNewJob()) +} + +func TestGet(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestGet(validNewJob()) +} + +func TestList(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestList(validNewJob()) +} + +func TestWatch(t *testing.T) { + storage, fakeClient := newStorage(t) + test := registrytest.New(t, fakeClient, storage.Etcd) + test.TestWatch( + validNewJob(), + // matching labels + []labels.Set{}, + // not matching labels + []labels.Set{ + {"x": "y"}, + }, + // matching fields + []fields.Set{}, + // not matching fields + []fields.Set{ + {"metadata.name": "xyz"}, + {"name": "foo"}, + }, + ) +} diff --git a/pkg/registry/job/registry.go b/pkg/registry/job/registry.go new file mode 100644 index 00000000000..521bd4d020f --- /dev/null +++ b/pkg/registry/job/registry.go @@ -0,0 +1,99 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package job + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/watch" +) + +// Registry is an interface for things that know how to store Jobs. +type Registry interface { + // ListJobs obtains a list of Jobs having labels and fields which match selector. + ListJobs(ctx api.Context, label labels.Selector, field fields.Selector) (*expapi.JobList, error) + // WatchJobs watch for new/changed/deleted Jobs. + WatchJobs(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + // GetJobs gets a specific Job. + GetJob(ctx api.Context, name string) (*expapi.Job, error) + // CreateJob creates a Job based on a specification. + CreateJob(ctx api.Context, job *expapi.Job) (*expapi.Job, error) + // UpdateJob updates an existing Job. + UpdateJob(ctx api.Context, job *expapi.Job) (*expapi.Job, error) + // DeleteJob deletes an existing Job. + DeleteJob(ctx api.Context, name string) error +} + +// storage puts strong typing around storage calls +type storage struct { + rest.StandardStorage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched +// types will panic. +func NewRegistry(s rest.StandardStorage) Registry { + return &storage{s} +} + +func (s *storage) ListJobs(ctx api.Context, label labels.Selector, field fields.Selector) (*expapi.JobList, error) { + if !field.Empty() { + return nil, fmt.Errorf("field selector not supported yet") + } + obj, err := s.List(ctx, label, field) + if err != nil { + return nil, err + } + return obj.(*expapi.JobList), err +} + +func (s *storage) WatchJobs(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return s.Watch(ctx, label, field, resourceVersion) +} + +func (s *storage) GetJob(ctx api.Context, name string) (*expapi.Job, error) { + obj, err := s.Get(ctx, name) + if err != nil { + return nil, err + } + return obj.(*expapi.Job), nil +} + +func (s *storage) CreateJob(ctx api.Context, job *expapi.Job) (*expapi.Job, error) { + obj, err := s.Create(ctx, job) + if err != nil { + return nil, err + } + return obj.(*expapi.Job), nil +} + +func (s *storage) UpdateJob(ctx api.Context, job *expapi.Job) (*expapi.Job, error) { + obj, _, err := s.Update(ctx, job) + if err != nil { + return nil, err + } + return obj.(*expapi.Job), nil +} + +func (s *storage) DeleteJob(ctx api.Context, name string) error { + _, err := s.Delete(ctx, name, nil) + return err +} diff --git a/pkg/registry/job/strategy.go b/pkg/registry/job/strategy.go new file mode 100644 index 00000000000..240ce10414c --- /dev/null +++ b/pkg/registry/job/strategy.go @@ -0,0 +1,105 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package job + +import ( + "fmt" + "strconv" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/expapi/validation" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/fielderrors" +) + +// jobStrategy implements verification logic for Replication Controllers. +type jobStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating Replication Controller objects. +var Strategy = jobStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped returns true because all jobs need to be within a namespace. +func (jobStrategy) NamespaceScoped() bool { + return true +} + +// PrepareForCreate clears the status of a job before creation. +func (jobStrategy) PrepareForCreate(obj runtime.Object) { + job := obj.(*expapi.Job) + job.Status = expapi.JobStatus{} +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (jobStrategy) PrepareForUpdate(obj, old runtime.Object) { + newJob := obj.(*expapi.Job) + oldJob := old.(*expapi.Job) + newJob.Status = oldJob.Status +} + +// Validate validates a new job. +func (jobStrategy) Validate(ctx api.Context, obj runtime.Object) fielderrors.ValidationErrorList { + job := obj.(*expapi.Job) + return validation.ValidateJob(job) +} + +func (jobStrategy) AllowUnconditionalUpdate() bool { + return true +} + +// AllowCreateOnUpdate is false for jobs; this means a POST is needed to create one. +func (jobStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (jobStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { + validationErrorList := validation.ValidateJob(obj.(*expapi.Job)) + updateErrorList := validation.ValidateJobUpdate(old.(*expapi.Job), obj.(*expapi.Job)) + return append(validationErrorList, updateErrorList...) +} + +// JobSelectableFields returns a field set that represents the object for matching purposes. +func JobToSelectableFields(job *expapi.Job) fields.Set { + return fields.Set{ + "metadata.name": job.Name, + "status.successful": strconv.Itoa(job.Status.Successful), + } +} + +// MatchJob is the filter used by the generic etcd backend to route +// watch events from etcd to clients of the apiserver only interested in specific +// labels/fields. +func MatchJob(label labels.Selector, field fields.Selector) generic.Matcher { + return &generic.SelectionPredicate{ + Label: label, + Field: field, + GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) { + job, ok := obj.(*expapi.Job) + if !ok { + return nil, nil, fmt.Errorf("Given object is not a job.") + } + return labels.Set(job.ObjectMeta.Labels), JobToSelectableFields(job), nil + }, + } +}