From 875755f992ada8639229b010a0a22d1f943e499b Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Mon, 8 Feb 2016 15:55:40 -0800 Subject: [PATCH 1/4] Added Selector Generation to Job. Added selector generation to Job's strategy.Validate, right before validation. Can't do in defaulting since UID is not known. Added a validation to Job to ensure that the generated labels and selector are correct when generation was requested. This happens right after generation, but validation is in a better place to return an error. Adds "manualSelector" field to batch/v1 Job to control selector generation. Adds same field to extensions/__internal. Conversion between those two is automatic. Adds "autoSelector" field to extensions/v1beta1 Job. Used for storing batch/v1 Jobs - Default for v1 is to do generation. - Default for v1beta1 is to not do it. - In both cases, unset == false == do the default thing. Release notes: Added batch/v1 group, which contains just Job, and which is the next version of extensions/v1beta1 Job. The changes from the previous version are: - Users no longer need to ensure labels on their pod template are unique to the enclosing job (but may add labels as needed for categorization). - In v1beta1, job.spec.selector was defaulted from pod labels, with the user responsible for uniqueness. In v1, a unique label is generated and added to the pod template, and used as the selector (other labels added by user stay on pod template, but need not be used by selector). - a new field called "manualSelector" field exists to control whether the new behavior is used, versus a more error-prone but more flexible "manual" (not generated) seletor. Most users will not need to use this field and should leave it unset. Users who are creating extensions.Job go objects and then posting them using the go client will see a change in the default behavior. They need to either stop providing a selector (relying on selector generation) or else specify "spec.manualSelector" until they are ready to do the former. --- examples/examples_test.go | 7 +- pkg/api/testing/fuzzer.go | 11 ++ pkg/apis/batch/v1/defaults.go | 12 -- pkg/apis/batch/v1/types.go | 14 +++ pkg/apis/extensions/types.go | 17 ++- pkg/apis/extensions/v1beta1/types.go | 9 ++ pkg/apis/extensions/validation/validation.go | 54 +++++++++ .../extensions/validation/validation_test.go | 109 ++++++++++++++---- pkg/kubectl/run.go | 7 ++ pkg/kubectl/run_test.go | 1 + pkg/registry/job/etcd/etcd_test.go | 7 ++ pkg/registry/job/strategy.go | 55 +++++++++ pkg/registry/job/strategy_test.go | 63 +++++++++- test/e2e/job.go | 11 +- test/integration/master_test.go | 15 +-- 15 files changed, 339 insertions(+), 53 deletions(-) diff --git a/examples/examples_test.go b/examples/examples_test.go index fa77d676a49..c31109f424e 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -32,7 +32,9 @@ import ( "k8s.io/kubernetes/pkg/apis/extensions" expvalidation "k8s.io/kubernetes/pkg/apis/extensions/validation" "k8s.io/kubernetes/pkg/capabilities" + "k8s.io/kubernetes/pkg/registry/job" "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/util/validation/field" "k8s.io/kubernetes/pkg/util/yaml" schedulerapi "k8s.io/kubernetes/plugin/pkg/scheduler/api" @@ -111,7 +113,10 @@ func validateObject(obj runtime.Object) (errors field.ErrorList) { if t.Namespace == "" { t.Namespace = api.NamespaceDefault } - errors = expvalidation.ValidateJob(t) + // Job needs generateSelector called before validation, and job.Validate does this. + // See: https://github.com/kubernetes/kubernetes/issues/20951#issuecomment-187787040 + t.ObjectMeta.UID = types.UID("fakeuid") + errors = job.Strategy.Validate(nil, t) case *extensions.Ingress: if t.Namespace == "" { t.Namespace = api.NamespaceDefault diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 092802d111f..1807dd5c004 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -162,6 +162,11 @@ func FuzzerFor(t *testing.T, version unversioned.GroupVersion, src rand.Source) parallelism := int(c.Rand.Int31()) j.Completions = &completions j.Parallelism = ¶llelism + if c.Rand.Int31()%2 == 0 { + j.ManualSelector = newBool(true) + } else { + j.ManualSelector = nil + } }, func(j *api.List, c fuzz.Continue) { c.FuzzNoCustom(j) // fuzz self without calling this function again @@ -411,3 +416,9 @@ func FuzzerFor(t *testing.T, version unversioned.GroupVersion, src rand.Source) ) return f } + +func newBool(val bool) *bool { + p := new(bool) + *p = val + return p +} diff --git a/pkg/apis/batch/v1/defaults.go b/pkg/apis/batch/v1/defaults.go index e4e592b9681..759ab0fb6de 100644 --- a/pkg/apis/batch/v1/defaults.go +++ b/pkg/apis/batch/v1/defaults.go @@ -23,18 +23,6 @@ import ( func addDefaultingFuncs(scheme *runtime.Scheme) { scheme.AddDefaultingFuncs( func(obj *Job) { - labels := obj.Spec.Template.Labels - // TODO: support templates defined elsewhere when we support them in the API - if labels != nil { - if obj.Spec.Selector == nil { - obj.Spec.Selector = &LabelSelector{ - MatchLabels: labels, - } - } - if len(obj.Labels) == 0 { - obj.Labels = labels - } - } // For a non-parallel job, you can leave both `.spec.completions` and // `.spec.parallelism` unset. When both are unset, both are defaulted to 1. if obj.Spec.Completions == nil && obj.Spec.Parallelism == nil { diff --git a/pkg/apis/batch/v1/types.go b/pkg/apis/batch/v1/types.go index 87e960104b1..9dfe0d3dcf9 100644 --- a/pkg/apis/batch/v1/types.go +++ b/pkg/apis/batch/v1/types.go @@ -63,6 +63,7 @@ type JobSpec struct { // pod signals the success of all pods, and allows parallelism to have any positive // value. Setting to 1 means that parallelism is limited to 1 and the success of that // pod signals the success of the job. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md Completions *int32 `json:"completions,omitempty"` // Optional duration in seconds relative to the startTime that the job may be active @@ -70,9 +71,22 @@ type JobSpec struct { ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"` // Selector is a label query over pods that should match the pod count. + // Normally, the system sets this field for you. // More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors Selector *LabelSelector `json:"selector,omitempty"` + // ManualSelector controls generation of pod labels and pod selectors. + // Leave `manualSelector` unset unless you are certain what you are doing. + // When false or unset, the system pick labels unique to this job + // and appends those labels to the pod template. When true, + // the user is responsible for picking unique labels and specifying + // the selector. Failure to pick a unique label may cause this + // and other jobs to not function correctly. However, You may see + // `manualSelector=true` in jobs that were created with the old `extensions/v1beta1` + // API. + // More info: http://releases.k8s.io/HEAD/docs/design/selector-generation.md + ManualSelector *bool `json:"manualSelector,omitempty"` + // Template is the object that describes the pod that will be created when // executing a job. // More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md diff --git a/pkg/apis/extensions/types.go b/pkg/apis/extensions/types.go index 6b46cb6074e..15377083e98 100644 --- a/pkg/apis/extensions/types.go +++ b/pkg/apis/extensions/types.go @@ -535,7 +535,10 @@ type JobSpec struct { Parallelism *int `json:"parallelism,omitempty"` // Completions specifies the desired number of successfully finished pods the - // job should be run with. When unset, any pod exiting signals the job to complete. + // job should be run with. Setting to nil means that the success of any + // pod signals the success of all pods, and allows parallelism to have any positive + // value. Setting to 1 means that parallelism is limited to 1 and the success of that + // pod signals the success of the job. Completions *int `json:"completions,omitempty"` // Optional duration in seconds relative to the startTime that the job may be active @@ -543,8 +546,20 @@ type JobSpec struct { ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"` // Selector is a label query over pods that should match the pod count. + // Normally, the system sets this field for you. Selector *unversioned.LabelSelector `json:"selector,omitempty"` + // ManualSelector controls generation of pod labels and pod selectors. + // Leave `manualSelector` unset unless you are certain what you are doing. + // When false or unset, the system pick labels unique to this job + // and appends those labels to the pod template. When true, + // the user is responsible for picking unique labels and specifying + // the selector. Failure to pick a unique label may cause this + // and other jobs to not function correctly. However, You may see + // `manualSelector=true` in jobs that were created with the old `extensions/v1beta1` + // API. + ManualSelector *bool `json:"manualSelector,omitempty"` + // Template is the object that describes the pod that will be created when // executing a job. Template api.PodTemplateSpec `json:"template"` diff --git a/pkg/apis/extensions/v1beta1/types.go b/pkg/apis/extensions/v1beta1/types.go index 9de6cb1f395..19895d90eb2 100644 --- a/pkg/apis/extensions/v1beta1/types.go +++ b/pkg/apis/extensions/v1beta1/types.go @@ -543,6 +543,7 @@ type JobSpec struct { // pod signals the success of all pods, and allows parallelism to have any positive // value. Setting to 1 means that parallelism is limited to 1 and the success of that // pod signals the success of the job. + // More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md Completions *int32 `json:"completions,omitempty"` // Optional duration in seconds relative to the startTime that the job may be active @@ -550,9 +551,17 @@ type JobSpec struct { ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"` // Selector is a label query over pods that should match the pod count. + // Normally, the system sets this field for you. // More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors Selector *LabelSelector `json:"selector,omitempty"` + // AutoSelector controls generation of pod labels and pod selectors. + // It was not present in the original extensions/v1beta1 Job definition, but exists + // to allow conversion from batch/v1 Jobs, where it corresponds to, but has the opposite + // meaning as, ManualSelector. + // More info: http://releases.k8s.io/HEAD/docs/design/selector-generation.md + AutoSelector *bool `json:"autoSelector,omitempty"` + // Template is the object that describes the pod that will be created when // executing a job. // More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index 157dc87dacc..764a5fc3ab9 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -379,9 +379,62 @@ func ValidateThirdPartyResourceData(obj *extensions.ThirdPartyResourceData) fiel return allErrs } +// TODO: generalize for other controller objects that will follow the same pattern, such as ReplicaSet and DaemonSet, and +// move to new location. Replace extensions.Job with an interface. +// +// ValidateGeneratedSelector validates that the generated selector on a controller object match the controller object +// metadata, and the labels on the pod template are as generated. +func ValidateGeneratedSelector(obj *extensions.Job) field.ErrorList { + allErrs := field.ErrorList{} + if obj.Spec.ManualSelector != nil && *obj.Spec.ManualSelector { + return allErrs + } + + if obj.Spec.Selector == nil { + return allErrs // This case should already have been checked in caller. No need for more errors. + } + + // If somehow uid was unset then we would get "controller-uid=" as the selector + // which is bad. + if obj.ObjectMeta.UID == "" { + allErrs = append(allErrs, field.Required(field.NewPath("metadata").Child("uid"), "")) + } + + // If somehow uid was unset then we would get "controller-uid=" as the selector + // which is bad. + if obj.ObjectMeta.UID == "" { + allErrs = append(allErrs, field.Required(field.NewPath("metadata").Child("uid"), "")) + } + + // If selector generation was requested, then expected labels must be + // present on pod template, and much match job's uid and name. The + // generated (not-manual) selectors/labels ensure no overlap with other + // controllers. The manual mode allows orphaning, adoption, + // backward-compatibility, and experimentation with new + // labeling/selection schemes. Automatic selector generation should + // have placed certain labels on the pod, but this could have failed if + // the user added coflicting labels. Validate that the expected + // generated ones are there. + + allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), "controller-uid", string(obj.UID))...) + allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), "job-name", string(obj.Name))...) + expectedLabels := make(map[string]string) + expectedLabels["controller-uid"] = string(obj.UID) + expectedLabels["job-name"] = string(obj.Name) + // Whether manually or automatically generated, the selector of the job must match the pods it will produce. + if selector, err := unversioned.LabelSelectorAsSelector(obj.Spec.Selector); err == nil { + if !selector.Matches(labels.Set(expectedLabels)) { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("selector"), obj.Spec.Selector, "`selector` not auto-generated")) + } + } + + return allErrs +} + func ValidateJob(job *extensions.Job) field.ErrorList { // Jobs and rcs have the same name validation allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateGeneratedSelector(job)...) allErrs = append(allErrs, ValidateJobSpec(&job.Spec, field.NewPath("spec"))...) return allErrs } @@ -404,6 +457,7 @@ func ValidateJobSpec(spec *extensions.JobSpec, fldPath *field.Path) field.ErrorL allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...) } + // Whether manually or automatically generated, the selector of the job must match the pods it will produce. if selector, err := unversioned.LabelSelectorAsSelector(spec.Selector); err == nil { labels := labels.Set(spec.Template.Labels) if !selector.Matches(labels) { diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index c421bdd404f..ff88893b91f 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/controller/podautoscaler" + "k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/util/intstr" ) @@ -975,12 +976,15 @@ func TestValidateDeploymentRollback(t *testing.T) { } func TestValidateJob(t *testing.T) { - validSelector := &unversioned.LabelSelector{ + validManualSelector := &unversioned.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, } - validPodTemplateSpec := api.PodTemplateSpec{ + validGeneratedSelector := &unversioned.LabelSelector{ + MatchLabels: map[string]string{"collection-uid": "1a2b3c"}, + } + validPodTemplateSpecForManual := api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ - Labels: validSelector.MatchLabels, + Labels: validManualSelector.MatchLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, @@ -988,21 +992,45 @@ func TestValidateJob(t *testing.T) { Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, } - successCases := []extensions.Job{ - { + validPodTemplateSpecForGenerated := api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validGeneratedSelector.MatchLabels, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + } + successCases := map[string]extensions.Job{ + "manual selector": { ObjectMeta: api.ObjectMeta{ Name: "myjob", Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), }, Spec: extensions.JobSpec{ - Selector: validSelector, - Template: validPodTemplateSpec, + Selector: validManualSelector, + ManualSelector: newBool(true), + Template: validPodTemplateSpecForManual, + }, + }, + "generated selector": { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: extensions.JobSpec{ + Selector: validGeneratedSelector, + ManualSelector: newBool(true), + Template: validPodTemplateSpecForGenerated, }, }, } - for _, successCase := range successCases { - if errs := ValidateJob(&successCase); len(errs) != 0 { - t.Errorf("expected success: %v", errs) + for k, v := range successCases { + if errs := ValidateJob(&v); len(errs) != 0 { + t.Errorf("expected success for %s: %v", k, errs) } } negative := -1 @@ -1012,51 +1040,59 @@ func TestValidateJob(t *testing.T) { ObjectMeta: api.ObjectMeta{ Name: "myjob", Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), }, Spec: extensions.JobSpec{ - Parallelism: &negative, - Selector: validSelector, - Template: validPodTemplateSpec, + Parallelism: &negative, + ManualSelector: newBool(true), + Template: validPodTemplateSpecForGenerated, }, }, "spec.completions:must be greater than or equal to 0": { ObjectMeta: api.ObjectMeta{ Name: "myjob", Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), }, Spec: extensions.JobSpec{ - Completions: &negative, - Selector: validSelector, - Template: validPodTemplateSpec, + Completions: &negative, + Selector: validManualSelector, + ManualSelector: newBool(true), + Template: validPodTemplateSpecForGenerated, }, }, "spec.activeDeadlineSeconds:must be greater than or equal to 0": { ObjectMeta: api.ObjectMeta{ Name: "myjob", Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), }, Spec: extensions.JobSpec{ ActiveDeadlineSeconds: &negative64, - Selector: validSelector, - Template: validPodTemplateSpec, + Selector: validManualSelector, + ManualSelector: newBool(true), + Template: validPodTemplateSpecForGenerated, }, }, "spec.selector:Required value": { ObjectMeta: api.ObjectMeta{ Name: "myjob", Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), }, Spec: extensions.JobSpec{ - Template: validPodTemplateSpec, + Template: validPodTemplateSpecForGenerated, }, }, "spec.template.metadata.labels: Invalid value: {\"y\":\"z\"}: `selector` does not match template `labels`": { ObjectMeta: api.ObjectMeta{ Name: "myjob", Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), }, Spec: extensions.JobSpec{ - Selector: validSelector, + Selector: validManualSelector, + ManualSelector: newBool(true), Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: map[string]string{"y": "z"}, @@ -1069,16 +1105,39 @@ func TestValidateJob(t *testing.T) { }, }, }, + "spec.template.metadata.labels: Invalid value: {\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": { + ObjectMeta: api.ObjectMeta{ + Name: "myjob", + Namespace: api.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: extensions.JobSpec{ + Selector: validManualSelector, + ManualSelector: newBool(true), + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"controller-uid": "4d5e6f"}, + }, + 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, + UID: types.UID("1a2b3c"), }, Spec: extensions.JobSpec{ - Selector: validSelector, + Selector: validManualSelector, + ManualSelector: newBool(true), Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ - Labels: validSelector.MatchLabels, + Labels: validManualSelector.MatchLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, @@ -2070,3 +2129,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) { } } } + +func newBool(val bool) *bool { + p := new(bool) + *p = val + return p +} diff --git a/pkg/kubectl/run.go b/pkg/kubectl/run.go index e0ed8ada89f..8cf4325b9c2 100644 --- a/pkg/kubectl/run.go +++ b/pkg/kubectl/run.go @@ -269,6 +269,7 @@ func (JobV1Beta1) Generate(genericParams map[string]interface{}) (runtime.Object Selector: &unversioned.LabelSelector{ MatchLabels: labels, }, + ManualSelector: newBool(true), Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: labels, @@ -607,3 +608,9 @@ func parseEnvs(envArray []string) ([]api.EnvVar, error) { } return envs, nil } + +func newBool(val bool) *bool { + p := new(bool) + *p = val + return p +} diff --git a/pkg/kubectl/run_test.go b/pkg/kubectl/run_test.go index 27a366c418b..46ca5984eb9 100644 --- a/pkg/kubectl/run_test.go +++ b/pkg/kubectl/run_test.go @@ -749,6 +749,7 @@ func TestGenerateJob(t *testing.T) { Selector: &unversioned.LabelSelector{ MatchLabels: map[string]string{"foo": "bar", "baz": "blah"}, }, + ManualSelector: newBool(true), Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: map[string]string{"foo": "bar", "baz": "blah"}, diff --git a/pkg/registry/job/etcd/etcd_test.go b/pkg/registry/job/etcd/etcd_test.go index 623125e8a66..9f52ccf8a19 100644 --- a/pkg/registry/job/etcd/etcd_test.go +++ b/pkg/registry/job/etcd/etcd_test.go @@ -53,6 +53,7 @@ func validNewJob() *extensions.Job { Selector: &unversioned.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, }, + ManualSelector: newBool(true), Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: map[string]string{"a": "b"}, @@ -165,3 +166,9 @@ func TestWatch(t *testing.T) { } // TODO: test update /status + +func newBool(val bool) *bool { + p := new(bool) + *p = val + return p +} diff --git a/pkg/registry/job/strategy.go b/pkg/registry/job/strategy.go index 807dc078a42..aef9eebf205 100644 --- a/pkg/registry/job/strategy.go +++ b/pkg/registry/job/strategy.go @@ -21,6 +21,7 @@ import ( "strconv" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/extensions/validation" "k8s.io/kubernetes/pkg/fields" @@ -60,9 +61,63 @@ func (jobStrategy) PrepareForUpdate(obj, old runtime.Object) { // Validate validates a new job. func (jobStrategy) Validate(ctx api.Context, obj runtime.Object) field.ErrorList { job := obj.(*extensions.Job) + // TODO: move UID generation earlier and do this in defaulting logic? + if job.Spec.ManualSelector == nil || *job.Spec.ManualSelector == false { + generateSelector(job) + } return validation.ValidateJob(job) } +// generateSelector adds a selector to a job and labels to its template +// which can be used to uniquely identify the pods created by that job, +// if the user has requested this behavior. +func generateSelector(obj *extensions.Job) { + if obj.Spec.Template.Labels == nil { + obj.Spec.Template.Labels = make(map[string]string) + } + // The job-name label is unique except in cases that are expected to be + // quite uncommon, and is more user friendly than uid. So, we add it as + // a label. + _, found := obj.Spec.Template.Labels["job-name"] + if found { + // User asked us to not automatically generate a selector and labels, + // but set a possibly conflicting value. If there is a conflict, + // we will reject in validation. + } else { + obj.Spec.Template.Labels["job-name"] = string(obj.ObjectMeta.Name) + } + // The controller-uid label makes the pods that belong to this job + // only match this job. + _, found = obj.Spec.Template.Labels["controller-uid"] + if found { + // User asked us to automatically generate a selector and labels, + // but set a possibly conflicting value. If there is a conflict, + // we will reject in validation. + } else { + obj.Spec.Template.Labels["controller-uid"] = string(obj.ObjectMeta.UID) + } + // Select the controller-uid label. This is sufficient for uniqueness. + if obj.Spec.Selector == nil { + obj.Spec.Selector = &unversioned.LabelSelector{} + } + if obj.Spec.Selector.MatchLabels == nil { + obj.Spec.Selector.MatchLabels = make(map[string]string) + } + if _, found := obj.Spec.Selector.MatchLabels["controller-uid"]; !found { + obj.Spec.Selector.MatchLabels["controller-uid"] = string(obj.ObjectMeta.UID) + } + // If the user specified matchLabel controller-uid=$WRONGUID, then it should fail + // in validation, either because the selector does not match the pod template + // (controller-uid=$WRONGUID does not match controller-uid=$UID, which we applied + // above, or we will reject in validation because the template has the wrong + // labels. +} + +// TODO: generalize generateSelector so it can work for other controller +// objects such as ReplicaSet. Can use pkg/api/meta to generically get the +// UID, but need some way to generically access the selector and pod labels +// fields. + // Canonicalize normalizes the object after validation. func (jobStrategy) Canonicalize(obj runtime.Object) { } diff --git a/pkg/registry/job/strategy_test.go b/pkg/registry/job/strategy_test.go index 924e363d1ad..13eb6c58279 100644 --- a/pkg/registry/job/strategy_test.go +++ b/pkg/registry/job/strategy_test.go @@ -17,6 +17,7 @@ limitations under the License. package job import ( + "reflect" "testing" "k8s.io/kubernetes/pkg/api" @@ -25,8 +26,15 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/types" ) +func newBool(a bool) *bool { + r := new(bool) + *r = a + return r +} + func TestJobStrategy(t *testing.T) { ctx := api.NewDefaultContext() if !Strategy.NamespaceScoped() { @@ -55,8 +63,9 @@ func TestJobStrategy(t *testing.T) { Namespace: api.NamespaceDefault, }, Spec: extensions.JobSpec{ - Selector: validSelector, - Template: validPodTemplateSpec, + Selector: validSelector, + Template: validPodTemplateSpec, + ManualSelector: newBool(true), }, Status: extensions.JobStatus{ Active: 11, @@ -93,6 +102,56 @@ func TestJobStrategy(t *testing.T) { } } +func TestJobStrategyWithGeneration(t *testing.T) { + ctx := api.NewDefaultContext() + + theUID := types.UID("1a2b3c4d5e6f7g8h9i0k") + + validPodTemplateSpec := api.PodTemplateSpec{ + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + } + job := &extensions.Job{ + ObjectMeta: api.ObjectMeta{ + Name: "myjob2", + Namespace: api.NamespaceDefault, + UID: theUID, + }, + Spec: extensions.JobSpec{ + Selector: nil, + Template: validPodTemplateSpec, + }, + } + + Strategy.PrepareForCreate(job) + errs := Strategy.Validate(ctx, job) + if len(errs) != 0 { + t.Errorf("Unexpected error validating %v", errs) + } + + // Validate the stuff that validation should have validated. + if job.Spec.Selector == nil { + t.Errorf("Selector not generated") + } + expectedLabels := make(map[string]string) + expectedLabels["controller-uid"] = string(theUID) + if !reflect.DeepEqual(job.Spec.Selector.MatchLabels, expectedLabels) { + t.Errorf("Expected label selector not generated") + } + if job.Spec.Template.ObjectMeta.Labels == nil { + t.Errorf("Expected template labels not generated") + } + if v, ok := job.Spec.Template.ObjectMeta.Labels["job-name"]; !ok || v != "myjob2" { + t.Errorf("Expected template labels not present") + } + if v, ok := job.Spec.Template.ObjectMeta.Labels["controller-uid"]; !ok || v != string(theUID) { + t.Errorf("Expected template labels not present: ok: %v, v: %v", ok, v) + } +} + func TestJobStatusStrategy(t *testing.T) { ctx := api.NewDefaultContext() if !StatusStrategy.NamespaceScoped() { diff --git a/test/e2e/job.go b/test/e2e/job.go index bf20cf6a22b..9c9a8b02153 100644 --- a/test/e2e/job.go +++ b/test/e2e/job.go @@ -204,8 +204,9 @@ func newTestJob(behavior, name string, rPol api.RestartPolicy, parallelism, comp Name: name, }, Spec: extensions.JobSpec{ - Parallelism: ¶llelism, - Completions: &completions, + Parallelism: ¶llelism, + Completions: &completions, + ManualSelector: newBool(true), Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: map[string]string{jobSelectorKey: name}, @@ -312,3 +313,9 @@ func waitForJobFail(c *client.Client, ns, jobName string) error { return false, nil }) } + +func newBool(val bool) *bool { + p := new(bool) + *p = val + return p +} diff --git a/test/integration/master_test.go b/test/integration/master_test.go index 1a19d0b2b61..8c62236d14a 100644 --- a/test/integration/master_test.go +++ b/test/integration/master_test.go @@ -204,26 +204,15 @@ var jobV1 string = ` "kind": "Job", "apiVersion": "batch/v1", "metadata": { - "name": "pi", - "labels": { - "app": "pi" - } + "name": "pi" }, "spec": { "parallelism": 1, "completions": 1, - "selector": { - "matchLabels": { - "app": "pi" - } - }, "template": { "metadata": { "name": "pi", - "creationTimestamp": null, - "labels": { - "app": "pi" - } + "creationTimestamp": null }, "spec": { "containers": [ From 79d4ab77fc0b358e02e18b46c0d13e6c6f953c4f Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Mon, 22 Feb 2016 12:06:16 -0800 Subject: [PATCH 2/4] Conversion between manualSelector and autoSelector --- pkg/apis/extensions/v1beta1/conversion.go | 106 ++++++++++++++++++ .../extensions/v1beta1/conversion_test.go | 83 ++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 pkg/apis/extensions/v1beta1/conversion_test.go diff --git a/pkg/apis/extensions/v1beta1/conversion.go b/pkg/apis/extensions/v1beta1/conversion.go index 595091e027c..198ed3f4c67 100644 --- a/pkg/apis/extensions/v1beta1/conversion.go +++ b/pkg/apis/extensions/v1beta1/conversion.go @@ -42,6 +42,8 @@ func addConversionFuncs(scheme *runtime.Scheme) { Convert_v1beta1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment, Convert_extensions_ReplicaSetSpec_To_v1beta1_ReplicaSetSpec, Convert_v1beta1_ReplicaSetSpec_To_extensions_ReplicaSetSpec, + Convert_extensions_JobSpec_To_v1beta1_JobSpec, + Convert_v1beta1_JobSpec_To_extensions_JobSpec, ) if err != nil { // If one of the conversion functions is malformed, detect it immediately. @@ -277,3 +279,107 @@ func Convert_v1beta1_ReplicaSetSpec_To_extensions_ReplicaSetSpec(in *ReplicaSetS } return nil } + +func Convert_extensions_JobSpec_To_v1beta1_JobSpec(in *extensions.JobSpec, out *JobSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*extensions.JobSpec))(in) + } + if in.Parallelism != nil { + out.Parallelism = new(int32) + *out.Parallelism = int32(*in.Parallelism) + } else { + out.Parallelism = nil + } + if in.Completions != nil { + out.Completions = new(int32) + *out.Completions = int32(*in.Completions) + } else { + out.Completions = nil + } + if in.ActiveDeadlineSeconds != nil { + out.ActiveDeadlineSeconds = new(int64) + *out.ActiveDeadlineSeconds = *in.ActiveDeadlineSeconds + } else { + out.ActiveDeadlineSeconds = nil + } + // unable to generate simple pointer conversion for unversioned.LabelSelector -> v1beta1.LabelSelector + if in.Selector != nil { + out.Selector = new(LabelSelector) + if err := Convert_unversioned_LabelSelector_To_v1beta1_LabelSelector(in.Selector, out.Selector, s); err != nil { + return err + } + } else { + out.Selector = nil + } + + // BEGIN non-standard conversion + // autoSelector has opposite meaning as manualSelector. + // in both cases, unset means false, and unset is always preferred to false. + // unset vs set-false distinction is not preserved. + manualSelector := in.ManualSelector != nil && *in.ManualSelector + autoSelector := !manualSelector + if autoSelector { + out.AutoSelector = new(bool) + *out.AutoSelector = true + } else { + out.AutoSelector = nil + } + // END non-standard conversion + + if err := Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + return nil +} + +func Convert_v1beta1_JobSpec_To_extensions_JobSpec(in *JobSpec, out *extensions.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 = int(*in.Parallelism) + } else { + out.Parallelism = nil + } + if in.Completions != nil { + out.Completions = new(int) + *out.Completions = int(*in.Completions) + } else { + out.Completions = nil + } + if in.ActiveDeadlineSeconds != nil { + out.ActiveDeadlineSeconds = new(int64) + *out.ActiveDeadlineSeconds = *in.ActiveDeadlineSeconds + } else { + out.ActiveDeadlineSeconds = nil + } + // unable to generate simple pointer conversion for v1beta1.LabelSelector -> unversioned.LabelSelector + if in.Selector != nil { + out.Selector = new(unversioned.LabelSelector) + if err := Convert_v1beta1_LabelSelector_To_unversioned_LabelSelector(in.Selector, out.Selector, s); err != nil { + return err + } + } else { + out.Selector = nil + } + + // BEGIN non-standard conversion + // autoSelector has opposite meaning as manualSelector. + // in both cases, unset means false, and unset is always preferred to false. + // unset vs set-false distinction is not preserved. + autoSelector := bool(in.AutoSelector != nil && *in.AutoSelector) + manualSelector := !autoSelector + if manualSelector { + out.ManualSelector = new(bool) + *out.ManualSelector = true + } else { + out.ManualSelector = nil + } + // END non-standard conversion + + if err := Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + return nil +} diff --git a/pkg/apis/extensions/v1beta1/conversion_test.go b/pkg/apis/extensions/v1beta1/conversion_test.go new file mode 100644 index 00000000000..a0e43dfbca8 --- /dev/null +++ b/pkg/apis/extensions/v1beta1/conversion_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2016 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 v1beta1_test + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + versioned "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" +) + +// TestJobSpecConversion tests that ManualSelector and AutoSelector +// are handled correctly. +func TestJobSpecConversion(t *testing.T) { + pTrue := new(bool) + *pTrue = true + pFalse := new(bool) + *pFalse = false + + // False or nil convert to true. + // True converts to nil. + tests := []struct { + in *bool + expectOut *bool + }{ + { + in: nil, + expectOut: pTrue, + }, + { + in: pFalse, + expectOut: pTrue, + }, + { + in: pTrue, + expectOut: nil, + }, + } + + // Test internal -> v1beta1. + for _, test := range tests { + i := &extensions.JobSpec{ + ManualSelector: test.in, + } + v := versioned.JobSpec{} + if err := api.Scheme.Convert(i, &v); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(test.expectOut, v.AutoSelector) { + t.Fatalf("want v1beta1.AutoSelector %v, got %v", test.expectOut, v.AutoSelector) + } + } + + // Test v1beta1 -> internal. + for _, test := range tests { + i := &versioned.JobSpec{ + AutoSelector: test.in, + } + e := extensions.JobSpec{} + if err := api.Scheme.Convert(i, &e); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(test.expectOut, e.ManualSelector) { + t.Fatalf("want extensions.ManualSelector %v, got %v", test.expectOut, e.ManualSelector) + } + } +} From 2afcc7d165f4b60cd29cf8e93b640bf2d708dfef Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Mon, 22 Feb 2016 10:19:27 -0800 Subject: [PATCH 3/4] Regenerate --- api/swagger-spec/batch_v1.json | 8 +- api/swagger-spec/extensions_v1beta1.json | 8 +- .../extensions/v1beta1/definitions.html | 11 +- pkg/apis/batch/v1/conversion_generated.go | 12 ++ pkg/apis/batch/v1/deep_copy_generated.go | 6 + pkg/apis/batch/v1/types.generated.go | 180 ++++++++++++----- .../batch/v1/types_swagger_doc_generated.go | 5 +- pkg/apis/extensions/types.generated.go | 184 +++++++++++++----- .../v1beta1/conversion_generated.go | 10 +- .../extensions/v1beta1/deep_copy_generated.go | 6 + .../extensions/v1beta1/types.generated.go | 180 ++++++++++++----- .../v1beta1/types_swagger_doc_generated.go | 5 +- 12 files changed, 442 insertions(+), 173 deletions(-) diff --git a/api/swagger-spec/batch_v1.json b/api/swagger-spec/batch_v1.json index 85fda7525e3..8cc5288359c 100644 --- a/api/swagger-spec/batch_v1.json +++ b/api/swagger-spec/batch_v1.json @@ -991,7 +991,7 @@ "completions": { "type": "integer", "format": "int32", - "description": "Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job." + "description": "Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md" }, "activeDeadlineSeconds": { "type": "integer", @@ -1000,7 +1000,11 @@ }, "selector": { "$ref": "v1.LabelSelector", - "description": "Selector is a label query over pods that should match the pod count. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors" + "description": "Selector is a label query over pods that should match the pod count. Normally, the system sets this field for you. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors" + }, + "manualSelector": { + "type": "boolean", + "description": "ManualSelector controls generation of pod labels and pod selectors. Leave `manualSelector` unset unless you are certain what you are doing. When false or unset, the system pick labels unique to this job and appends those labels to the pod template. When true, the user is responsible for picking unique labels and specifying the selector. Failure to pick a unique label may cause this and other jobs to not function correctly. However, You may see `manualSelector=true` in jobs that were created with the old `extensions/v1beta1` API. More info: http://releases.k8s.io/HEAD/docs/design/selector-generation.md" }, "template": { "$ref": "v1.PodTemplateSpec", diff --git a/api/swagger-spec/extensions_v1beta1.json b/api/swagger-spec/extensions_v1beta1.json index 56eb9eadc36..48834bc8ab0 100644 --- a/api/swagger-spec/extensions_v1beta1.json +++ b/api/swagger-spec/extensions_v1beta1.json @@ -7326,7 +7326,7 @@ "completions": { "type": "integer", "format": "int32", - "description": "Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job." + "description": "Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md" }, "activeDeadlineSeconds": { "type": "integer", @@ -7335,7 +7335,11 @@ }, "selector": { "$ref": "v1beta1.LabelSelector", - "description": "Selector is a label query over pods that should match the pod count. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors" + "description": "Selector is a label query over pods that should match the pod count. Normally, the system sets this field for you. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors" + }, + "autoSelector": { + "type": "boolean", + "description": "AutoSelector controls generation of pod labels and pod selectors. It was not present in the original extensions/v1beta1 Job definition, but exists to allow conversion from batch/v1 Jobs, where it corresponds to, but has the opposite meaning as, ManualSelector. More info: http://releases.k8s.io/HEAD/docs/design/selector-generation.md" }, "template": { "$ref": "v1.PodTemplateSpec", diff --git a/docs/api-reference/extensions/v1beta1/definitions.html b/docs/api-reference/extensions/v1beta1/definitions.html index 46b3aec66e4..bd650c29b8f 100755 --- a/docs/api-reference/extensions/v1beta1/definitions.html +++ b/docs/api-reference/extensions/v1beta1/definitions.html @@ -4437,7 +4437,7 @@ Populated by the system when a graceful deletion is requested. Read-only. More i

completions

-

Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job.

+

Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md

false

integer (int32)

@@ -4451,12 +4451,19 @@ Populated by the system when a graceful deletion is requested. Read-only. More i

selector

-

Selector is a label query over pods that should match the pod count. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors

+

Selector is a label query over pods that should match the pod count. Normally, the system sets this field for you. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors

false

v1beta1.LabelSelector

+

autoSelector

+

AutoSelector controls generation of pod labels and pod selectors. It was not present in the original extensions/v1beta1 Job definition, but exists to allow conversion from batch/v1 Jobs, where it corresponds to, but has the opposite meaning as, ManualSelector. More info: http://releases.k8s.io/HEAD/docs/design/selector-generation.md

+

false

+

boolean

+

false

+ +

template

Template is the object that describes the pod that will be created when executing a job. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md

true

diff --git a/pkg/apis/batch/v1/conversion_generated.go b/pkg/apis/batch/v1/conversion_generated.go index c7e4104922a..9a7b4876f60 100644 --- a/pkg/apis/batch/v1/conversion_generated.go +++ b/pkg/apis/batch/v1/conversion_generated.go @@ -2681,6 +2681,12 @@ func autoConvert_v1_JobSpec_To_extensions_JobSpec(in *JobSpec, out *extensions.J } else { out.Selector = nil } + if in.ManualSelector != nil { + out.ManualSelector = new(bool) + *out.ManualSelector = *in.ManualSelector + } else { + out.ManualSelector = nil + } if err := Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { return err } @@ -2885,6 +2891,12 @@ func autoConvert_extensions_JobSpec_To_v1_JobSpec(in *extensions.JobSpec, out *J } else { out.Selector = nil } + if in.ManualSelector != nil { + out.ManualSelector = new(bool) + *out.ManualSelector = *in.ManualSelector + } else { + out.ManualSelector = nil + } if err := Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { return err } diff --git a/pkg/apis/batch/v1/deep_copy_generated.go b/pkg/apis/batch/v1/deep_copy_generated.go index e91102dd970..3ddbd6d2b7a 100644 --- a/pkg/apis/batch/v1/deep_copy_generated.go +++ b/pkg/apis/batch/v1/deep_copy_generated.go @@ -1076,6 +1076,12 @@ func deepCopy_v1_JobSpec(in JobSpec, out *JobSpec, c *conversion.Cloner) error { } else { out.Selector = nil } + if in.ManualSelector != nil { + out.ManualSelector = new(bool) + *out.ManualSelector = *in.ManualSelector + } else { + out.ManualSelector = nil + } if err := deepCopy_v1_PodTemplateSpec(in.Template, &out.Template, c); err != nil { return err } diff --git a/pkg/apis/batch/v1/types.generated.go b/pkg/apis/batch/v1/types.generated.go index f73d599bb71..2c07db2168e 100644 --- a/pkg/apis/batch/v1/types.generated.go +++ b/pkg/apis/batch/v1/types.generated.go @@ -778,16 +778,17 @@ func (x *JobSpec) CodecEncodeSelf(e *codec1978.Encoder) { } else { yysep2 := !z.EncBinary() yy2arr2 := z.EncBasicHandle().StructToArray - var yyq2 [5]bool + var yyq2 [6]bool _, _, _ = yysep2, yyq2, yy2arr2 const yyr2 bool = false yyq2[0] = x.Parallelism != nil yyq2[1] = x.Completions != nil yyq2[2] = x.ActiveDeadlineSeconds != nil yyq2[3] = x.Selector != nil + yyq2[4] = x.ManualSelector != nil var yynn2 int if yyr2 || yy2arr2 { - r.EncodeArrayStart(5) + r.EncodeArrayStart(6) } else { yynn2 = 1 for _, b := range yyq2 { @@ -928,14 +929,49 @@ func (x *JobSpec) CodecEncodeSelf(e *codec1978.Encoder) { } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayElem1234) - yy22 := &x.Template - yy22.CodecEncodeSelf(e) + if yyq2[4] { + if x.ManualSelector == nil { + r.EncodeNil() + } else { + yy22 := *x.ManualSelector + yym23 := z.EncBinary() + _ = yym23 + if false { + } else { + r.EncodeBool(bool(yy22)) + } + } + } else { + r.EncodeNil() + } + } else { + if yyq2[4] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("manualSelector")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + if x.ManualSelector == nil { + r.EncodeNil() + } else { + yy24 := *x.ManualSelector + yym25 := z.EncBinary() + _ = yym25 + if false { + } else { + r.EncodeBool(bool(yy24)) + } + } + } + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + yy27 := &x.Template + yy27.CodecEncodeSelf(e) } else { z.EncSendContainerState(codecSelfer_containerMapKey1234) r.EncodeString(codecSelferC_UTF81234, string("template")) z.EncSendContainerState(codecSelfer_containerMapValue1234) - yy24 := &x.Template - yy24.CodecEncodeSelf(e) + yy29 := &x.Template + yy29.CodecEncodeSelf(e) } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayEnd1234) @@ -1057,12 +1093,28 @@ func (x *JobSpec) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { } x.Selector.CodecDecodeSelf(d) } + case "manualSelector": + if r.TryDecodeAsNil() { + if x.ManualSelector != nil { + x.ManualSelector = nil + } + } else { + if x.ManualSelector == nil { + x.ManualSelector = new(bool) + } + yym12 := z.DecBinary() + _ = yym12 + if false { + } else { + *((*bool)(x.ManualSelector)) = r.DecodeBool() + } + } case "template": if r.TryDecodeAsNil() { x.Template = pkg2_v1.PodTemplateSpec{} } else { - yyv11 := &x.Template - yyv11.CodecDecodeSelf(d) + yyv13 := &x.Template + yyv13.CodecDecodeSelf(d) } default: z.DecStructFieldNotFound(-1, yys3) @@ -1075,16 +1127,16 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { var h codecSelfer1234 z, r := codec1978.GenHelperDecoder(d) _, _, _ = h, z, r - var yyj12 int - var yyb12 bool - var yyhl12 bool = l >= 0 - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + var yyj14 int + var yyb14 bool + var yyhl14 bool = l >= 0 + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -1097,20 +1149,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Parallelism == nil { x.Parallelism = new(int32) } - yym14 := z.DecBinary() - _ = yym14 + yym16 := z.DecBinary() + _ = yym16 if false { } else { *((*int32)(x.Parallelism)) = int32(r.DecodeInt(32)) } } - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -1123,20 +1175,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Completions == nil { x.Completions = new(int32) } - yym16 := z.DecBinary() - _ = yym16 + yym18 := z.DecBinary() + _ = yym18 if false { } else { *((*int32)(x.Completions)) = int32(r.DecodeInt(32)) } } - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -1149,20 +1201,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.ActiveDeadlineSeconds == nil { x.ActiveDeadlineSeconds = new(int64) } - yym18 := z.DecBinary() - _ = yym18 + yym20 := z.DecBinary() + _ = yym20 if false { } else { *((*int64)(x.ActiveDeadlineSeconds)) = int64(r.DecodeInt(64)) } } - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -1177,13 +1229,39 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { } x.Selector.CodecDecodeSelf(d) } - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + if x.ManualSelector != nil { + x.ManualSelector = nil + } + } else { + if x.ManualSelector == nil { + x.ManualSelector = new(bool) + } + yym23 := z.DecBinary() + _ = yym23 + if false { + } else { + *((*bool)(x.ManualSelector)) = r.DecodeBool() + } + } + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l + } else { + yyb14 = r.CheckBreak() + } + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -1191,21 +1269,21 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.Template = pkg2_v1.PodTemplateSpec{} } else { - yyv20 := &x.Template - yyv20.CodecDecodeSelf(d) + yyv24 := &x.Template + yyv24.CodecDecodeSelf(d) } for { - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { break } z.DecSendContainerState(codecSelfer_containerArrayElem1234) - z.DecStructFieldNotFound(yyj12-1, "") + z.DecStructFieldNotFound(yyj14-1, "") } z.DecSendContainerState(codecSelfer_containerArrayEnd1234) } @@ -2789,7 +2867,7 @@ func (x codecSelfer1234) decSliceJob(v *[]Job, d *codec1978.Decoder) { yyrg1 := len(yyv1) > 0 yyv21 := yyv1 - yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 632) + yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 640) if yyrt1 { if yyrl1 <= cap(yyv1) { yyv1 = yyv1[:yyrl1] diff --git a/pkg/apis/batch/v1/types_swagger_doc_generated.go b/pkg/apis/batch/v1/types_swagger_doc_generated.go index 8e93bb83866..8b52558439e 100644 --- a/pkg/apis/batch/v1/types_swagger_doc_generated.go +++ b/pkg/apis/batch/v1/types_swagger_doc_generated.go @@ -65,9 +65,10 @@ func (JobList) SwaggerDoc() map[string]string { var map_JobSpec = map[string]string{ "": "JobSpec describes how the job execution will look like.", "parallelism": "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. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md", - "completions": "Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job.", + "completions": "Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md", "activeDeadlineSeconds": "Optional duration in seconds relative to the startTime that the job may be active before the system tries to terminate it; value must be positive integer", - "selector": "Selector is a label query over pods that should match the pod count. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors", + "selector": "Selector is a label query over pods that should match the pod count. Normally, the system sets this field for you. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors", + "manualSelector": "ManualSelector controls generation of pod labels and pod selectors. Leave `manualSelector` unset unless you are certain what you are doing. When false or unset, the system pick labels unique to this job and appends those labels to the pod template. When true, the user is responsible for picking unique labels and specifying the selector. Failure to pick a unique label may cause this and other jobs to not function correctly. However, You may see `manualSelector=true` in jobs that were created with the old `extensions/v1beta1` API. More info: http://releases.k8s.io/HEAD/docs/design/selector-generation.md", "template": "Template is the object that describes the pod that will be created when executing a job. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md", } diff --git a/pkg/apis/extensions/types.generated.go b/pkg/apis/extensions/types.generated.go index 719562774b6..2a29d959373 100644 --- a/pkg/apis/extensions/types.generated.go +++ b/pkg/apis/extensions/types.generated.go @@ -9925,16 +9925,17 @@ func (x *JobSpec) CodecEncodeSelf(e *codec1978.Encoder) { } else { yysep2 := !z.EncBinary() yy2arr2 := z.EncBasicHandle().StructToArray - var yyq2 [5]bool + var yyq2 [6]bool _, _, _ = yysep2, yyq2, yy2arr2 const yyr2 bool = false yyq2[0] = x.Parallelism != nil yyq2[1] = x.Completions != nil yyq2[2] = x.ActiveDeadlineSeconds != nil yyq2[3] = x.Selector != nil + yyq2[4] = x.ManualSelector != nil var yynn2 int if yyr2 || yy2arr2 { - r.EncodeArrayStart(5) + r.EncodeArrayStart(6) } else { yynn2 = 1 for _, b := range yyq2 { @@ -10087,14 +10088,49 @@ func (x *JobSpec) CodecEncodeSelf(e *codec1978.Encoder) { } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayElem1234) - yy22 := &x.Template - yy22.CodecEncodeSelf(e) + if yyq2[4] { + if x.ManualSelector == nil { + r.EncodeNil() + } else { + yy22 := *x.ManualSelector + yym23 := z.EncBinary() + _ = yym23 + if false { + } else { + r.EncodeBool(bool(yy22)) + } + } + } else { + r.EncodeNil() + } + } else { + if yyq2[4] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("manualSelector")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + if x.ManualSelector == nil { + r.EncodeNil() + } else { + yy24 := *x.ManualSelector + yym25 := z.EncBinary() + _ = yym25 + if false { + } else { + r.EncodeBool(bool(yy24)) + } + } + } + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + yy27 := &x.Template + yy27.CodecEncodeSelf(e) } else { z.EncSendContainerState(codecSelfer_containerMapKey1234) r.EncodeString(codecSelferC_UTF81234, string("template")) z.EncSendContainerState(codecSelfer_containerMapValue1234) - yy24 := &x.Template - yy24.CodecEncodeSelf(e) + yy29 := &x.Template + yy29.CodecEncodeSelf(e) } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayEnd1234) @@ -10222,12 +10258,28 @@ func (x *JobSpec) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { z.DecFallback(x.Selector, false) } } + case "manualSelector": + if r.TryDecodeAsNil() { + if x.ManualSelector != nil { + x.ManualSelector = nil + } + } else { + if x.ManualSelector == nil { + x.ManualSelector = new(bool) + } + yym13 := z.DecBinary() + _ = yym13 + if false { + } else { + *((*bool)(x.ManualSelector)) = r.DecodeBool() + } + } case "template": if r.TryDecodeAsNil() { x.Template = pkg2_api.PodTemplateSpec{} } else { - yyv12 := &x.Template - yyv12.CodecDecodeSelf(d) + yyv14 := &x.Template + yyv14.CodecDecodeSelf(d) } default: z.DecStructFieldNotFound(-1, yys3) @@ -10240,16 +10292,16 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { var h codecSelfer1234 z, r := codec1978.GenHelperDecoder(d) _, _, _ = h, z, r - var yyj13 int - var yyb13 bool - var yyhl13 bool = l >= 0 - yyj13++ - if yyhl13 { - yyb13 = yyj13 > l + var yyj15 int + var yyb15 bool + var yyhl15 bool = l >= 0 + yyj15++ + if yyhl15 { + yyb15 = yyj15 > l } else { - yyb13 = r.CheckBreak() + yyb15 = r.CheckBreak() } - if yyb13 { + if yyb15 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10262,20 +10314,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Parallelism == nil { x.Parallelism = new(int) } - yym15 := z.DecBinary() - _ = yym15 + yym17 := z.DecBinary() + _ = yym17 if false { } else { *((*int)(x.Parallelism)) = int(r.DecodeInt(codecSelferBitsize1234)) } } - yyj13++ - if yyhl13 { - yyb13 = yyj13 > l + yyj15++ + if yyhl15 { + yyb15 = yyj15 > l } else { - yyb13 = r.CheckBreak() + yyb15 = r.CheckBreak() } - if yyb13 { + if yyb15 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10288,20 +10340,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Completions == nil { x.Completions = new(int) } - yym17 := z.DecBinary() - _ = yym17 + yym19 := z.DecBinary() + _ = yym19 if false { } else { *((*int)(x.Completions)) = int(r.DecodeInt(codecSelferBitsize1234)) } } - yyj13++ - if yyhl13 { - yyb13 = yyj13 > l + yyj15++ + if yyhl15 { + yyb15 = yyj15 > l } else { - yyb13 = r.CheckBreak() + yyb15 = r.CheckBreak() } - if yyb13 { + if yyb15 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10314,20 +10366,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.ActiveDeadlineSeconds == nil { x.ActiveDeadlineSeconds = new(int64) } - yym19 := z.DecBinary() - _ = yym19 + yym21 := z.DecBinary() + _ = yym21 if false { } else { *((*int64)(x.ActiveDeadlineSeconds)) = int64(r.DecodeInt(64)) } } - yyj13++ - if yyhl13 { - yyb13 = yyj13 > l + yyj15++ + if yyhl15 { + yyb15 = yyj15 > l } else { - yyb13 = r.CheckBreak() + yyb15 = r.CheckBreak() } - if yyb13 { + if yyb15 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10340,21 +10392,47 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Selector == nil { x.Selector = new(pkg1_unversioned.LabelSelector) } - yym21 := z.DecBinary() - _ = yym21 + yym23 := z.DecBinary() + _ = yym23 if false { } else if z.HasExtensions() && z.DecExt(x.Selector) { } else { z.DecFallback(x.Selector, false) } } - yyj13++ - if yyhl13 { - yyb13 = yyj13 > l + yyj15++ + if yyhl15 { + yyb15 = yyj15 > l } else { - yyb13 = r.CheckBreak() + yyb15 = r.CheckBreak() } - if yyb13 { + if yyb15 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + if x.ManualSelector != nil { + x.ManualSelector = nil + } + } else { + if x.ManualSelector == nil { + x.ManualSelector = new(bool) + } + yym25 := z.DecBinary() + _ = yym25 + if false { + } else { + *((*bool)(x.ManualSelector)) = r.DecodeBool() + } + } + yyj15++ + if yyhl15 { + yyb15 = yyj15 > l + } else { + yyb15 = r.CheckBreak() + } + if yyb15 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10362,21 +10440,21 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.Template = pkg2_api.PodTemplateSpec{} } else { - yyv22 := &x.Template - yyv22.CodecDecodeSelf(d) + yyv26 := &x.Template + yyv26.CodecDecodeSelf(d) } for { - yyj13++ - if yyhl13 { - yyb13 = yyj13 > l + yyj15++ + if yyhl15 { + yyb15 = yyj15 > l } else { - yyb13 = r.CheckBreak() + yyb15 = r.CheckBreak() } - if yyb13 { + if yyb15 { break } z.DecSendContainerState(codecSelfer_containerArrayElem1234) - z.DecStructFieldNotFound(yyj13-1, "") + z.DecStructFieldNotFound(yyj15-1, "") } z.DecSendContainerState(codecSelfer_containerArrayEnd1234) } @@ -19286,7 +19364,7 @@ func (x codecSelfer1234) decSliceJob(v *[]Job, d *codec1978.Decoder) { yyrg1 := len(yyv1) > 0 yyv21 := yyv1 - yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 616) + yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 624) if yyrt1 { if yyrl1 <= cap(yyv1) { yyv1 = yyv1[:yyrl1] diff --git a/pkg/apis/extensions/v1beta1/conversion_generated.go b/pkg/apis/extensions/v1beta1/conversion_generated.go index 6d4d3dc59fc..7a57339c671 100644 --- a/pkg/apis/extensions/v1beta1/conversion_generated.go +++ b/pkg/apis/extensions/v1beta1/conversion_generated.go @@ -3460,16 +3460,13 @@ func autoConvert_extensions_JobSpec_To_v1beta1_JobSpec(in *extensions.JobSpec, o } else { out.Selector = nil } + // in.ManualSelector has no peer in out if err := Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { return err } return nil } -func Convert_extensions_JobSpec_To_v1beta1_JobSpec(in *extensions.JobSpec, out *JobSpec, s conversion.Scope) error { - return autoConvert_extensions_JobSpec_To_v1beta1_JobSpec(in, out, s) -} - func autoConvert_extensions_JobStatus_To_v1beta1_JobStatus(in *extensions.JobStatus, out *JobStatus, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*extensions.JobStatus))(in) @@ -4730,16 +4727,13 @@ func autoConvert_v1beta1_JobSpec_To_extensions_JobSpec(in *JobSpec, out *extensi } else { out.Selector = nil } + // in.AutoSelector has no peer in out if err := Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { return err } return nil } -func Convert_v1beta1_JobSpec_To_extensions_JobSpec(in *JobSpec, out *extensions.JobSpec, s conversion.Scope) error { - return autoConvert_v1beta1_JobSpec_To_extensions_JobSpec(in, out, s) -} - func autoConvert_v1beta1_JobStatus_To_extensions_JobStatus(in *JobStatus, out *extensions.JobStatus, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*JobStatus))(in) diff --git a/pkg/apis/extensions/v1beta1/deep_copy_generated.go b/pkg/apis/extensions/v1beta1/deep_copy_generated.go index 447f17a5466..50684723bc0 100644 --- a/pkg/apis/extensions/v1beta1/deep_copy_generated.go +++ b/pkg/apis/extensions/v1beta1/deep_copy_generated.go @@ -1565,6 +1565,12 @@ func deepCopy_v1beta1_JobSpec(in JobSpec, out *JobSpec, c *conversion.Cloner) er } else { out.Selector = nil } + if in.AutoSelector != nil { + out.AutoSelector = new(bool) + *out.AutoSelector = *in.AutoSelector + } else { + out.AutoSelector = nil + } if err := deepCopy_v1_PodTemplateSpec(in.Template, &out.Template, c); err != nil { return err } diff --git a/pkg/apis/extensions/v1beta1/types.generated.go b/pkg/apis/extensions/v1beta1/types.generated.go index 5bd64943027..9022e6e5435 100644 --- a/pkg/apis/extensions/v1beta1/types.generated.go +++ b/pkg/apis/extensions/v1beta1/types.generated.go @@ -9935,16 +9935,17 @@ func (x *JobSpec) CodecEncodeSelf(e *codec1978.Encoder) { } else { yysep2 := !z.EncBinary() yy2arr2 := z.EncBasicHandle().StructToArray - var yyq2 [5]bool + var yyq2 [6]bool _, _, _ = yysep2, yyq2, yy2arr2 const yyr2 bool = false yyq2[0] = x.Parallelism != nil yyq2[1] = x.Completions != nil yyq2[2] = x.ActiveDeadlineSeconds != nil yyq2[3] = x.Selector != nil + yyq2[4] = x.AutoSelector != nil var yynn2 int if yyr2 || yy2arr2 { - r.EncodeArrayStart(5) + r.EncodeArrayStart(6) } else { yynn2 = 1 for _, b := range yyq2 { @@ -10085,14 +10086,49 @@ func (x *JobSpec) CodecEncodeSelf(e *codec1978.Encoder) { } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayElem1234) - yy22 := &x.Template - yy22.CodecEncodeSelf(e) + if yyq2[4] { + if x.AutoSelector == nil { + r.EncodeNil() + } else { + yy22 := *x.AutoSelector + yym23 := z.EncBinary() + _ = yym23 + if false { + } else { + r.EncodeBool(bool(yy22)) + } + } + } else { + r.EncodeNil() + } + } else { + if yyq2[4] { + z.EncSendContainerState(codecSelfer_containerMapKey1234) + r.EncodeString(codecSelferC_UTF81234, string("autoSelector")) + z.EncSendContainerState(codecSelfer_containerMapValue1234) + if x.AutoSelector == nil { + r.EncodeNil() + } else { + yy24 := *x.AutoSelector + yym25 := z.EncBinary() + _ = yym25 + if false { + } else { + r.EncodeBool(bool(yy24)) + } + } + } + } + if yyr2 || yy2arr2 { + z.EncSendContainerState(codecSelfer_containerArrayElem1234) + yy27 := &x.Template + yy27.CodecEncodeSelf(e) } else { z.EncSendContainerState(codecSelfer_containerMapKey1234) r.EncodeString(codecSelferC_UTF81234, string("template")) z.EncSendContainerState(codecSelfer_containerMapValue1234) - yy24 := &x.Template - yy24.CodecEncodeSelf(e) + yy29 := &x.Template + yy29.CodecEncodeSelf(e) } if yyr2 || yy2arr2 { z.EncSendContainerState(codecSelfer_containerArrayEnd1234) @@ -10214,12 +10250,28 @@ func (x *JobSpec) codecDecodeSelfFromMap(l int, d *codec1978.Decoder) { } x.Selector.CodecDecodeSelf(d) } + case "autoSelector": + if r.TryDecodeAsNil() { + if x.AutoSelector != nil { + x.AutoSelector = nil + } + } else { + if x.AutoSelector == nil { + x.AutoSelector = new(bool) + } + yym12 := z.DecBinary() + _ = yym12 + if false { + } else { + *((*bool)(x.AutoSelector)) = r.DecodeBool() + } + } case "template": if r.TryDecodeAsNil() { x.Template = pkg2_v1.PodTemplateSpec{} } else { - yyv11 := &x.Template - yyv11.CodecDecodeSelf(d) + yyv13 := &x.Template + yyv13.CodecDecodeSelf(d) } default: z.DecStructFieldNotFound(-1, yys3) @@ -10232,16 +10284,16 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { var h codecSelfer1234 z, r := codec1978.GenHelperDecoder(d) _, _, _ = h, z, r - var yyj12 int - var yyb12 bool - var yyhl12 bool = l >= 0 - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + var yyj14 int + var yyb14 bool + var yyhl14 bool = l >= 0 + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10254,20 +10306,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Parallelism == nil { x.Parallelism = new(int32) } - yym14 := z.DecBinary() - _ = yym14 + yym16 := z.DecBinary() + _ = yym16 if false { } else { *((*int32)(x.Parallelism)) = int32(r.DecodeInt(32)) } } - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10280,20 +10332,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.Completions == nil { x.Completions = new(int32) } - yym16 := z.DecBinary() - _ = yym16 + yym18 := z.DecBinary() + _ = yym18 if false { } else { *((*int32)(x.Completions)) = int32(r.DecodeInt(32)) } } - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10306,20 +10358,20 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if x.ActiveDeadlineSeconds == nil { x.ActiveDeadlineSeconds = new(int64) } - yym18 := z.DecBinary() - _ = yym18 + yym20 := z.DecBinary() + _ = yym20 if false { } else { *((*int64)(x.ActiveDeadlineSeconds)) = int64(r.DecodeInt(64)) } } - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10334,13 +10386,39 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { } x.Selector.CodecDecodeSelf(d) } - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { + z.DecSendContainerState(codecSelfer_containerArrayEnd1234) + return + } + z.DecSendContainerState(codecSelfer_containerArrayElem1234) + if r.TryDecodeAsNil() { + if x.AutoSelector != nil { + x.AutoSelector = nil + } + } else { + if x.AutoSelector == nil { + x.AutoSelector = new(bool) + } + yym23 := z.DecBinary() + _ = yym23 + if false { + } else { + *((*bool)(x.AutoSelector)) = r.DecodeBool() + } + } + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l + } else { + yyb14 = r.CheckBreak() + } + if yyb14 { z.DecSendContainerState(codecSelfer_containerArrayEnd1234) return } @@ -10348,21 +10426,21 @@ func (x *JobSpec) codecDecodeSelfFromArray(l int, d *codec1978.Decoder) { if r.TryDecodeAsNil() { x.Template = pkg2_v1.PodTemplateSpec{} } else { - yyv20 := &x.Template - yyv20.CodecDecodeSelf(d) + yyv24 := &x.Template + yyv24.CodecDecodeSelf(d) } for { - yyj12++ - if yyhl12 { - yyb12 = yyj12 > l + yyj14++ + if yyhl14 { + yyb14 = yyj14 > l } else { - yyb12 = r.CheckBreak() + yyb14 = r.CheckBreak() } - if yyb12 { + if yyb14 { break } z.DecSendContainerState(codecSelfer_containerArrayElem1234) - z.DecStructFieldNotFound(yyj12-1, "") + z.DecStructFieldNotFound(yyj14-1, "") } z.DecSendContainerState(codecSelfer_containerArrayEnd1234) } @@ -20615,7 +20693,7 @@ func (x codecSelfer1234) decSliceJob(v *[]Job, d *codec1978.Decoder) { yyrg1 := len(yyv1) > 0 yyv21 := yyv1 - yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 632) + yyrl1, yyrt1 = z.DecInferLen(yyl1, z.DecBasicHandle().MaxInitLen, 640) if yyrt1 { if yyrl1 <= cap(yyv1) { yyv1 = yyv1[:yyrl1] diff --git a/pkg/apis/extensions/v1beta1/types_swagger_doc_generated.go b/pkg/apis/extensions/v1beta1/types_swagger_doc_generated.go index 3f29ee3fea9..da3cef13247 100644 --- a/pkg/apis/extensions/v1beta1/types_swagger_doc_generated.go +++ b/pkg/apis/extensions/v1beta1/types_swagger_doc_generated.go @@ -417,9 +417,10 @@ func (JobList) SwaggerDoc() map[string]string { var map_JobSpec = map[string]string{ "": "JobSpec describes how the job execution will look like.", "parallelism": "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. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md", - "completions": "Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job.", + "completions": "Completions specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md", "activeDeadlineSeconds": "Optional duration in seconds relative to the startTime that the job may be active before the system tries to terminate it; value must be positive integer", - "selector": "Selector is a label query over pods that should match the pod count. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors", + "selector": "Selector is a label query over pods that should match the pod count. Normally, the system sets this field for you. More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors", + "autoSelector": "AutoSelector controls generation of pod labels and pod selectors. It was not present in the original extensions/v1beta1 Job definition, but exists to allow conversion from batch/v1 Jobs, where it corresponds to, but has the opposite meaning as, ManualSelector. More info: http://releases.k8s.io/HEAD/docs/design/selector-generation.md", "template": "Template is the object that describes the pod that will be created when executing a job. More info: http://releases.k8s.io/HEAD/docs/user-guide/jobs.md", } From 095a85e76ec8a23b8e597a4762488eef3208c278 Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Wed, 24 Feb 2016 23:05:02 -0800 Subject: [PATCH 4/4] Update docs and examples to batch/v1 Job Documented manualSelector field. Documented that you do not need to provide a selector or unique labels with batch/v1 Job. Updated all Job examples to apiVersion: batch/v1 Updated all Job examples to use generated selectors. --- docs/user-guide/job.yaml | 7 +- docs/user-guide/jobs.md | 89 +++++++++++++++++++------ examples/job/expansions/README.md | 59 ++++++++-------- examples/job/expansions/job.yaml.jinja2 | 12 ++-- examples/job/expansions/job.yaml.txt | 11 ++- examples/job/work-queue-1/README.md | 7 +- examples/job/work-queue-1/job.yaml | 7 +- examples/job/work-queue-2/README.md | 7 +- examples/job/work-queue-2/job.yaml | 7 +- 9 files changed, 108 insertions(+), 98 deletions(-) diff --git a/docs/user-guide/job.yaml b/docs/user-guide/job.yaml index c1c70082337..ece4512a8ac 100644 --- a/docs/user-guide/job.yaml +++ b/docs/user-guide/job.yaml @@ -1,16 +1,11 @@ -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: pi spec: - selector: - matchLabels: - app: pi template: metadata: name: pi - labels: - app: pi spec: containers: - name: pi diff --git a/docs/user-guide/jobs.md b/docs/user-guide/jobs.md index 5486acff8ec..41d7e630007 100644 --- a/docs/user-guide/jobs.md +++ b/docs/user-guide/jobs.md @@ -47,6 +47,8 @@ Documentation for other releases can be found at - [Controlling Parallelism](#controlling-parallelism) - [Handling Pod and Container Failures](#handling-pod-and-container-failures) - [Job Patterns](#job-patterns) + - [Advanced Usage](#advanced-usage) + - [Specifying your own pod selector](#specifying-your-own-pod-selector) - [Alternatives](#alternatives) - [Bare Pods](#bare-pods) - [Replication Controller](#replication-controller) @@ -76,19 +78,14 @@ It takes around 10s to complete. ```yaml -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: pi spec: - selector: - matchLabels: - app: pi template: metadata: name: pi - labels: - app: pi spec: containers: - name: pi @@ -170,21 +167,9 @@ Only a [`RestartPolicy`](pod-states.md) equal to `Never` or `OnFailure` are allo ### Pod Selector -The `.spec.selector` field is a label query over a set of pods. +The `.spec.selector` field is optional. In almost all cases you should not specify it. +See section [specifying your own pod selector](#specifying-your-own-pod-selector). -The `spec.selector` is an object consisting of two fields: -* `matchLabels` - works the same as the `.spec.selector` of a [ReplicationController](replication-controller.md) -* `matchExpressions` - allows to build more sophisticated selectors by specifying key, - list of values and an operator that relates the key and values. - -When the two are specified the result is ANDed. - -If `.spec.selector` is unspecified, `.spec.selector.matchLabels` will be defaulted to -`.spec.template.metadata.labels`. - -Also you should not normally create any pods whose labels match this selector, either directly, -via another Job, or via another controller such as ReplicationController. Otherwise, the Job will -think that those pods were created by it. Kubernetes will not stop you from doing this. ### Parallel Jobs @@ -323,6 +308,70 @@ Here, `W` is the number of work items. | Single Job with Static Work Assignment | W | any | +## Advanced Usage + +### Specifying your own pod selector + +Normally, when you create a job object, you do not specify `spec.selector`. +The system defaulting logic adds this field when the job is created. +It picks a selector value that will not overlap with any other jobs. + +However, in some cases, you might need to override this automatically set selector. +To do this, you can specify the `spec.selector` of the job. + +Be very careful when doing this. If you specify a label selector which is not +unique to the pods of that job, and which matches unrelated pods, then pods of the unrelated +job may be deleted, or this job may count other pods as completing it, or one or both +of the jobs may refuse to create pods or run to completion. If a non-unique selector is +chosen, then other controllers (e.g. ReplicationController) and their pods may behave +in unpredicatable ways too. Kubernetes will not stop you from making a mistake when +specifying `spec.selector`. + +Here is an example of a case when you might want to use this feature. + +Say job `old` is already running. You want existing pods +to keep running, but you want the rest of the pods it creates +to use a different pod template and for the job to have a new name. +You cannot update the job because these fields are not updatable. +Therefore, you delete job `old` but leave its pods +running, using `kubectl delete jobs/old-one --cascade=false`. +Before deleting it, you make a note of what selector it uses: + +``` +kind: Job +metadata: + name: old + ... +spec: + selector: + matchLabels: + job-uid: a8f3d00d-c6d2-11e5-9f87-42010af00002 + ... +``` + +Then you create a new job with name `new` and you explicitly specify the same selector. +Since the existing pods have label `job-uid=a8f3d00d-c6d2-11e5-9f87-42010af00002`, +they are controlled by job `new` as well. + +You need to specify `manualSelector: true` in the new job since you are not using +the selector that the system normally generates for you automatically. + +``` +kind: Job +metadata: + name: new + ... +spec: + manualSelector: true + selector: + matchLabels: + job-uid: a8f3d00d-c6d2-11e5-9f87-42010af00002 + ... +``` + +The new Job itself will have a different uid from `a8f3d00d-c6d2-11e5-9f87-42010af00002`. Setting +`manualSelector: true` tells the system to that you know what you are doing and to allow this +mismatch. ## Alternatives diff --git a/examples/job/expansions/README.md b/examples/job/expansions/README.md index 739755ccbf9..9b409080861 100644 --- a/examples/job/expansions/README.md +++ b/examples/job/expansions/README.md @@ -40,21 +40,18 @@ First, create a template of a Job object: ``` -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: process-item-$ITEM + labels: + jobgroup: jobexample spec: - selector: - matchLabels: - app: jobexample - item: $ITEM template: metadata: name: jobexample labels: - app: jobexample - item: $ITEM + jobgroup: jobexample spec: containers: - name: c @@ -75,12 +72,14 @@ In a real use case, the processing would be some substantial computation, such a of a movie, or processing a range of rows in a database. The "$ITEM" parameter would specify for example, the frame number or the row range. -This Job has two labels. The first label, `app=jobexample`, distinguishes this group of jobs from -other groups of jobs (these are not shown, but there might be other ones). This label -makes it convenient to operate on all the jobs in the group at once. The second label, with -key `item`, distinguishes individual jobs in the group. Each Job object needs to have -a unique label that no other job has. This is it. -Neither of these label keys are special to kubernetes -- you can pick your own label scheme. +This Job and its Pod template have a label: `jobgroup=jobexample`. There is nothing special +to the system about this label. This label +makes it convenient to operate on all the jobs in this group at once. +We also put the same label on the pod template so that we can check on all Pods of these Jobs +with a single command. +After the job is created, the system will add more labels that distinguish one Job's pods +from another Job's pods. +Note that the label key `jobgroup` is not special to Kubernetes. you can pick your own label scheme. Next, expand the template into multiple files, one for each item to be processed. @@ -113,27 +112,25 @@ job "process-item-cherry" created Now, check on the jobs: ```console -$ kubectl get jobs -l app=jobexample -L item -JOB CONTAINER(S) IMAGE(S) SELECTOR SUCCESSFUL ITEM -process-item-apple c busybox app in (jobexample),item in (apple) 1 apple -process-item-banana c busybox app in (jobexample),item in (banana) 1 banana -process-item-cherry c busybox app in (jobexample),item in (cherry) 1 cherry +$ kubectl get jobs -l app=jobexample +JOB CONTAINER(S) IMAGE(S) SELECTOR SUCCESSFUL +process-item-apple c busybox app in (jobexample),item in (apple) 1 +process-item-banana c busybox app in (jobexample),item in (banana) 1 +process-item-cherry c busybox app in (jobexample),item in (cherry) 1 ``` Here we use the `-l` option to select all jobs that are part of this group of jobs. (There might be other unrelated jobs in the system that we do not care to see.) -The `-L` option adds an extra column with just the `item` label value. - We can check on the pods as well using the same label selector: ```console -$ kubectl get pods -l app=jobexample -L item -NAME READY STATUS RESTARTS AGE ITEM -process-item-apple-kixwv 0/1 Completed 0 4m apple -process-item-banana-wrsf7 0/1 Completed 0 4m banana -process-item-cherry-dnfu9 0/1 Completed 0 4m cherry +$ kubectl get pods -l app=jobexample +NAME READY STATUS RESTARTS AGE +process-item-apple-kixwv 0/1 Completed 0 4m +process-item-banana-wrsf7 0/1 Completed 0 4m +process-item-cherry-dnfu9 0/1 Completed 0 4m ``` There is not a single command to check on the output of all jobs at once, @@ -170,21 +167,17 @@ First, download or paste the following template file to a file called `job.yaml. {%- for p in params %} {%- set name = p["name"] %} {%- set url = p["url"] %} -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: jobexample-{{ name }} + labels: + jobgroup: jobexample spec: - selector: - matchLabels: - app: jobexample - item: {{ name }} template: - metadata: name: jobexample labels: - app: jobexample - item: {{ name }} + jobgroup: jobexample spec: containers: - name: c diff --git a/examples/job/expansions/job.yaml.jinja2 b/examples/job/expansions/job.yaml.jinja2 index 962505a522a..0577238517a 100644 --- a/examples/job/expansions/job.yaml.jinja2 +++ b/examples/job/expansions/job.yaml.jinja2 @@ -5,21 +5,17 @@ {%- for p in params %} {%- set name = p["name"] %} {%- set url = p["url"] %} -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: jobexample-{{ name }} + labels: + jobgroup: jobexample spec: - selector: - matchLabels: - app: jobexample - item: {{ name }} template: - metadata: name: jobexample labels: - app: jobexample - item: {{ name }} + jobgroup: jobexample spec: containers: - name: c diff --git a/examples/job/expansions/job.yaml.txt b/examples/job/expansions/job.yaml.txt index 318c61b5996..790025b38b8 100644 --- a/examples/job/expansions/job.yaml.txt +++ b/examples/job/expansions/job.yaml.txt @@ -1,18 +1,15 @@ -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: process-item-$ITEM + labels: + jobgroup: jobexample spec: - selector: - matchLabels: - app: jobexample - item: $ITEM template: metadata: name: jobexample labels: - app: jobexample - item: $ITEM + jobgroup: jobexample spec: containers: - name: c diff --git a/examples/job/work-queue-1/README.md b/examples/job/work-queue-1/README.md index 57c71d667ea..5fab5b3fc3e 100644 --- a/examples/job/work-queue-1/README.md +++ b/examples/job/work-queue-1/README.md @@ -262,21 +262,16 @@ image to match the name you used, and call it `./job.yaml`. ```yaml -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: job-wq-1 spec: - selector: - matchLabels: - app: job-wq-1 completions: 8 parallelism: 2 template: metadata: name: job-wq-1 - labels: - app: job-wq-1 spec: containers: - name: c diff --git a/examples/job/work-queue-1/job.yaml b/examples/job/work-queue-1/job.yaml index 50c1730f398..d2696ed0222 100644 --- a/examples/job/work-queue-1/job.yaml +++ b/examples/job/work-queue-1/job.yaml @@ -1,18 +1,13 @@ -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: job-wq-1 spec: - selector: - matchLabels: - app: job-wq-1 completions: 8 parallelism: 2 template: metadata: name: job-wq-1 - labels: - app: job-wq-1 spec: containers: - name: c diff --git a/examples/job/work-queue-2/README.md b/examples/job/work-queue-2/README.md index ad5066312ab..31e1ad9049d 100644 --- a/examples/job/work-queue-2/README.md +++ b/examples/job/work-queue-2/README.md @@ -217,20 +217,15 @@ Here is the job definition: ```yaml -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: job-wq-2 spec: - selector: - matchLabels: - app: job-wq-2 parallelism: 2 template: metadata: name: job-wq-2 - labels: - app: job-wq-2 spec: containers: - name: c diff --git a/examples/job/work-queue-2/job.yaml b/examples/job/work-queue-2/job.yaml index 09a55ec4b61..ee7a06c7329 100644 --- a/examples/job/work-queue-2/job.yaml +++ b/examples/job/work-queue-2/job.yaml @@ -1,17 +1,12 @@ -apiVersion: extensions/v1beta1 +apiVersion: batch/v1 kind: Job metadata: name: job-wq-2 spec: - selector: - matchLabels: - app: job-wq-2 parallelism: 2 template: metadata: name: job-wq-2 - labels: - app: job-wq-2 spec: containers: - name: c