Support for the Job managedBy field (alpha) (#123273)
* support for the managed-by label in Job * Use managedBy field instead of managed-by label * Additional review remarks * Review remarks 2 * review remarks 3 * Skip cleanup of finalizers for job with custom managedBy * Drop the performance optimization * imrpove logs
This commit is contained in:
24
api/openapi-spec/swagger.json
generated
24
api/openapi-spec/swagger.json
generated
@@ -4295,6 +4295,10 @@
|
|||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"managedBy": {
|
||||||
|
"description": "ManagedBy field indicates the controller that manages a Job. The k8s Job controller reconciles jobs which don't have this field at all or the field value is the reserved string `kubernetes.io/job-controller`, but skips reconciling Jobs with a custom value for this field. The value must be a valid domain-prefixed path (e.g. acme.io/foo) - all characters before the first \"/\" must be a valid subdomain as defined by RFC 1123. All characters trailing the first \"/\" must be valid HTTP Path characters as defined by RFC 3986. The value cannot exceed 64 characters.\n\nThis field is alpha-level. The job controller accepts setting the field when the feature gate JobManagedBy is enabled (disabled by default).",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"manualSelector": {
|
"manualSelector": {
|
||||||
"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: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#specifying-your-own-pod-selector",
|
"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: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#specifying-your-own-pod-selector",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -4344,7 +4348,7 @@
|
|||||||
"description": "JobStatus represents the current state of a Job.",
|
"description": "JobStatus represents the current state of a Job.",
|
||||||
"properties": {
|
"properties": {
|
||||||
"active": {
|
"active": {
|
||||||
"description": "The number of pending and running pods which are not terminating (without a deletionTimestamp).",
|
"description": "The number of pending and running pods which are not terminating (without a deletionTimestamp). The value is zero for finished jobs.",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -4354,10 +4358,10 @@
|
|||||||
},
|
},
|
||||||
"completionTime": {
|
"completionTime": {
|
||||||
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time",
|
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time",
|
||||||
"description": "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. The completion time is only set when the job finishes successfully."
|
"description": "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. The completion time is set when the job finishes successfully, and only then. The value cannot be updated or removed. The value indicates the same or later point in time as the startTime field."
|
||||||
},
|
},
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"description": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
|
"description": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true.\n\nA job is considered finished when it is in a terminal condition, either \"Complete\" or \"Failed\". At that point, all pods of the job are in terminal phase. Job cannot be both in the \"Complete\" and \"Failed\" conditions. Additionally, it cannot be in the \"Complete\" and \"FailureTarget\" conditions. The \"Complete\", \"Failed\" and \"FailureTarget\" conditions cannot be disabled.\n\nMore info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/io.k8s.api.batch.v1.JobCondition"
|
"$ref": "#/definitions/io.k8s.api.batch.v1.JobCondition"
|
||||||
},
|
},
|
||||||
@@ -4367,36 +4371,36 @@
|
|||||||
"x-kubernetes-patch-strategy": "merge"
|
"x-kubernetes-patch-strategy": "merge"
|
||||||
},
|
},
|
||||||
"failed": {
|
"failed": {
|
||||||
"description": "The number of pods which reached phase Failed.",
|
"description": "The number of pods which reached phase Failed. The value increases monotonically.",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"failedIndexes": {
|
"failedIndexes": {
|
||||||
"description": "FailedIndexes holds the failed indexes when backoffLimitPerIndex=true. The indexes are represented in the text format analogous as for the `completedIndexes` field, ie. they are kept as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the failed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\". This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` feature gate is enabled (enabled by default).",
|
"description": "FailedIndexes holds the failed indexes when spec.backoffLimitPerIndex is set. The indexes are represented in the text format analogous as for the `completedIndexes` field, ie. they are kept as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the failed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\". The set of failed indexes cannot overlap with the set of completed indexes.\n\nThis field is beta-level. It can be used when the `JobBackoffLimitPerIndex` feature gate is enabled (enabled by default).",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"ready": {
|
"ready": {
|
||||||
"description": "The number of pods which have a Ready condition.",
|
"description": "The number of pods which have a Ready condition. The value is zero (or null) for finished jobs.",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"startTime": {
|
"startTime": {
|
||||||
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time",
|
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time",
|
||||||
"description": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC."
|
"description": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC.\n\nOnce set, the field can only be removed when the job is suspended. The field cannot be modified while the job is unsuspended or finished."
|
||||||
},
|
},
|
||||||
"succeeded": {
|
"succeeded": {
|
||||||
"description": "The number of pods which reached phase Succeeded.",
|
"description": "The number of pods which reached phase Succeeded. The value increases monotonically for a given spec. However, it may decrease in reaction to scale down of elastic indexed jobs.",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"terminating": {
|
"terminating": {
|
||||||
"description": "The number of pods which are terminating (in phase Pending or Running and have a deletionTimestamp).\n\nThis field is beta-level. The job controller populates the field when the feature gate JobPodReplacementPolicy is enabled (enabled by default).",
|
"description": "The number of pods which are terminating (in phase Pending or Running and have a deletionTimestamp). The value is zero (or null) for finished jobs.\n\nThis field is beta-level. The job controller populates the field when the feature gate JobPodReplacementPolicy is enabled (enabled by default).",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"uncountedTerminatedPods": {
|
"uncountedTerminatedPods": {
|
||||||
"$ref": "#/definitions/io.k8s.api.batch.v1.UncountedTerminatedPods",
|
"$ref": "#/definitions/io.k8s.api.batch.v1.UncountedTerminatedPods",
|
||||||
"description": "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null."
|
"description": "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null. The structure is empty for finished jobs."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
@@ -344,6 +344,10 @@
|
|||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"managedBy": {
|
||||||
|
"description": "ManagedBy field indicates the controller that manages a Job. The k8s Job controller reconciles jobs which don't have this field at all or the field value is the reserved string `kubernetes.io/job-controller`, but skips reconciling Jobs with a custom value for this field. The value must be a valid domain-prefixed path (e.g. acme.io/foo) - all characters before the first \"/\" must be a valid subdomain as defined by RFC 1123. All characters trailing the first \"/\" must be valid HTTP Path characters as defined by RFC 3986. The value cannot exceed 64 characters.\n\nThis field is alpha-level. The job controller accepts setting the field when the feature gate JobManagedBy is enabled (disabled by default).",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"manualSelector": {
|
"manualSelector": {
|
||||||
"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: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#specifying-your-own-pod-selector",
|
"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: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#specifying-your-own-pod-selector",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -406,7 +410,7 @@
|
|||||||
"description": "JobStatus represents the current state of a Job.",
|
"description": "JobStatus represents the current state of a Job.",
|
||||||
"properties": {
|
"properties": {
|
||||||
"active": {
|
"active": {
|
||||||
"description": "The number of pending and running pods which are not terminating (without a deletionTimestamp).",
|
"description": "The number of pending and running pods which are not terminating (without a deletionTimestamp). The value is zero for finished jobs.",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -420,10 +424,10 @@
|
|||||||
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time"
|
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "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. The completion time is only set when the job finishes successfully."
|
"description": "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. The completion time is set when the job finishes successfully, and only then. The value cannot be updated or removed. The value indicates the same or later point in time as the startTime field."
|
||||||
},
|
},
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"description": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
|
"description": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true.\n\nA job is considered finished when it is in a terminal condition, either \"Complete\" or \"Failed\". At that point, all pods of the job are in terminal phase. Job cannot be both in the \"Complete\" and \"Failed\" conditions. Additionally, it cannot be in the \"Complete\" and \"FailureTarget\" conditions. The \"Complete\", \"Failed\" and \"FailureTarget\" conditions cannot be disabled.\n\nMore info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
|
||||||
"items": {
|
"items": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@@ -438,16 +442,16 @@
|
|||||||
"x-kubernetes-patch-strategy": "merge"
|
"x-kubernetes-patch-strategy": "merge"
|
||||||
},
|
},
|
||||||
"failed": {
|
"failed": {
|
||||||
"description": "The number of pods which reached phase Failed.",
|
"description": "The number of pods which reached phase Failed. The value increases monotonically.",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"failedIndexes": {
|
"failedIndexes": {
|
||||||
"description": "FailedIndexes holds the failed indexes when backoffLimitPerIndex=true. The indexes are represented in the text format analogous as for the `completedIndexes` field, ie. they are kept as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the failed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\". This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` feature gate is enabled (enabled by default).",
|
"description": "FailedIndexes holds the failed indexes when spec.backoffLimitPerIndex is set. The indexes are represented in the text format analogous as for the `completedIndexes` field, ie. they are kept as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the failed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\". The set of failed indexes cannot overlap with the set of completed indexes.\n\nThis field is beta-level. It can be used when the `JobBackoffLimitPerIndex` feature gate is enabled (enabled by default).",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"ready": {
|
"ready": {
|
||||||
"description": "The number of pods which have a Ready condition.",
|
"description": "The number of pods which have a Ready condition. The value is zero (or null) for finished jobs.",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -457,15 +461,15 @@
|
|||||||
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time"
|
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC."
|
"description": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC.\n\nOnce set, the field can only be removed when the job is suspended. The field cannot be modified while the job is unsuspended or finished."
|
||||||
},
|
},
|
||||||
"succeeded": {
|
"succeeded": {
|
||||||
"description": "The number of pods which reached phase Succeeded.",
|
"description": "The number of pods which reached phase Succeeded. The value increases monotonically for a given spec. However, it may decrease in reaction to scale down of elastic indexed jobs.",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"terminating": {
|
"terminating": {
|
||||||
"description": "The number of pods which are terminating (in phase Pending or Running and have a deletionTimestamp).\n\nThis field is beta-level. The job controller populates the field when the feature gate JobPodReplacementPolicy is enabled (enabled by default).",
|
"description": "The number of pods which are terminating (in phase Pending or Running and have a deletionTimestamp). The value is zero (or null) for finished jobs.\n\nThis field is beta-level. The job controller populates the field when the feature gate JobPodReplacementPolicy is enabled (enabled by default).",
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -475,7 +479,7 @@
|
|||||||
"$ref": "#/components/schemas/io.k8s.api.batch.v1.UncountedTerminatedPods"
|
"$ref": "#/components/schemas/io.k8s.api.batch.v1.UncountedTerminatedPods"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null."
|
"description": "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null. The structure is empty for finished jobs."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
@@ -64,6 +64,9 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||||||
podReplacementPolicy = batch.Failed
|
podReplacementPolicy = batch.Failed
|
||||||
}
|
}
|
||||||
j.PodReplacementPolicy = &podReplacementPolicy
|
j.PodReplacementPolicy = &podReplacementPolicy
|
||||||
|
if c.RandBool() {
|
||||||
|
c.Fuzz(j.ManagedBy)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
func(sj *batch.CronJobSpec, c fuzz.Continue) {
|
func(sj *batch.CronJobSpec, c fuzz.Continue) {
|
||||||
c.FuzzNoCustom(sj)
|
c.FuzzNoCustom(sj)
|
||||||
|
@@ -51,6 +51,9 @@ const (
|
|||||||
// to the pod, which don't count towards the backoff limit, according to the
|
// to the pod, which don't count towards the backoff limit, according to the
|
||||||
// pod failure policy. When the annotation is absent zero is implied.
|
// pod failure policy. When the annotation is absent zero is implied.
|
||||||
JobIndexIgnoredFailureCountAnnotation = labelPrefix + "job-index-ignored-failure-count"
|
JobIndexIgnoredFailureCountAnnotation = labelPrefix + "job-index-ignored-failure-count"
|
||||||
|
// JobControllerName reserved value for the managedBy field for the built-in
|
||||||
|
// Job controller.
|
||||||
|
JobControllerName = "kubernetes.io/job-controller"
|
||||||
)
|
)
|
||||||
|
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
@@ -409,6 +412,20 @@ type JobSpec struct {
|
|||||||
// This is on by default.
|
// This is on by default.
|
||||||
// +optional
|
// +optional
|
||||||
PodReplacementPolicy *PodReplacementPolicy
|
PodReplacementPolicy *PodReplacementPolicy
|
||||||
|
|
||||||
|
// ManagedBy field indicates the controller that manages a Job. The k8s Job
|
||||||
|
// controller reconciles jobs which don't have this field at all or the field
|
||||||
|
// value is the reserved string `kubernetes.io/job-controller`, but skips
|
||||||
|
// reconciling Jobs with a custom value for this field.
|
||||||
|
// The value must be a valid domain-prefixed path (e.g. acme.io/foo) -
|
||||||
|
// all characters before the first "/" must be a valid subdomain as defined
|
||||||
|
// by RFC 1123. All characters trailing the first "/" must be valid HTTP Path
|
||||||
|
// characters as defined by RFC 3986. The value cannot exceed 64 characters.
|
||||||
|
//
|
||||||
|
// This field is alpha-level. The job controller accepts setting the field
|
||||||
|
// when the feature gate JobManagedBy is enabled (disabled by default).
|
||||||
|
// +optional
|
||||||
|
ManagedBy *string
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobStatus represents the current state of a Job.
|
// JobStatus represents the current state of a Job.
|
||||||
@@ -420,6 +437,13 @@ type JobStatus struct {
|
|||||||
// status true; when the Job is resumed, the status of this condition will
|
// status true; when the Job is resumed, the status of this condition will
|
||||||
// become false. When a Job is completed, one of the conditions will have
|
// become false. When a Job is completed, one of the conditions will have
|
||||||
// type "Complete" and status true.
|
// type "Complete" and status true.
|
||||||
|
//
|
||||||
|
// A job is considered finished when it is in a terminal condition, either
|
||||||
|
// "Complete" or "Failed". At that point, all pods of the job are in terminal
|
||||||
|
// phase. Job cannot be both in the "Complete" and "Failed" conditions.
|
||||||
|
// Additionally, it cannot be in the "Complete" and "FailureTarget" conditions.
|
||||||
|
// The "Complete", "Failed" and "FailureTarget" conditions cannot be disabled.
|
||||||
|
//
|
||||||
// +optional
|
// +optional
|
||||||
Conditions []JobCondition
|
Conditions []JobCondition
|
||||||
|
|
||||||
@@ -427,23 +451,31 @@ type JobStatus struct {
|
|||||||
// Job is created in the suspended state, this field is not set until the
|
// Job is created in the suspended state, this field is not set until the
|
||||||
// first time it is resumed. This field is reset every time a Job is resumed
|
// first time it is resumed. This field is reset every time a Job is resumed
|
||||||
// from suspension. It is represented in RFC3339 form and is in UTC.
|
// from suspension. It is represented in RFC3339 form and is in UTC.
|
||||||
|
//
|
||||||
|
// Once set, the field can only be removed when the job is suspended.
|
||||||
|
// The field cannot be modified while the job is unsuspended or finished.
|
||||||
|
//
|
||||||
// +optional
|
// +optional
|
||||||
StartTime *metav1.Time
|
StartTime *metav1.Time
|
||||||
|
|
||||||
// Represents time when the job was completed. It is not guaranteed to
|
// Represents time when the job was completed. It is not guaranteed to
|
||||||
// be set in happens-before order across separate operations.
|
// be set in happens-before order across separate operations.
|
||||||
// It is represented in RFC3339 form and is in UTC.
|
// It is represented in RFC3339 form and is in UTC.
|
||||||
// The completion time is only set when the job finishes successfully.
|
// The completion time is set when the job finishes successfully, and only then.
|
||||||
|
// The value cannot be updated or removed. The value indicates the same or
|
||||||
|
// later point in time as the startTime field.
|
||||||
// +optional
|
// +optional
|
||||||
CompletionTime *metav1.Time
|
CompletionTime *metav1.Time
|
||||||
|
|
||||||
// The number of pending and running pods which are not terminating (without
|
// The number of pending and running pods which are not terminating (without
|
||||||
// a deletionTimestamp).
|
// a deletionTimestamp).
|
||||||
|
// The value is zero for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
Active int32
|
Active int32
|
||||||
|
|
||||||
// The number of pods which are terminating (in phase Pending or Running
|
// The number of pods which are terminating (in phase Pending or Running
|
||||||
// and have a deletionTimestamp).
|
// and have a deletionTimestamp).
|
||||||
|
// The value is zero (or null) for finished jobs.
|
||||||
//
|
//
|
||||||
// This field is beta-level. The job controller populates the field when
|
// This field is beta-level. The job controller populates the field when
|
||||||
// the feature gate JobPodReplacementPolicy is enabled (enabled by default).
|
// the feature gate JobPodReplacementPolicy is enabled (enabled by default).
|
||||||
@@ -451,14 +483,18 @@ type JobStatus struct {
|
|||||||
Terminating *int32
|
Terminating *int32
|
||||||
|
|
||||||
// The number of active pods which have a Ready condition.
|
// The number of active pods which have a Ready condition.
|
||||||
|
// The value is zero (or null) for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
Ready *int32
|
Ready *int32
|
||||||
|
|
||||||
// The number of pods which reached phase Succeeded.
|
// The number of pods which reached phase Succeeded.
|
||||||
|
// The value increases monotonically for a given spec. However, it may
|
||||||
|
// decrease in reaction to scale down of elastic indexed jobs.
|
||||||
// +optional
|
// +optional
|
||||||
Succeeded int32
|
Succeeded int32
|
||||||
|
|
||||||
// The number of pods which reached phase Failed.
|
// The number of pods which reached phase Failed.
|
||||||
|
// The value increases monotonically.
|
||||||
// +optional
|
// +optional
|
||||||
Failed int32
|
Failed int32
|
||||||
|
|
||||||
@@ -472,7 +508,7 @@ type JobStatus struct {
|
|||||||
// +optional
|
// +optional
|
||||||
CompletedIndexes string
|
CompletedIndexes string
|
||||||
|
|
||||||
// FailedIndexes holds the failed indexes when backoffLimitPerIndex=true.
|
// FailedIndexes holds the failed indexes when spec.backoffLimitPerIndex is set.
|
||||||
// The indexes are represented in the text format analogous as for the
|
// The indexes are represented in the text format analogous as for the
|
||||||
// `completedIndexes` field, ie. they are kept as decimal integers
|
// `completedIndexes` field, ie. they are kept as decimal integers
|
||||||
// separated by commas. The numbers are listed in increasing order. Three or
|
// separated by commas. The numbers are listed in increasing order. Three or
|
||||||
@@ -480,6 +516,8 @@ type JobStatus struct {
|
|||||||
// last element of the series, separated by a hyphen.
|
// last element of the series, separated by a hyphen.
|
||||||
// For example, if the failed indexes are 1, 3, 4, 5 and 7, they are
|
// For example, if the failed indexes are 1, 3, 4, 5 and 7, they are
|
||||||
// represented as "1,3-5,7".
|
// represented as "1,3-5,7".
|
||||||
|
// The set of failed indexes cannot overlap with the set of completed indexes.
|
||||||
|
//
|
||||||
// This field is beta-level. It can be used when the `JobBackoffLimitPerIndex`
|
// This field is beta-level. It can be used when the `JobBackoffLimitPerIndex`
|
||||||
// feature gate is enabled (enabled by default).
|
// feature gate is enabled (enabled by default).
|
||||||
// +optional
|
// +optional
|
||||||
@@ -499,6 +537,7 @@ type JobStatus struct {
|
|||||||
//
|
//
|
||||||
// Old jobs might not be tracked using this field, in which case the field
|
// Old jobs might not be tracked using this field, in which case the field
|
||||||
// remains null.
|
// remains null.
|
||||||
|
// The structure is empty for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
UncountedTerminatedPods *UncountedTerminatedPods
|
UncountedTerminatedPods *UncountedTerminatedPods
|
||||||
}
|
}
|
||||||
|
2
pkg/apis/batch/v1/zz_generated.conversion.go
generated
2
pkg/apis/batch/v1/zz_generated.conversion.go
generated
@@ -452,6 +452,7 @@ func autoConvert_v1_JobSpec_To_batch_JobSpec(in *v1.JobSpec, out *batch.JobSpec,
|
|||||||
out.CompletionMode = (*batch.CompletionMode)(unsafe.Pointer(in.CompletionMode))
|
out.CompletionMode = (*batch.CompletionMode)(unsafe.Pointer(in.CompletionMode))
|
||||||
out.Suspend = (*bool)(unsafe.Pointer(in.Suspend))
|
out.Suspend = (*bool)(unsafe.Pointer(in.Suspend))
|
||||||
out.PodReplacementPolicy = (*batch.PodReplacementPolicy)(unsafe.Pointer(in.PodReplacementPolicy))
|
out.PodReplacementPolicy = (*batch.PodReplacementPolicy)(unsafe.Pointer(in.PodReplacementPolicy))
|
||||||
|
out.ManagedBy = (*string)(unsafe.Pointer(in.ManagedBy))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +473,7 @@ func autoConvert_batch_JobSpec_To_v1_JobSpec(in *batch.JobSpec, out *v1.JobSpec,
|
|||||||
out.CompletionMode = (*v1.CompletionMode)(unsafe.Pointer(in.CompletionMode))
|
out.CompletionMode = (*v1.CompletionMode)(unsafe.Pointer(in.CompletionMode))
|
||||||
out.Suspend = (*bool)(unsafe.Pointer(in.Suspend))
|
out.Suspend = (*bool)(unsafe.Pointer(in.Suspend))
|
||||||
out.PodReplacementPolicy = (*v1.PodReplacementPolicy)(unsafe.Pointer(in.PodReplacementPolicy))
|
out.PodReplacementPolicy = (*v1.PodReplacementPolicy)(unsafe.Pointer(in.PodReplacementPolicy))
|
||||||
|
out.ManagedBy = (*string)(unsafe.Pointer(in.ManagedBy))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ package validation
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ import (
|
|||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxParallelismForIndexJob is the maximum parallelism that an Indexed Job
|
// maxParallelismForIndexJob is the maximum parallelism that an Indexed Job
|
||||||
@@ -61,6 +63,9 @@ const (
|
|||||||
|
|
||||||
// maximum number of patterns for a OnPodConditions requirement in pod failure policy
|
// maximum number of patterns for a OnPodConditions requirement in pod failure policy
|
||||||
maxPodFailurePolicyOnPodConditionsPatterns = 20
|
maxPodFailurePolicyOnPodConditionsPatterns = 20
|
||||||
|
|
||||||
|
// maximum length of the value of the managedBy field
|
||||||
|
maxManagedByLength = 63
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -206,6 +211,12 @@ func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path, opts apivalidatio
|
|||||||
allErrs = append(allErrs, field.Required(fldPath.Child("backoffLimitPerIndex"), fmt.Sprintf("when maxFailedIndexes is specified")))
|
allErrs = append(allErrs, field.Required(fldPath.Child("backoffLimitPerIndex"), fmt.Sprintf("when maxFailedIndexes is specified")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if spec.ManagedBy != nil {
|
||||||
|
allErrs = append(allErrs, apimachineryvalidation.IsDomainPrefixedPath(fldPath.Child("managedBy"), *spec.ManagedBy)...)
|
||||||
|
if len(*spec.ManagedBy) > maxManagedByLength {
|
||||||
|
allErrs = append(allErrs, field.TooLongMaxLength(fldPath.Child("managedBy"), *spec.ManagedBy, maxManagedByLength))
|
||||||
|
}
|
||||||
|
}
|
||||||
if spec.CompletionMode != nil {
|
if spec.CompletionMode != nil {
|
||||||
if *spec.CompletionMode != batch.NonIndexedCompletion && *spec.CompletionMode != batch.IndexedCompletion {
|
if *spec.CompletionMode != batch.NonIndexedCompletion && *spec.CompletionMode != batch.IndexedCompletion {
|
||||||
allErrs = append(allErrs, field.NotSupported(fldPath.Child("completionMode"), spec.CompletionMode, []batch.CompletionMode{batch.NonIndexedCompletion, batch.IndexedCompletion}))
|
allErrs = append(allErrs, field.NotSupported(fldPath.Child("completionMode"), spec.CompletionMode, []batch.CompletionMode{batch.NonIndexedCompletion, batch.IndexedCompletion}))
|
||||||
@@ -390,8 +401,9 @@ func validatePodFailurePolicyRuleOnExitCodes(onExitCode *batch.PodFailurePolicyO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateJobStatus validates a JobStatus and returns an ErrorList with any errors.
|
// validateJobStatus validates a JobStatus and returns an ErrorList with any errors.
|
||||||
func validateJobStatus(status *batch.JobStatus, fldPath *field.Path) field.ErrorList {
|
func validateJobStatus(job *batch.Job, fldPath *field.Path, opts JobStatusValidationOptions) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
status := job.Status
|
||||||
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Active), fldPath.Child("active"))...)
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Active), fldPath.Child("active"))...)
|
||||||
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Succeeded), fldPath.Child("succeeded"))...)
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Succeeded), fldPath.Child("succeeded"))...)
|
||||||
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Failed), fldPath.Child("failed"))...)
|
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Failed), fldPath.Child("failed"))...)
|
||||||
@@ -425,6 +437,91 @@ func validateJobStatus(status *batch.JobStatus, fldPath *field.Path) field.Error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if opts.RejectCompleteJobWithFailedCondition {
|
||||||
|
if IsJobComplete(job) && IsJobFailed(job) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set Complete=True and Failed=true conditions"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectCompleteJobWithFailureTargetCondition {
|
||||||
|
if IsJobComplete(job) && IsConditionTrue(status.Conditions, batch.JobFailureTarget) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), field.OmitValueType{}, "cannot set Complete=True and FailureTarget=true conditions"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectNotCompleteJobWithCompletionTime {
|
||||||
|
if status.CompletionTime != nil && !IsJobComplete(job) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("completionTime"), status.CompletionTime, "cannot set completionTime when there is no Complete=True condition"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectCompleteJobWithoutCompletionTime {
|
||||||
|
if status.CompletionTime == nil && IsJobComplete(job) {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("completionTime"), "completionTime is required for Complete jobs"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectCompletionTimeBeforeStartTime {
|
||||||
|
if status.StartTime != nil && status.CompletionTime != nil && status.CompletionTime.Before(status.StartTime) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("completionTime"), status.CompletionTime, "completionTime cannot be set before startTime"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isJobFinished := IsJobFinished(job)
|
||||||
|
if opts.RejectFinishedJobWithActivePods {
|
||||||
|
if status.Active > 0 && isJobFinished {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("active"), status.Active, "active>0 is invalid for finished job"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectFinishedJobWithTerminatingPods {
|
||||||
|
if status.Terminating != nil && *status.Terminating > 0 && isJobFinished {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("terminating"), status.Terminating, "terminating>0 is invalid for finished job"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectFinishedJobWithoutStartTime {
|
||||||
|
if status.StartTime == nil && isJobFinished {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("startTime"), "startTime is required for finished job"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectFinishedJobWithUncountedTerminatedPods {
|
||||||
|
if isJobFinished && status.UncountedTerminatedPods != nil && len(status.UncountedTerminatedPods.Failed)+len(status.UncountedTerminatedPods.Succeeded) > 0 {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("uncountedTerminatedPods"), status.UncountedTerminatedPods, "uncountedTerminatedPods needs to be empty for finished job"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectInvalidCompletedIndexes {
|
||||||
|
if job.Spec.Completions != nil {
|
||||||
|
if err := validateIndexesFormat(status.CompletedIndexes, int32(*job.Spec.Completions)); err != nil {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("completedIndexes"), status.CompletedIndexes, fmt.Sprintf("error parsing completedIndexes: %s", err.Error())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectInvalidFailedIndexes {
|
||||||
|
if job.Spec.Completions != nil && job.Spec.BackoffLimitPerIndex != nil && status.FailedIndexes != nil {
|
||||||
|
if err := validateIndexesFormat(*status.FailedIndexes, int32(*job.Spec.Completions)); err != nil {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("failedIndexes"), status.FailedIndexes, fmt.Sprintf("error parsing failedIndexes: %s", err.Error())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isIndexed := ptr.Deref(job.Spec.CompletionMode, batch.NonIndexedCompletion) == batch.IndexedCompletion
|
||||||
|
if opts.RejectCompletedIndexesForNonIndexedJob {
|
||||||
|
if len(status.CompletedIndexes) != 0 && !isIndexed {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("completedIndexes"), status.CompletedIndexes, "cannot set non-empty completedIndexes when non-indexed completion mode"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectFailedIndexesForNoBackoffLimitPerIndex {
|
||||||
|
// Note that this check also verifies that FailedIndexes are not used for
|
||||||
|
// regular (non-indexed) jobs, because regular jobs have backoffLimitPerIndex = nil.
|
||||||
|
if job.Spec.BackoffLimitPerIndex == nil && status.FailedIndexes != nil {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("failedIndexes"), *status.FailedIndexes, "cannot set non-null failedIndexes when backoffLimitPerIndex is null"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectMoreReadyThanActivePods {
|
||||||
|
if status.Ready != nil && *status.Ready > status.Active {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("ready"), *status.Ready, "cannot set more ready pods than active"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectFailedIndexesOverlappingCompleted {
|
||||||
|
if job.Spec.Completions != nil && status.FailedIndexes != nil {
|
||||||
|
if err := validateFailedIndexesNotOverlapCompleted(status.CompletedIndexes, *status.FailedIndexes, int32(*job.Spec.Completions)); err != nil {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("failedIndexes"), *status.FailedIndexes, err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,9 +533,9 @@ func ValidateJobUpdate(job, oldJob *batch.Job, opts JobValidationOptions) field.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateJobUpdateStatus validates an update to the status of a Job and returns an ErrorList with any errors.
|
// ValidateJobUpdateStatus validates an update to the status of a Job and returns an ErrorList with any errors.
|
||||||
func ValidateJobUpdateStatus(job, oldJob *batch.Job) field.ErrorList {
|
func ValidateJobUpdateStatus(job, oldJob *batch.Job, opts JobStatusValidationOptions) field.ErrorList {
|
||||||
allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
|
allErrs := apivalidation.ValidateObjectMetaUpdate(&job.ObjectMeta, &oldJob.ObjectMeta, field.NewPath("metadata"))
|
||||||
allErrs = append(allErrs, ValidateJobStatusUpdate(job.Status, oldJob.Status)...)
|
allErrs = append(allErrs, ValidateJobStatusUpdate(job, oldJob, opts)...)
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +549,7 @@ func ValidateJobSpecUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path, opt
|
|||||||
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.CompletionMode, oldSpec.CompletionMode, fldPath.Child("completionMode"))...)
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.CompletionMode, oldSpec.CompletionMode, fldPath.Child("completionMode"))...)
|
||||||
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.PodFailurePolicy, oldSpec.PodFailurePolicy, fldPath.Child("podFailurePolicy"))...)
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.PodFailurePolicy, oldSpec.PodFailurePolicy, fldPath.Child("podFailurePolicy"))...)
|
||||||
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.BackoffLimitPerIndex, oldSpec.BackoffLimitPerIndex, fldPath.Child("backoffLimitPerIndex"))...)
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.BackoffLimitPerIndex, oldSpec.BackoffLimitPerIndex, fldPath.Child("backoffLimitPerIndex"))...)
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(spec.ManagedBy, oldSpec.ManagedBy, fldPath.Child("managedBy"))...)
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,9 +584,43 @@ func validatePodTemplateUpdate(spec, oldSpec batch.JobSpec, fldPath *field.Path,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateJobStatusUpdate validates an update to a JobStatus and returns an ErrorList with any errors.
|
// ValidateJobStatusUpdate validates an update to a JobStatus and returns an ErrorList with any errors.
|
||||||
func ValidateJobStatusUpdate(status, oldStatus batch.JobStatus) field.ErrorList {
|
func ValidateJobStatusUpdate(job, oldJob *batch.Job, opts JobStatusValidationOptions) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
allErrs = append(allErrs, validateJobStatus(&status, field.NewPath("status"))...)
|
statusFld := field.NewPath("status")
|
||||||
|
allErrs = append(allErrs, validateJobStatus(job, statusFld, opts)...)
|
||||||
|
|
||||||
|
if opts.RejectDisablingTerminalCondition {
|
||||||
|
for _, cType := range []batch.JobConditionType{batch.JobFailed, batch.JobComplete, batch.JobFailureTarget} {
|
||||||
|
if IsConditionTrue(oldJob.Status.Conditions, cType) && !IsConditionTrue(job.Status.Conditions, cType) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(statusFld.Child("conditions"), field.OmitValueType{}, fmt.Sprintf("cannot disable the terminal %s=True condition", string(cType))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectDecreasingFailedCounter {
|
||||||
|
if job.Status.Failed < oldJob.Status.Failed {
|
||||||
|
allErrs = append(allErrs, field.Invalid(statusFld.Child("failed"), job.Status.Failed, "cannot decrease the failed counter"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectDecreasingSucceededCounter {
|
||||||
|
if job.Status.Succeeded < oldJob.Status.Succeeded {
|
||||||
|
allErrs = append(allErrs, field.Invalid(statusFld.Child("succeeded"), job.Status.Succeeded, "cannot decrease the succeeded counter"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectMutatingCompletionTime {
|
||||||
|
// Note that we check the condition only when `job.Status.CompletionTime != nil`, this is because
|
||||||
|
// we don't want to block transitions to completionTime = nil when the job is not finished yet.
|
||||||
|
// Setting completionTime = nil for finished jobs is prevented in RejectCompleteJobWithoutCompletionTime.
|
||||||
|
if job.Status.CompletionTime != nil && oldJob.Status.CompletionTime != nil && !ptr.Equal(job.Status.CompletionTime, oldJob.Status.CompletionTime) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(statusFld.Child("completionTime"), job.Status.CompletionTime, "completionTime cannot be mutated"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.RejectStartTimeUpdateForUnsuspendedJob {
|
||||||
|
// Note that we check `oldJob.Status.StartTime != nil` to allow transitioning from
|
||||||
|
// startTime = nil to startTime != nil for unsuspended jobs, which is a desired transition.
|
||||||
|
if oldJob.Status.StartTime != nil && !ptr.Equal(oldJob.Status.StartTime, job.Status.StartTime) && !ptr.Deref(job.Spec.Suspend, false) {
|
||||||
|
allErrs = append(allErrs, field.Required(statusFld.Child("startTime"), "startTime cannot be removed for unsuspended job"))
|
||||||
|
}
|
||||||
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,6 +798,124 @@ func validateCompletions(spec, oldSpec batch.JobSpec, fldPath *field.Path, opts
|
|||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsJobFinished(job *batch.Job) bool {
|
||||||
|
for _, c := range job.Status.Conditions {
|
||||||
|
if (c.Type == batch.JobComplete || c.Type == batch.JobFailed) && c.Status == api.ConditionTrue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsJobComplete(job *batch.Job) bool {
|
||||||
|
return IsConditionTrue(job.Status.Conditions, batch.JobComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsJobFailed(job *batch.Job) bool {
|
||||||
|
return IsConditionTrue(job.Status.Conditions, batch.JobFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsConditionTrue(list []batch.JobCondition, cType batch.JobConditionType) bool {
|
||||||
|
for _, c := range list {
|
||||||
|
if c.Type == cType && c.Status == api.ConditionTrue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFailedIndexesNotOverlapCompleted(completedIndexesStr string, failedIndexesStr string, completions int32) error {
|
||||||
|
if len(completedIndexesStr) == 0 || len(failedIndexesStr) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
completedIndexesIntervals := strings.Split(completedIndexesStr, ",")
|
||||||
|
failedIndexesIntervals := strings.Split(failedIndexesStr, ",")
|
||||||
|
var completedPos, failedPos int
|
||||||
|
cX, cY, cErr := parseIndexInterval(completedIndexesIntervals[completedPos], completions)
|
||||||
|
fX, fY, fErr := parseIndexInterval(failedIndexesIntervals[failedPos], completions)
|
||||||
|
for completedPos < len(completedIndexesIntervals) && failedPos < len(failedIndexesIntervals) {
|
||||||
|
if cErr != nil {
|
||||||
|
// Failure to parse "completed" interval. We go to the next interval,
|
||||||
|
// the error will be reported to the user when validating the format.
|
||||||
|
completedPos++
|
||||||
|
if completedPos < len(completedIndexesIntervals) {
|
||||||
|
cX, cY, cErr = parseIndexInterval(completedIndexesIntervals[completedPos], completions)
|
||||||
|
}
|
||||||
|
} else if fErr != nil {
|
||||||
|
// Failure to parse "failed" interval. We go to the next interval,
|
||||||
|
// the error will be reported to the user when validating the format.
|
||||||
|
failedPos++
|
||||||
|
if failedPos < len(failedIndexesIntervals) {
|
||||||
|
fX, fY, fErr = parseIndexInterval(failedIndexesIntervals[failedPos], completions)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We have one failed and one completed interval parsed.
|
||||||
|
if cX <= fY && fX <= cY {
|
||||||
|
return fmt.Errorf("failedIndexes and completedIndexes overlap at index: %d", max(cX, fX))
|
||||||
|
}
|
||||||
|
// No overlap, let's move to the next one.
|
||||||
|
if cX <= fX {
|
||||||
|
completedPos++
|
||||||
|
if completedPos < len(completedIndexesIntervals) {
|
||||||
|
cX, cY, cErr = parseIndexInterval(completedIndexesIntervals[completedPos], completions)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failedPos++
|
||||||
|
if failedPos < len(failedIndexesIntervals) {
|
||||||
|
fX, fY, fErr = parseIndexInterval(failedIndexesIntervals[failedPos], completions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateIndexesFormat(indexesStr string, completions int32) error {
|
||||||
|
if len(indexesStr) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var lastIndex *int32
|
||||||
|
for _, intervalStr := range strings.Split(indexesStr, ",") {
|
||||||
|
x, y, err := parseIndexInterval(intervalStr, completions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if lastIndex != nil && *lastIndex >= x {
|
||||||
|
return fmt.Errorf("non-increasing order, previous: %d, current: %d", *lastIndex, x)
|
||||||
|
}
|
||||||
|
lastIndex = &y
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIndexInterval(intervalStr string, completions int32) (int32, int32, error) {
|
||||||
|
limitsStr := strings.Split(intervalStr, "-")
|
||||||
|
if len(limitsStr) > 2 {
|
||||||
|
return 0, 0, fmt.Errorf("the fragment %q violates the requirement that an index interval can have at most two parts separated by '-'", intervalStr)
|
||||||
|
}
|
||||||
|
x, err := strconv.Atoi(limitsStr[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("cannot convert string to integer for index: %q", limitsStr[0])
|
||||||
|
}
|
||||||
|
if x >= int(completions) {
|
||||||
|
return 0, 0, fmt.Errorf("too large index: %q", limitsStr[0])
|
||||||
|
}
|
||||||
|
if len(limitsStr) > 1 {
|
||||||
|
y, err := strconv.Atoi(limitsStr[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("cannot convert string to integer for index: %q", limitsStr[1])
|
||||||
|
}
|
||||||
|
if y >= int(completions) {
|
||||||
|
return 0, 0, fmt.Errorf("too large index: %q", limitsStr[1])
|
||||||
|
}
|
||||||
|
if x >= y {
|
||||||
|
return 0, 0, fmt.Errorf("non-increasing order, previous: %d, current: %d", x, y)
|
||||||
|
}
|
||||||
|
return int32(x), int32(y), nil
|
||||||
|
}
|
||||||
|
return int32(x), int32(x), nil
|
||||||
|
}
|
||||||
|
|
||||||
type JobValidationOptions struct {
|
type JobValidationOptions struct {
|
||||||
apivalidation.PodValidationOptions
|
apivalidation.PodValidationOptions
|
||||||
// Allow mutable node affinity, selector and tolerations of the template
|
// Allow mutable node affinity, selector and tolerations of the template
|
||||||
@@ -675,3 +925,26 @@ type JobValidationOptions struct {
|
|||||||
// Require Job to have the label on batch.kubernetes.io/job-name and batch.kubernetes.io/controller-uid
|
// Require Job to have the label on batch.kubernetes.io/job-name and batch.kubernetes.io/controller-uid
|
||||||
RequirePrefixedLabels bool
|
RequirePrefixedLabels bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JobStatusValidationOptions struct {
|
||||||
|
RejectDecreasingSucceededCounter bool
|
||||||
|
RejectDecreasingFailedCounter bool
|
||||||
|
RejectDisablingTerminalCondition bool
|
||||||
|
RejectInvalidCompletedIndexes bool
|
||||||
|
RejectInvalidFailedIndexes bool
|
||||||
|
RejectFailedIndexesOverlappingCompleted bool
|
||||||
|
RejectCompletedIndexesForNonIndexedJob bool
|
||||||
|
RejectFailedIndexesForNoBackoffLimitPerIndex bool
|
||||||
|
RejectMoreReadyThanActivePods bool
|
||||||
|
RejectFinishedJobWithActivePods bool
|
||||||
|
RejectFinishedJobWithTerminatingPods bool
|
||||||
|
RejectFinishedJobWithoutStartTime bool
|
||||||
|
RejectFinishedJobWithUncountedTerminatedPods bool
|
||||||
|
RejectStartTimeUpdateForUnsuspendedJob bool
|
||||||
|
RejectCompletionTimeBeforeStartTime bool
|
||||||
|
RejectMutatingCompletionTime bool
|
||||||
|
RejectCompleteJobWithoutCompletionTime bool
|
||||||
|
RejectNotCompleteJobWithCompletionTime bool
|
||||||
|
RejectCompleteJobWithFailedCondition bool
|
||||||
|
RejectCompleteJobWithFailureTargetCondition bool
|
||||||
|
}
|
||||||
|
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
_ "time/tzdata"
|
_ "time/tzdata"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -33,6 +34,7 @@ import (
|
|||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -380,6 +382,17 @@ func TestValidateJob(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"valid managedBy field": {
|
||||||
|
opts: JobValidationOptions{RequirePrefixedLabels: true},
|
||||||
|
job: batch.Job{
|
||||||
|
ObjectMeta: validJobObjectMeta,
|
||||||
|
Spec: batch.JobSpec{
|
||||||
|
Selector: validGeneratedSelector,
|
||||||
|
Template: validPodTemplateSpecForGenerated,
|
||||||
|
ManagedBy: ptr.To("example.com/foo"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for k, v := range successCases {
|
for k, v := range successCases {
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
@@ -394,6 +407,28 @@ func TestValidateJob(t *testing.T) {
|
|||||||
opts JobValidationOptions
|
opts JobValidationOptions
|
||||||
job batch.Job
|
job batch.Job
|
||||||
}{
|
}{
|
||||||
|
`spec.managedBy: Too long: may not be longer than 63`: {
|
||||||
|
opts: JobValidationOptions{RequirePrefixedLabels: true},
|
||||||
|
job: batch.Job{
|
||||||
|
ObjectMeta: validJobObjectMeta,
|
||||||
|
Spec: batch.JobSpec{
|
||||||
|
Selector: validGeneratedSelector,
|
||||||
|
Template: validPodTemplateSpecForGenerated,
|
||||||
|
ManagedBy: ptr.To("example.com/" + strings.Repeat("x", 60)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`spec.managedBy: Invalid value: "invalid custom controller name": must be a domain-prefixed path (such as "acme.io/foo")`: {
|
||||||
|
opts: JobValidationOptions{RequirePrefixedLabels: true},
|
||||||
|
job: batch.Job{
|
||||||
|
ObjectMeta: validJobObjectMeta,
|
||||||
|
Spec: batch.JobSpec{
|
||||||
|
Selector: validGeneratedSelector,
|
||||||
|
Template: validPodTemplateSpecForGenerated,
|
||||||
|
ManagedBy: ptr.To("invalid custom controller name"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
`spec.podFailurePolicy.rules[0]: Invalid value: specifying one of OnExitCodes and OnPodConditions is required`: {
|
`spec.podFailurePolicy.rules[0]: Invalid value: specifying one of OnExitCodes and OnPodConditions is required`: {
|
||||||
job: batch.Job{
|
job: batch.Job{
|
||||||
ObjectMeta: validJobObjectMeta,
|
ObjectMeta: validJobObjectMeta,
|
||||||
@@ -1349,6 +1384,39 @@ func TestValidateJobUpdate(t *testing.T) {
|
|||||||
job.Spec.ManualSelector = pointer.Bool(true)
|
job.Spec.ManualSelector = pointer.Bool(true)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"invalid attempt to set managedBy field": {
|
||||||
|
old: batch.Job{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||||
|
Spec: batch.JobSpec{
|
||||||
|
Selector: validGeneratedSelector,
|
||||||
|
Template: validPodTemplateSpecForGenerated,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: func(job *batch.Job) {
|
||||||
|
job.Spec.ManagedBy = ptr.To("example.com/custom-controller")
|
||||||
|
},
|
||||||
|
err: &field.Error{
|
||||||
|
Type: field.ErrorTypeInvalid,
|
||||||
|
Field: "spec.managedBy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid update of the managedBy field": {
|
||||||
|
old: batch.Job{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||||
|
Spec: batch.JobSpec{
|
||||||
|
Selector: validGeneratedSelector,
|
||||||
|
Template: validPodTemplateSpecForGenerated,
|
||||||
|
ManagedBy: ptr.To("example.com/custom-controller1"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: func(job *batch.Job) {
|
||||||
|
job.Spec.ManagedBy = ptr.To("example.com/custom-controller2")
|
||||||
|
},
|
||||||
|
err: &field.Error{
|
||||||
|
Type: field.ErrorTypeInvalid,
|
||||||
|
Field: "spec.managedBy",
|
||||||
|
},
|
||||||
|
},
|
||||||
"immutable completions for non-indexed jobs": {
|
"immutable completions for non-indexed jobs": {
|
||||||
old: batch.Job{
|
old: batch.Job{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||||
@@ -2014,6 +2082,8 @@ func TestValidateJobUpdate(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateJobUpdateStatus(t *testing.T) {
|
func TestValidateJobUpdateStatus(t *testing.T) {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
|
opts JobStatusValidationOptions
|
||||||
|
|
||||||
old batch.Job
|
old batch.Job
|
||||||
update batch.Job
|
update batch.Job
|
||||||
wantErrs field.ErrorList
|
wantErrs field.ErrorList
|
||||||
@@ -2141,7 +2211,7 @@ func TestValidateJobUpdateStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for name, tc := range cases {
|
for name, tc := range cases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
errs := ValidateJobUpdateStatus(&tc.update, &tc.old)
|
errs := ValidateJobUpdateStatus(&tc.update, &tc.old, tc.opts)
|
||||||
if diff := cmp.Diff(tc.wantErrs, errs, ignoreErrValueDetail); diff != "" {
|
if diff := cmp.Diff(tc.wantErrs, errs, ignoreErrValueDetail); diff != "" {
|
||||||
t.Errorf("Unexpected errors (-want,+got):\n%s", diff)
|
t.Errorf("Unexpected errors (-want,+got):\n%s", diff)
|
||||||
}
|
}
|
||||||
@@ -3587,3 +3657,161 @@ func TestTimeZones(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateIndexesString(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
indexesString string
|
||||||
|
completions int32
|
||||||
|
wantError error
|
||||||
|
}{
|
||||||
|
"empty is valid": {
|
||||||
|
indexesString: "",
|
||||||
|
completions: 6,
|
||||||
|
},
|
||||||
|
"single number is valid": {
|
||||||
|
indexesString: "1",
|
||||||
|
completions: 6,
|
||||||
|
},
|
||||||
|
"single interval is valid": {
|
||||||
|
indexesString: "1-3",
|
||||||
|
completions: 6,
|
||||||
|
},
|
||||||
|
"mixed intervals valid": {
|
||||||
|
indexesString: "0,1-3,5,7-10",
|
||||||
|
completions: 12,
|
||||||
|
},
|
||||||
|
"invalid due to extra space": {
|
||||||
|
indexesString: "0,1-3, 5",
|
||||||
|
completions: 6,
|
||||||
|
wantError: errors.New(`cannot convert string to integer for index: " 5"`),
|
||||||
|
},
|
||||||
|
"invalid due to too large index": {
|
||||||
|
indexesString: "0,1-3,5",
|
||||||
|
completions: 5,
|
||||||
|
wantError: errors.New(`too large index: "5"`),
|
||||||
|
},
|
||||||
|
"invalid due to non-increasing order of intervals": {
|
||||||
|
indexesString: "1-3,0,5",
|
||||||
|
completions: 6,
|
||||||
|
wantError: errors.New(`non-increasing order, previous: 3, current: 0`),
|
||||||
|
},
|
||||||
|
"invalid due to non-increasing order between intervals": {
|
||||||
|
indexesString: "0,0,5",
|
||||||
|
completions: 6,
|
||||||
|
wantError: errors.New(`non-increasing order, previous: 0, current: 0`),
|
||||||
|
},
|
||||||
|
"invalid due to non-increasing order within interval": {
|
||||||
|
indexesString: "0,1-1,5",
|
||||||
|
completions: 6,
|
||||||
|
wantError: errors.New(`non-increasing order, previous: 1, current: 1`),
|
||||||
|
},
|
||||||
|
"invalid due to starting with '-'": {
|
||||||
|
indexesString: "-1,0",
|
||||||
|
completions: 6,
|
||||||
|
wantError: errors.New(`cannot convert string to integer for index: ""`),
|
||||||
|
},
|
||||||
|
"invalid due to ending with '-'": {
|
||||||
|
indexesString: "0,1-",
|
||||||
|
completions: 6,
|
||||||
|
wantError: errors.New(`cannot convert string to integer for index: ""`),
|
||||||
|
},
|
||||||
|
"invalid due to repeated '-'": {
|
||||||
|
indexesString: "0,1--3",
|
||||||
|
completions: 6,
|
||||||
|
wantError: errors.New(`the fragment "1--3" violates the requirement that an index interval can have at most two parts separated by '-'`),
|
||||||
|
},
|
||||||
|
"invalid due to repeated ','": {
|
||||||
|
indexesString: "0,,1,3",
|
||||||
|
completions: 6,
|
||||||
|
wantError: errors.New(`cannot convert string to integer for index: ""`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
gotErr := validateIndexesFormat(tc.indexesString, tc.completions)
|
||||||
|
if tc.wantError == nil && gotErr != nil {
|
||||||
|
t.Errorf("unexpected error: %s", gotErr)
|
||||||
|
} else if tc.wantError != nil && gotErr == nil {
|
||||||
|
t.Errorf("missing error: %s", tc.wantError)
|
||||||
|
} else if tc.wantError != nil && gotErr != nil {
|
||||||
|
if diff := cmp.Diff(tc.wantError.Error(), gotErr.Error()); diff != "" {
|
||||||
|
t.Errorf("unexpected error, diff: %s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateFailedIndexesNotOverlapCompleted(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
completedIndexesStr string
|
||||||
|
failedIndexesStr string
|
||||||
|
completions int32
|
||||||
|
wantError error
|
||||||
|
}{
|
||||||
|
"empty intervals": {
|
||||||
|
completedIndexesStr: "",
|
||||||
|
failedIndexesStr: "",
|
||||||
|
completions: 6,
|
||||||
|
},
|
||||||
|
"empty completed intervals": {
|
||||||
|
completedIndexesStr: "",
|
||||||
|
failedIndexesStr: "1-3",
|
||||||
|
completions: 6,
|
||||||
|
},
|
||||||
|
"empty failed intervals": {
|
||||||
|
completedIndexesStr: "1-2",
|
||||||
|
failedIndexesStr: "",
|
||||||
|
completions: 6,
|
||||||
|
},
|
||||||
|
"non-overlapping intervals": {
|
||||||
|
completedIndexesStr: "0,2-4,6-8,12-19",
|
||||||
|
failedIndexesStr: "1,9-10",
|
||||||
|
completions: 20,
|
||||||
|
},
|
||||||
|
"overlapping intervals": {
|
||||||
|
completedIndexesStr: "0,2-4,6-8,12-19",
|
||||||
|
failedIndexesStr: "1,8,9-10",
|
||||||
|
completions: 20,
|
||||||
|
wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"),
|
||||||
|
},
|
||||||
|
"overlapping intervals, corrupted completed interval skipped": {
|
||||||
|
completedIndexesStr: "0,2-4,x,6-8,12-19",
|
||||||
|
failedIndexesStr: "1,8,9-10",
|
||||||
|
completions: 20,
|
||||||
|
wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"),
|
||||||
|
},
|
||||||
|
"overlapping intervals, corrupted failed interval skipped": {
|
||||||
|
completedIndexesStr: "0,2-4,6-8,12-19",
|
||||||
|
failedIndexesStr: "1,y,8,9-10",
|
||||||
|
completions: 20,
|
||||||
|
wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"),
|
||||||
|
},
|
||||||
|
"overlapping intervals, first corrupted intervals skipped": {
|
||||||
|
completedIndexesStr: "x,0,2-4,6-8,12-19",
|
||||||
|
failedIndexesStr: "y,1,8,9-10",
|
||||||
|
completions: 20,
|
||||||
|
wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"),
|
||||||
|
},
|
||||||
|
"non-overlapping intervals, last intervals corrupted": {
|
||||||
|
completedIndexesStr: "0,2-4,6-8,12-19,x",
|
||||||
|
failedIndexesStr: "1,9-10,y",
|
||||||
|
completions: 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
gotErr := validateFailedIndexesNotOverlapCompleted(tc.completedIndexesStr, tc.failedIndexesStr, tc.completions)
|
||||||
|
if tc.wantError == nil && gotErr != nil {
|
||||||
|
t.Errorf("unexpected error: %s", gotErr)
|
||||||
|
} else if tc.wantError != nil && gotErr == nil {
|
||||||
|
t.Errorf("missing error: %s", tc.wantError)
|
||||||
|
} else if tc.wantError != nil && gotErr != nil {
|
||||||
|
if diff := cmp.Diff(tc.wantError.Error(), gotErr.Error()); diff != "" {
|
||||||
|
t.Errorf("unexpected error, diff: %s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
5
pkg/apis/batch/zz_generated.deepcopy.go
generated
5
pkg/apis/batch/zz_generated.deepcopy.go
generated
@@ -308,6 +308,11 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) {
|
|||||||
*out = new(PodReplacementPolicy)
|
*out = new(PodReplacementPolicy)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.ManagedBy != nil {
|
||||||
|
in, out := &in.ManagedBy, &out.ManagedBy
|
||||||
|
*out = new(string)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -169,7 +169,7 @@ func newControllerWithClock(ctx context.Context, podInformer coreinformers.PodIn
|
|||||||
|
|
||||||
if _, err := jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
if _, err := jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
AddFunc: func(obj interface{}) {
|
AddFunc: func(obj interface{}) {
|
||||||
jm.enqueueSyncJobImmediately(logger, obj)
|
jm.addJob(logger, obj)
|
||||||
},
|
},
|
||||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||||
jm.updateJob(logger, oldObj, newObj)
|
jm.updateJob(logger, oldObj, newObj)
|
||||||
@@ -448,6 +448,17 @@ func (jm *Controller) deletePod(logger klog.Logger, obj interface{}, final bool)
|
|||||||
jm.enqueueSyncJobBatched(logger, job)
|
jm.enqueueSyncJobBatched(logger, job)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (jm *Controller) addJob(logger klog.Logger, obj interface{}) {
|
||||||
|
jm.enqueueSyncJobImmediately(logger, obj)
|
||||||
|
jobObj, ok := obj.(*batch.Job)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if controllerName := managedByExternalController(jobObj); controllerName != nil {
|
||||||
|
metrics.JobByExternalControllerTotal.WithLabelValues(*controllerName).Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (jm *Controller) updateJob(logger klog.Logger, old, cur interface{}) {
|
func (jm *Controller) updateJob(logger klog.Logger, old, cur interface{}) {
|
||||||
oldJob := old.(*batch.Job)
|
oldJob := old.(*batch.Job)
|
||||||
curJob := cur.(*batch.Job)
|
curJob := cur.(*batch.Job)
|
||||||
@@ -545,6 +556,7 @@ func (jm *Controller) enqueueSyncJobInternal(logger klog.Logger, obj interface{}
|
|||||||
utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %+v: %v", obj, err))
|
utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %+v: %v", obj, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle overlapping controllers better. Either disallow them at admission time or
|
// TODO: Handle overlapping controllers better. Either disallow them at admission time or
|
||||||
// deterministically avoid syncing controllers that fight over pods. Currently, we only
|
// deterministically avoid syncing controllers that fight over pods. Currently, we only
|
||||||
// ensure that the same controller is synced for a given pod. When we periodically relist
|
// ensure that the same controller is synced for a given pod. When we periodically relist
|
||||||
@@ -636,6 +648,13 @@ func (jm *Controller) syncOrphanPod(ctx context.Context, key string) error {
|
|||||||
// Make sure the pod is still orphaned.
|
// Make sure the pod is still orphaned.
|
||||||
if controllerRef := metav1.GetControllerOf(sharedPod); controllerRef != nil {
|
if controllerRef := metav1.GetControllerOf(sharedPod); controllerRef != nil {
|
||||||
job := jm.resolveControllerRef(sharedPod.Namespace, controllerRef)
|
job := jm.resolveControllerRef(sharedPod.Namespace, controllerRef)
|
||||||
|
if job != nil {
|
||||||
|
// Skip cleanup of finalizers for pods owned by a job managed by an external controller
|
||||||
|
if controllerName := managedByExternalController(job); controllerName != nil {
|
||||||
|
logger.V(2).Info("Skip cleanup of the job finalizer for a pod owned by a job that is managed by an external controller", "key", key, "podUID", sharedPod.UID, "jobUID", job.UID, "controllerName", controllerName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if job != nil && !IsJobFinished(job) {
|
if job != nil && !IsJobFinished(job) {
|
||||||
// The pod was adopted. Do not remove finalizer.
|
// The pod was adopted. Do not remove finalizer.
|
||||||
return nil
|
return nil
|
||||||
@@ -732,6 +751,17 @@ func (jm *Controller) syncJob(ctx context.Context, key string) (rErr error) {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip syncing of the job it is managed by another controller.
|
||||||
|
// We cannot rely solely on skipping of queueing such jobs for synchronization,
|
||||||
|
// because it is possible a synchronization task is queued for a job, without
|
||||||
|
// the managedBy field, but the job is quickly replaced by another job with
|
||||||
|
// the field. Then, the syncJob might be invoked for a job with the field.
|
||||||
|
if controllerName := managedByExternalController(sharedJob); controllerName != nil {
|
||||||
|
logger.V(2).Info("Skip syncing the job as it is managed by an external controller", "key", key, "uid", sharedJob.UID, "controllerName", controllerName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// make a copy so we don't mutate the shared cache
|
// make a copy so we don't mutate the shared cache
|
||||||
job := *sharedJob.DeepCopy()
|
job := *sharedJob.DeepCopy()
|
||||||
|
|
||||||
@@ -1934,3 +1964,12 @@ func recordJobPodsCreationTotal(job *batch.Job, jobCtx *syncJobCtx, succeeded, f
|
|||||||
metrics.JobPodsCreationTotal.WithLabelValues(reason, metrics.Failed).Add(float64(failed))
|
metrics.JobPodsCreationTotal.WithLabelValues(reason, metrics.Failed).Add(float64(failed))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func managedByExternalController(jobObj *batch.Job) *string {
|
||||||
|
if feature.DefaultFeatureGate.Enabled(features.JobManagedBy) {
|
||||||
|
if controllerName := jobObj.Spec.ManagedBy; controllerName != nil && *controllerName != batch.JobControllerName {
|
||||||
|
return controllerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -2292,6 +2292,126 @@ func TestSyncJobDeleted(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSyncJobWhenManagedBy(t *testing.T) {
|
||||||
|
_, ctx := ktesting.NewTestContext(t)
|
||||||
|
now := metav1.Now()
|
||||||
|
baseJob := batch.Job{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Job"},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "foobar",
|
||||||
|
Namespace: metav1.NamespaceDefault,
|
||||||
|
},
|
||||||
|
Spec: batch.JobSpec{
|
||||||
|
Template: v1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: v1.PodSpec{
|
||||||
|
Containers: []v1.Container{
|
||||||
|
{Image: "foo/bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Parallelism: ptr.To[int32](2),
|
||||||
|
Completions: ptr.To[int32](2),
|
||||||
|
BackoffLimit: ptr.To[int32](6),
|
||||||
|
},
|
||||||
|
Status: batch.JobStatus{
|
||||||
|
Active: 1,
|
||||||
|
Ready: ptr.To[int32](1),
|
||||||
|
StartTime: &now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
enableJobManagedBy bool
|
||||||
|
job batch.Job
|
||||||
|
wantStatus batch.JobStatus
|
||||||
|
}{
|
||||||
|
"job with custom value of managedBy; feature enabled; the status is unchanged": {
|
||||||
|
enableJobManagedBy: true,
|
||||||
|
job: func() batch.Job {
|
||||||
|
job := baseJob.DeepCopy()
|
||||||
|
job.Spec.ManagedBy = ptr.To("custom-managed-by")
|
||||||
|
return *job
|
||||||
|
}(),
|
||||||
|
wantStatus: baseJob.Status,
|
||||||
|
},
|
||||||
|
"job with well known value of the managedBy; feature enabled; the status is updated": {
|
||||||
|
enableJobManagedBy: true,
|
||||||
|
job: func() batch.Job {
|
||||||
|
job := baseJob.DeepCopy()
|
||||||
|
job.Spec.ManagedBy = ptr.To(batch.JobControllerName)
|
||||||
|
return *job
|
||||||
|
}(),
|
||||||
|
wantStatus: batch.JobStatus{
|
||||||
|
Active: 2,
|
||||||
|
Ready: ptr.To[int32](0),
|
||||||
|
StartTime: &now,
|
||||||
|
Terminating: ptr.To[int32](0),
|
||||||
|
UncountedTerminatedPods: &batch.UncountedTerminatedPods{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job with custom value of managedBy; feature disabled; the status is updated": {
|
||||||
|
job: func() batch.Job {
|
||||||
|
job := baseJob.DeepCopy()
|
||||||
|
job.Spec.ManagedBy = ptr.To("custom-managed-by")
|
||||||
|
return *job
|
||||||
|
}(),
|
||||||
|
wantStatus: batch.JobStatus{
|
||||||
|
Active: 2,
|
||||||
|
Ready: ptr.To[int32](0),
|
||||||
|
StartTime: &now,
|
||||||
|
Terminating: ptr.To[int32](0),
|
||||||
|
UncountedTerminatedPods: &batch.UncountedTerminatedPods{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"job without the managedBy; feature enabled; the status is updated": {
|
||||||
|
enableJobManagedBy: true,
|
||||||
|
job: baseJob,
|
||||||
|
wantStatus: batch.JobStatus{
|
||||||
|
Active: 2,
|
||||||
|
Ready: ptr.To[int32](0),
|
||||||
|
StartTime: &now,
|
||||||
|
Terminating: ptr.To[int32](0),
|
||||||
|
UncountedTerminatedPods: &batch.UncountedTerminatedPods{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobManagedBy, tc.enableJobManagedBy)()
|
||||||
|
|
||||||
|
clientset := clientset.NewForConfigOrDie(&restclient.Config{Host: "", ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}})
|
||||||
|
manager, sharedInformerFactory := newControllerFromClient(ctx, t, clientset, controller.NoResyncPeriodFunc)
|
||||||
|
fakePodControl := controller.FakePodControl{}
|
||||||
|
manager.podControl = &fakePodControl
|
||||||
|
manager.podStoreSynced = alwaysReady
|
||||||
|
manager.jobStoreSynced = alwaysReady
|
||||||
|
job := &tc.job
|
||||||
|
|
||||||
|
actual := job
|
||||||
|
manager.updateStatusHandler = func(_ context.Context, job *batch.Job) (*batch.Job, error) {
|
||||||
|
actual = job
|
||||||
|
return job, nil
|
||||||
|
}
|
||||||
|
if err := sharedInformerFactory.Batch().V1().Jobs().Informer().GetIndexer().Add(job); err != nil {
|
||||||
|
t.Fatalf("error %v while adding the %v job to the index", err, klog.KObj(job))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.syncJob(ctx, testutil.GetKey(job, t)); err != nil {
|
||||||
|
t.Fatalf("error %v while reconciling the job %v", err, testutil.GetKey(job, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.wantStatus, actual.Status); diff != "" {
|
||||||
|
t.Errorf("Unexpected job status (-want,+got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSyncJobWithJobPodFailurePolicy(t *testing.T) {
|
func TestSyncJobWithJobPodFailurePolicy(t *testing.T) {
|
||||||
_, ctx := ktesting.NewTestContext(t)
|
_, ctx := ktesting.NewTestContext(t)
|
||||||
now := metav1.Now()
|
now := metav1.Now()
|
||||||
|
@@ -71,6 +71,20 @@ var (
|
|||||||
[]string{"completion_mode", "result", "reason"},
|
[]string{"completion_mode", "result", "reason"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// JobByExternalControllerTotal tracks the number of Jobs that were created
|
||||||
|
// as managed by an external controller.
|
||||||
|
// The value of the label controller_name corresponds to the value of the
|
||||||
|
// managedBy field.
|
||||||
|
JobByExternalControllerTotal = metrics.NewCounterVec(
|
||||||
|
&metrics.CounterOpts{
|
||||||
|
Subsystem: JobControllerSubsystem,
|
||||||
|
Name: "jobs_by_external_controller_total",
|
||||||
|
Help: "The number of Jobs managed by an external controller",
|
||||||
|
StabilityLevel: metrics.ALPHA,
|
||||||
|
},
|
||||||
|
[]string{"controller_name"},
|
||||||
|
)
|
||||||
|
|
||||||
// JobPodsFinished records the number of finished Pods that the job controller
|
// JobPodsFinished records the number of finished Pods that the job controller
|
||||||
// finished tracking.
|
// finished tracking.
|
||||||
// It only applies to Jobs that were created while the feature gate
|
// It only applies to Jobs that were created while the feature gate
|
||||||
@@ -195,5 +209,6 @@ func Register() {
|
|||||||
legacyregistry.MustRegister(TerminatedPodsTrackingFinalizerTotal)
|
legacyregistry.MustRegister(TerminatedPodsTrackingFinalizerTotal)
|
||||||
legacyregistry.MustRegister(JobFinishedIndexesTotal)
|
legacyregistry.MustRegister(JobFinishedIndexesTotal)
|
||||||
legacyregistry.MustRegister(JobPodsCreationTotal)
|
legacyregistry.MustRegister(JobPodsCreationTotal)
|
||||||
|
legacyregistry.MustRegister(JobByExternalControllerTotal)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -349,6 +349,13 @@ const (
|
|||||||
// Allows users to specify counting of failed pods per index.
|
// Allows users to specify counting of failed pods per index.
|
||||||
JobBackoffLimitPerIndex featuregate.Feature = "JobBackoffLimitPerIndex"
|
JobBackoffLimitPerIndex featuregate.Feature = "JobBackoffLimitPerIndex"
|
||||||
|
|
||||||
|
// owner: @mimowo
|
||||||
|
// kep: https://kep.k8s.io/4368
|
||||||
|
// alpha: v1.30
|
||||||
|
//
|
||||||
|
// Allows to delegate reconciliation of a Job object to an external controller.
|
||||||
|
JobManagedBy featuregate.Feature = "JobManagedBy"
|
||||||
|
|
||||||
// owner: @mimowo
|
// owner: @mimowo
|
||||||
// kep: https://kep.k8s.io/3329
|
// kep: https://kep.k8s.io/3329
|
||||||
// alpha: v1.25
|
// alpha: v1.25
|
||||||
@@ -1048,6 +1055,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
|||||||
|
|
||||||
JobBackoffLimitPerIndex: {Default: true, PreRelease: featuregate.Beta},
|
JobBackoffLimitPerIndex: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
|
||||||
|
JobManagedBy: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
|
|
||||||
JobPodFailurePolicy: {Default: true, PreRelease: featuregate.Beta},
|
JobPodFailurePolicy: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
|
||||||
JobPodReplacementPolicy: {Default: true, PreRelease: featuregate.Beta},
|
JobPodReplacementPolicy: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
27
pkg/generated/openapi/zz_generated.openapi.go
generated
27
pkg/generated/openapi/zz_generated.openapi.go
generated
@@ -16235,6 +16235,13 @@ func schema_k8sio_api_batch_v1_JobSpec(ref common.ReferenceCallback) common.Open
|
|||||||
Enum: []interface{}{"Failed", "TerminatingOrFailed"},
|
Enum: []interface{}{"Failed", "TerminatingOrFailed"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"managedBy": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "ManagedBy field indicates the controller that manages a Job. The k8s Job controller reconciles jobs which don't have this field at all or the field value is the reserved string `kubernetes.io/job-controller`, but skips reconciling Jobs with a custom value for this field. The value must be a valid domain-prefixed path (e.g. acme.io/foo) - all characters before the first \"/\" must be a valid subdomain as defined by RFC 1123. All characters trailing the first \"/\" must be valid HTTP Path characters as defined by RFC 3986. The value cannot exceed 64 characters.\n\nThis field is alpha-level. The job controller accepts setting the field when the feature gate JobManagedBy is enabled (disabled by default).",
|
||||||
|
Type: []string{"string"},
|
||||||
|
Format: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Required: []string{"template"},
|
Required: []string{"template"},
|
||||||
},
|
},
|
||||||
@@ -16260,7 +16267,7 @@ func schema_k8sio_api_batch_v1_JobStatus(ref common.ReferenceCallback) common.Op
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
|
Description: "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true.\n\nA job is considered finished when it is in a terminal condition, either \"Complete\" or \"Failed\". At that point, all pods of the job are in terminal phase. Job cannot be both in the \"Complete\" and \"Failed\" conditions. Additionally, it cannot be in the \"Complete\" and \"FailureTarget\" conditions. The \"Complete\", \"Failed\" and \"FailureTarget\" conditions cannot be disabled.\n\nMore info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
|
||||||
Type: []string{"array"},
|
Type: []string{"array"},
|
||||||
Items: &spec.SchemaOrArray{
|
Items: &spec.SchemaOrArray{
|
||||||
Schema: &spec.Schema{
|
Schema: &spec.Schema{
|
||||||
@@ -16274,40 +16281,40 @@ func schema_k8sio_api_batch_v1_JobStatus(ref common.ReferenceCallback) common.Op
|
|||||||
},
|
},
|
||||||
"startTime": {
|
"startTime": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC.",
|
Description: "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC.\n\nOnce set, the field can only be removed when the job is suspended. The field cannot be modified while the job is unsuspended or finished.",
|
||||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"),
|
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"completionTime": {
|
"completionTime": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "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. The completion time is only set when the job finishes successfully.",
|
Description: "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. The completion time is set when the job finishes successfully, and only then. The value cannot be updated or removed. The value indicates the same or later point in time as the startTime field.",
|
||||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"),
|
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"active": {
|
"active": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "The number of pending and running pods which are not terminating (without a deletionTimestamp).",
|
Description: "The number of pending and running pods which are not terminating (without a deletionTimestamp). The value is zero for finished jobs.",
|
||||||
Type: []string{"integer"},
|
Type: []string{"integer"},
|
||||||
Format: "int32",
|
Format: "int32",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"succeeded": {
|
"succeeded": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "The number of pods which reached phase Succeeded.",
|
Description: "The number of pods which reached phase Succeeded. The value increases monotonically for a given spec. However, it may decrease in reaction to scale down of elastic indexed jobs.",
|
||||||
Type: []string{"integer"},
|
Type: []string{"integer"},
|
||||||
Format: "int32",
|
Format: "int32",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"failed": {
|
"failed": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "The number of pods which reached phase Failed.",
|
Description: "The number of pods which reached phase Failed. The value increases monotonically.",
|
||||||
Type: []string{"integer"},
|
Type: []string{"integer"},
|
||||||
Format: "int32",
|
Format: "int32",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"terminating": {
|
"terminating": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "The number of pods which are terminating (in phase Pending or Running and have a deletionTimestamp).\n\nThis field is beta-level. The job controller populates the field when the feature gate JobPodReplacementPolicy is enabled (enabled by default).",
|
Description: "The number of pods which are terminating (in phase Pending or Running and have a deletionTimestamp). The value is zero (or null) for finished jobs.\n\nThis field is beta-level. The job controller populates the field when the feature gate JobPodReplacementPolicy is enabled (enabled by default).",
|
||||||
Type: []string{"integer"},
|
Type: []string{"integer"},
|
||||||
Format: "int32",
|
Format: "int32",
|
||||||
},
|
},
|
||||||
@@ -16321,20 +16328,20 @@ func schema_k8sio_api_batch_v1_JobStatus(ref common.ReferenceCallback) common.Op
|
|||||||
},
|
},
|
||||||
"failedIndexes": {
|
"failedIndexes": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "FailedIndexes holds the failed indexes when backoffLimitPerIndex=true. The indexes are represented in the text format analogous as for the `completedIndexes` field, ie. they are kept as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the failed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\". This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` feature gate is enabled (enabled by default).",
|
Description: "FailedIndexes holds the failed indexes when spec.backoffLimitPerIndex is set. The indexes are represented in the text format analogous as for the `completedIndexes` field, ie. they are kept as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the failed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\". The set of failed indexes cannot overlap with the set of completed indexes.\n\nThis field is beta-level. It can be used when the `JobBackoffLimitPerIndex` feature gate is enabled (enabled by default).",
|
||||||
Type: []string{"string"},
|
Type: []string{"string"},
|
||||||
Format: "",
|
Format: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"uncountedTerminatedPods": {
|
"uncountedTerminatedPods": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null.",
|
Description: "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null. The structure is empty for finished jobs.",
|
||||||
Ref: ref("k8s.io/api/batch/v1.UncountedTerminatedPods"),
|
Ref: ref("k8s.io/api/batch/v1.UncountedTerminatedPods"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"ready": {
|
"ready": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "The number of pods which have a Ready condition.",
|
Description: "The number of pods which have a Ready condition. The value is zero (or null) for finished jobs.",
|
||||||
Type: []string{"integer"},
|
Type: []string{"integer"},
|
||||||
Format: "int32",
|
Format: "int32",
|
||||||
},
|
},
|
||||||
|
@@ -44,6 +44,7 @@ import (
|
|||||||
batchvalidation "k8s.io/kubernetes/pkg/apis/batch/validation"
|
batchvalidation "k8s.io/kubernetes/pkg/apis/batch/validation"
|
||||||
"k8s.io/kubernetes/pkg/apis/core"
|
"k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -100,6 +101,9 @@ func (jobStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
|||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodFailurePolicy) {
|
if !utilfeature.DefaultFeatureGate.Enabled(features.JobPodFailurePolicy) {
|
||||||
job.Spec.PodFailurePolicy = nil
|
job.Spec.PodFailurePolicy = nil
|
||||||
}
|
}
|
||||||
|
if !utilfeature.DefaultFeatureGate.Enabled(features.JobManagedBy) {
|
||||||
|
job.Spec.ManagedBy = nil
|
||||||
|
}
|
||||||
|
|
||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.JobBackoffLimitPerIndex) {
|
if !utilfeature.DefaultFeatureGate.Enabled(features.JobBackoffLimitPerIndex) {
|
||||||
job.Spec.BackoffLimitPerIndex = nil
|
job.Spec.BackoffLimitPerIndex = nil
|
||||||
@@ -331,7 +335,77 @@ func (jobStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (jobStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
func (jobStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
return batchvalidation.ValidateJobUpdateStatus(obj.(*batch.Job), old.(*batch.Job))
|
newJob := obj.(*batch.Job)
|
||||||
|
oldJob := old.(*batch.Job)
|
||||||
|
|
||||||
|
opts := getStatusValidationOptions(newJob, oldJob)
|
||||||
|
return batchvalidation.ValidateJobUpdateStatus(newJob, oldJob, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStatusValidationOptions returns validation options for Job status
|
||||||
|
func getStatusValidationOptions(newJob, oldJob *batch.Job) batchvalidation.JobStatusValidationOptions {
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.JobManagedBy) {
|
||||||
|
// A strengthened validation of the Job status transitions is needed since the
|
||||||
|
// Job managedBy field let's the Job object be controlled by external
|
||||||
|
// controllers. We want to make sure the transitions done by the external
|
||||||
|
// controllers meet the expectations of the clients of the Job API.
|
||||||
|
// For example, we verify that a Job in terminal state (Failed or Complete)
|
||||||
|
// does not flip to a non-terminal state.
|
||||||
|
//
|
||||||
|
// In the checks below we fail validation for Job status fields (or conditions) only if they change their values
|
||||||
|
// (compared to the oldJob). This allows proceeding with status updates unrelated to the fields violating the
|
||||||
|
// checks, while blocking bad status updates for jobs with correct status.
|
||||||
|
//
|
||||||
|
// Also note, there is another reason we run the validation rules only
|
||||||
|
// if the associated status fields changed. We do it also because some of
|
||||||
|
// the validation rules might be temporarily violated just after a user
|
||||||
|
// updating the spec. In that case we want to give time to the Job
|
||||||
|
// controller to "fix" the status in the following sync. For example, the
|
||||||
|
// rule for checking the format of completedIndexes expects them to be
|
||||||
|
// below .spec.completions, however, this it is ok if the
|
||||||
|
// status.completedIndexes go beyond completions just after a user scales
|
||||||
|
// down a Job.
|
||||||
|
isIndexed := ptr.Deref(newJob.Spec.CompletionMode, batch.NonIndexedCompletion) == batch.IndexedCompletion
|
||||||
|
|
||||||
|
isJobFinishedChanged := batchvalidation.IsJobFinished(oldJob) != batchvalidation.IsJobFinished(newJob)
|
||||||
|
isJobCompleteChanged := batchvalidation.IsJobComplete(oldJob) != batchvalidation.IsJobComplete(newJob)
|
||||||
|
isJobFailedChanged := batchvalidation.IsJobFailed(oldJob) != batchvalidation.IsJobFailed(newJob)
|
||||||
|
isJobFailureTargetChanged := batchvalidation.IsConditionTrue(oldJob.Status.Conditions, batch.JobFailureTarget) != batchvalidation.IsConditionTrue(newJob.Status.Conditions, batch.JobFailureTarget)
|
||||||
|
isCompletedIndexesChanged := oldJob.Status.CompletedIndexes != newJob.Status.CompletedIndexes
|
||||||
|
isFailedIndexesChanged := !ptr.Equal(oldJob.Status.FailedIndexes, newJob.Status.FailedIndexes)
|
||||||
|
isActiveChanged := oldJob.Status.Active != newJob.Status.Active
|
||||||
|
isReadyChanged := !ptr.Equal(oldJob.Status.Ready, newJob.Status.Ready)
|
||||||
|
isTerminatingChanged := !ptr.Equal(oldJob.Status.Terminating, newJob.Status.Terminating)
|
||||||
|
isStartTimeChanged := !ptr.Equal(oldJob.Status.StartTime, newJob.Status.StartTime)
|
||||||
|
isCompletionTimeChanged := !ptr.Equal(oldJob.Status.CompletionTime, newJob.Status.CompletionTime)
|
||||||
|
isUncountedTerminatedPodsChanged := !apiequality.Semantic.DeepEqual(oldJob.Status.UncountedTerminatedPods, newJob.Status.UncountedTerminatedPods)
|
||||||
|
|
||||||
|
return batchvalidation.JobStatusValidationOptions{
|
||||||
|
// We allow to decrease the counter for succeeded pods for jobs which
|
||||||
|
// have equal parallelism and completions, as they can be scaled-down.
|
||||||
|
RejectDecreasingSucceededCounter: !isIndexed || !ptr.Equal(newJob.Spec.Completions, newJob.Spec.Parallelism),
|
||||||
|
RejectDecreasingFailedCounter: true,
|
||||||
|
RejectDisablingTerminalCondition: true,
|
||||||
|
RejectInvalidCompletedIndexes: isCompletedIndexesChanged,
|
||||||
|
RejectInvalidFailedIndexes: isFailedIndexesChanged,
|
||||||
|
RejectCompletedIndexesForNonIndexedJob: isCompletedIndexesChanged,
|
||||||
|
RejectFailedIndexesForNoBackoffLimitPerIndex: isFailedIndexesChanged,
|
||||||
|
RejectFailedIndexesOverlappingCompleted: isFailedIndexesChanged || isCompletedIndexesChanged,
|
||||||
|
RejectMoreReadyThanActivePods: isReadyChanged || isActiveChanged,
|
||||||
|
RejectFinishedJobWithActivePods: isJobFinishedChanged || isActiveChanged,
|
||||||
|
RejectFinishedJobWithTerminatingPods: isJobFinishedChanged || isTerminatingChanged,
|
||||||
|
RejectFinishedJobWithoutStartTime: isJobFinishedChanged || isStartTimeChanged,
|
||||||
|
RejectFinishedJobWithUncountedTerminatedPods: isJobFinishedChanged || isUncountedTerminatedPodsChanged,
|
||||||
|
RejectStartTimeUpdateForUnsuspendedJob: isStartTimeChanged,
|
||||||
|
RejectCompletionTimeBeforeStartTime: isStartTimeChanged || isCompletionTimeChanged,
|
||||||
|
RejectMutatingCompletionTime: true,
|
||||||
|
RejectNotCompleteJobWithCompletionTime: isJobCompleteChanged || isCompletionTimeChanged,
|
||||||
|
RejectCompleteJobWithoutCompletionTime: isJobCompleteChanged || isCompletionTimeChanged,
|
||||||
|
RejectCompleteJobWithFailedCondition: isJobCompleteChanged || isJobFailedChanged,
|
||||||
|
RejectCompleteJobWithFailureTargetCondition: isJobCompleteChanged || isJobFailureTargetChanged,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return batchvalidation.JobStatusValidationOptions{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WarningsOnUpdate returns warnings for the given update.
|
// WarningsOnUpdate returns warnings for the given update.
|
||||||
|
File diff suppressed because it is too large
Load Diff
272
staging/src/k8s.io/api/batch/v1/generated.pb.go
generated
272
staging/src/k8s.io/api/batch/v1/generated.pb.go
generated
@@ -495,119 +495,120 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fileDescriptor_79228dc2c4001a22 = []byte{
|
var fileDescriptor_79228dc2c4001a22 = []byte{
|
||||||
// 1783 bytes of a gzipped FileDescriptorProto
|
// 1804 bytes of a gzipped FileDescriptorProto
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x58, 0xcd, 0x6f, 0x24, 0x47,
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x58, 0x4f, 0x6f, 0xe4, 0x48,
|
||||||
0x15, 0xf7, 0xd8, 0x1e, 0x7b, 0xa6, 0xc6, 0x1f, 0xb3, 0xb5, 0xde, 0xdd, 0xc1, 0x44, 0xd3, 0xce,
|
0x15, 0x4f, 0x27, 0xe9, 0xa4, 0xbb, 0x3a, 0x7f, 0x7a, 0x6a, 0x32, 0x33, 0x4d, 0x58, 0xb5, 0xb3,
|
||||||
0x6c, 0x12, 0x39, 0x28, 0xf4, 0x64, 0x9d, 0x15, 0xe1, 0x43, 0xa0, 0x6c, 0x7b, 0xd9, 0xb0, 0x66,
|
0x3d, 0xbb, 0xab, 0x2c, 0x2c, 0xee, 0x9d, 0xec, 0x88, 0xe5, 0x8f, 0x40, 0x3b, 0xce, 0x30, 0xcb,
|
||||||
0x9c, 0x1d, 0x6a, 0xbc, 0x20, 0x85, 0x80, 0xa8, 0xe9, 0xae, 0x19, 0x77, 0xb6, 0xa7, 0xab, 0xe9,
|
0x84, 0xce, 0x4e, 0x53, 0x9d, 0x01, 0x69, 0x59, 0x10, 0xd5, 0x76, 0x75, 0xc7, 0x3b, 0xb6, 0xcb,
|
||||||
0xaa, 0xb6, 0xd6, 0x17, 0x84, 0xc4, 0x1f, 0x00, 0x7f, 0x05, 0x47, 0x2e, 0x70, 0x86, 0x1b, 0xf2,
|
0xd8, 0xe5, 0x68, 0x72, 0x41, 0x48, 0x7c, 0x01, 0x3e, 0x05, 0x47, 0x2e, 0x70, 0x44, 0x70, 0x43,
|
||||||
0x31, 0xe2, 0x14, 0x71, 0x68, 0xb1, 0xcd, 0x1f, 0xc0, 0xdd, 0x08, 0x09, 0x55, 0x75, 0x4d, 0x7f,
|
0x39, 0xae, 0x38, 0xad, 0x38, 0x58, 0x8c, 0xf9, 0x00, 0xdc, 0x83, 0x90, 0x50, 0x95, 0xcb, 0x7f,
|
||||||
0x4d, 0xb7, 0xf1, 0x46, 0x62, 0x95, 0x9b, 0xfb, 0xbd, 0xdf, 0xfb, 0xd5, 0xc7, 0x7b, 0xf5, 0x7b,
|
0xdb, 0x0e, 0x99, 0x95, 0x18, 0x71, 0x8b, 0xdf, 0xfb, 0xbd, 0xdf, 0x7b, 0x55, 0xef, 0xd5, 0x7b,
|
||||||
0xcf, 0x03, 0xee, 0x3e, 0xfb, 0x26, 0xd3, 0x6d, 0xda, 0xc7, 0x9e, 0xdd, 0x1f, 0x63, 0x6e, 0x9e,
|
0x2f, 0x0d, 0xee, 0x3e, 0xfb, 0x86, 0xaf, 0x9a, 0x74, 0x88, 0x5d, 0x73, 0x38, 0xc5, 0x4c, 0x3f,
|
||||||
0xf6, 0xcf, 0xee, 0xf5, 0xa7, 0xc4, 0x25, 0x3e, 0xe6, 0xc4, 0xd2, 0x3d, 0x9f, 0x72, 0x0a, 0x6f,
|
0x1d, 0x9e, 0xdd, 0x1b, 0xce, 0x89, 0x43, 0x3c, 0xcc, 0x88, 0xa1, 0xba, 0x1e, 0x65, 0x14, 0xde,
|
||||||
0xc6, 0x20, 0x1d, 0x7b, 0xb6, 0x2e, 0x41, 0xfa, 0xd9, 0xbd, 0xdd, 0xaf, 0x4f, 0x6d, 0x7e, 0x1a,
|
0x8c, 0x41, 0x2a, 0x76, 0x4d, 0x55, 0x80, 0xd4, 0xb3, 0x7b, 0xbb, 0x5f, 0x9b, 0x9b, 0xec, 0x34,
|
||||||
0x8c, 0x75, 0x93, 0xce, 0xfa, 0x53, 0x3a, 0xa5, 0x7d, 0x89, 0x1d, 0x07, 0x13, 0xf9, 0x25, 0x3f,
|
0x98, 0xaa, 0x3a, 0xb5, 0x87, 0x73, 0x3a, 0xa7, 0x43, 0x81, 0x9d, 0x06, 0x33, 0xf1, 0x25, 0x3e,
|
||||||
0xe4, 0x5f, 0x31, 0xc7, 0x6e, 0x2f, 0xb3, 0x90, 0x49, 0x7d, 0x52, 0xb2, 0xce, 0xee, 0xfd, 0x14,
|
0xc4, 0x5f, 0x31, 0xc7, 0xee, 0x20, 0xe7, 0x48, 0xa7, 0x1e, 0xa9, 0xf0, 0xb3, 0x7b, 0x3f, 0xc3,
|
||||||
0x33, 0xc3, 0xe6, 0xa9, 0xed, 0x12, 0xff, 0xbc, 0xef, 0x3d, 0x9b, 0x0a, 0x03, 0xeb, 0xcf, 0x08,
|
0xd8, 0x58, 0x3f, 0x35, 0x1d, 0xe2, 0x9d, 0x0f, 0xdd, 0x67, 0x73, 0x2e, 0xf0, 0x87, 0x36, 0x61,
|
||||||
0xc7, 0x65, 0x51, 0xfd, 0xaa, 0x28, 0x3f, 0x70, 0xb9, 0x3d, 0x23, 0x0b, 0x01, 0xdf, 0xf8, 0x5f,
|
0xb8, 0xca, 0x6a, 0x58, 0x67, 0xe5, 0x05, 0x0e, 0x33, 0x6d, 0xb2, 0x60, 0xf0, 0xf5, 0xff, 0x66,
|
||||||
0x01, 0xcc, 0x3c, 0x25, 0x33, 0x5c, 0x8c, 0xeb, 0xfd, 0xbb, 0x06, 0xd6, 0x0f, 0x7d, 0xea, 0x1e,
|
0xe0, 0xeb, 0xa7, 0xc4, 0xc6, 0x65, 0xbb, 0xc1, 0xbf, 0x1a, 0x60, 0xfd, 0xd0, 0xa3, 0xce, 0x11,
|
||||||
0xd1, 0x31, 0xfc, 0x05, 0x68, 0x88, 0xfd, 0x58, 0x98, 0xe3, 0x4e, 0x6d, 0xaf, 0xb6, 0xdf, 0x3a,
|
0x9d, 0xc2, 0x9f, 0x83, 0x16, 0x8f, 0xc7, 0xc0, 0x0c, 0xf7, 0x1a, 0x7b, 0x8d, 0xfd, 0xce, 0xc1,
|
||||||
0x78, 0x57, 0x4f, 0x6f, 0x29, 0xa1, 0xd5, 0xbd, 0x67, 0x53, 0x61, 0x60, 0xba, 0x40, 0xeb, 0x67,
|
0xbb, 0x6a, 0x76, 0x4b, 0x29, 0xad, 0xea, 0x3e, 0x9b, 0x73, 0x81, 0xaf, 0x72, 0xb4, 0x7a, 0x76,
|
||||||
0xf7, 0xf4, 0x27, 0xe3, 0x4f, 0x89, 0xc9, 0x8f, 0x09, 0xc7, 0x06, 0xbc, 0x08, 0xb5, 0xa5, 0x28,
|
0x4f, 0x7d, 0x32, 0xfd, 0x94, 0xe8, 0xec, 0x98, 0x30, 0xac, 0xc1, 0x8b, 0x50, 0x59, 0x8a, 0x42,
|
||||||
0xd4, 0x40, 0x6a, 0x43, 0x09, 0x2b, 0x34, 0xc0, 0x2a, 0xf3, 0x88, 0xd9, 0x59, 0x96, 0xec, 0x7b,
|
0x05, 0x64, 0x32, 0x94, 0xb2, 0x42, 0x0d, 0xac, 0xfa, 0x2e, 0xd1, 0x7b, 0xcb, 0x82, 0x7d, 0x4f,
|
||||||
0x7a, 0x49, 0x0e, 0x74, 0xb5, 0x9b, 0x91, 0x47, 0x4c, 0x63, 0x43, 0xb1, 0xad, 0x8a, 0x2f, 0x24,
|
0xad, 0xc8, 0x81, 0x2a, 0xa3, 0x99, 0xb8, 0x44, 0xd7, 0x36, 0x24, 0xdb, 0x2a, 0xff, 0x42, 0xc2,
|
||||||
0x63, 0xe1, 0x11, 0x58, 0x63, 0x1c, 0xf3, 0x80, 0x75, 0x56, 0x24, 0x4b, 0xef, 0x4a, 0x16, 0x89,
|
0x16, 0x1e, 0x81, 0x35, 0x9f, 0x61, 0x16, 0xf8, 0xbd, 0x15, 0xc1, 0x32, 0xb8, 0x92, 0x45, 0x20,
|
||||||
0x34, 0xb6, 0x14, 0xcf, 0x5a, 0xfc, 0x8d, 0x14, 0x43, 0xef, 0x0f, 0x35, 0xd0, 0x52, 0xc8, 0x81,
|
0xb5, 0x2d, 0xc9, 0xb3, 0x16, 0x7f, 0x23, 0xc9, 0x30, 0xf8, 0x5d, 0x03, 0x74, 0x24, 0x72, 0x64,
|
||||||
0xcd, 0x38, 0xfc, 0x64, 0xe1, 0x06, 0xf4, 0xeb, 0xdd, 0x80, 0x88, 0x96, 0xe7, 0x6f, 0xab, 0x95,
|
0xfa, 0x0c, 0x7e, 0xb2, 0x70, 0x03, 0xea, 0xf5, 0x6e, 0x80, 0x5b, 0x8b, 0xf3, 0x77, 0xa5, 0xa7,
|
||||||
0x1a, 0x73, 0x4b, 0xe6, 0xf4, 0x0f, 0x40, 0xdd, 0xe6, 0x64, 0xc6, 0x3a, 0xcb, 0x7b, 0x2b, 0xfb,
|
0x56, 0x22, 0xc9, 0x9d, 0xfe, 0x01, 0x68, 0x9a, 0x8c, 0xd8, 0x7e, 0x6f, 0x79, 0x6f, 0x65, 0xbf,
|
||||||
0xad, 0x83, 0xd7, 0xae, 0xda, 0xb8, 0xb1, 0xa9, 0x88, 0xea, 0x8f, 0x45, 0x08, 0x8a, 0x23, 0x7b,
|
0x73, 0xf0, 0xda, 0x55, 0x81, 0x6b, 0x9b, 0x92, 0xa8, 0xf9, 0x98, 0x9b, 0xa0, 0xd8, 0x72, 0xf0,
|
||||||
0x7f, 0x5b, 0x4d, 0x36, 0x2c, 0xae, 0x04, 0xbe, 0x03, 0x1a, 0x22, 0xb1, 0x56, 0xe0, 0x10, 0xb9,
|
0xd7, 0xd5, 0x34, 0x60, 0x7e, 0x25, 0xf0, 0x1d, 0xd0, 0xe2, 0x89, 0x35, 0x02, 0x8b, 0x88, 0x80,
|
||||||
0xe1, 0x66, 0xba, 0x81, 0x91, 0xb2, 0xa3, 0x04, 0x01, 0xf7, 0x41, 0x43, 0xd4, 0xc2, 0xc7, 0xd4,
|
0xdb, 0x59, 0x00, 0x13, 0x29, 0x47, 0x29, 0x02, 0xee, 0x83, 0x16, 0xaf, 0x85, 0x8f, 0xa9, 0x43,
|
||||||
0x25, 0x9d, 0x86, 0x44, 0x6f, 0x08, 0xe4, 0x89, 0xb2, 0xa1, 0xc4, 0x0b, 0x9f, 0x82, 0x3b, 0x8c,
|
0x7a, 0x2d, 0x81, 0xde, 0xe0, 0xc8, 0x13, 0x29, 0x43, 0xa9, 0x16, 0x3e, 0x05, 0x77, 0x7c, 0x86,
|
||||||
0x63, 0x9f, 0xdb, 0xee, 0xf4, 0x21, 0xc1, 0x96, 0x63, 0xbb, 0x64, 0x44, 0x4c, 0xea, 0x5a, 0x4c,
|
0x3d, 0x66, 0x3a, 0xf3, 0x87, 0x04, 0x1b, 0x96, 0xe9, 0x90, 0x09, 0xd1, 0xa9, 0x63, 0xf8, 0x22,
|
||||||
0xe6, 0x6e, 0xc5, 0xf8, 0x6a, 0x14, 0x6a, 0x77, 0x46, 0xe5, 0x10, 0x54, 0x15, 0x0b, 0x3f, 0x01,
|
0x77, 0x2b, 0xda, 0x97, 0xa3, 0x50, 0xb9, 0x33, 0xa9, 0x86, 0xa0, 0x3a, 0x5b, 0xf8, 0x09, 0xb8,
|
||||||
0x37, 0x4c, 0xea, 0x9a, 0x81, 0xef, 0x13, 0xd7, 0x3c, 0x1f, 0x52, 0xc7, 0x36, 0xcf, 0x65, 0x1a,
|
0xa1, 0x53, 0x47, 0x0f, 0x3c, 0x8f, 0x38, 0xfa, 0xf9, 0x98, 0x5a, 0xa6, 0x7e, 0x2e, 0xd2, 0xd8,
|
||||||
0x9b, 0x86, 0xae, 0xf6, 0x7d, 0xe3, 0xb0, 0x08, 0xb8, 0x2c, 0x33, 0xa2, 0x45, 0x22, 0xf8, 0x26,
|
0xd6, 0x54, 0x19, 0xf7, 0x8d, 0xc3, 0x32, 0xe0, 0xb2, 0x4a, 0x88, 0x16, 0x89, 0xe0, 0x9b, 0x60,
|
||||||
0x58, 0x67, 0x01, 0xf3, 0x88, 0x6b, 0x75, 0x56, 0xf7, 0x6a, 0xfb, 0x0d, 0xa3, 0x15, 0x85, 0xda,
|
0xdd, 0x0f, 0x7c, 0x97, 0x38, 0x46, 0x6f, 0x75, 0xaf, 0xb1, 0xdf, 0xd2, 0x3a, 0x51, 0xa8, 0xac,
|
||||||
0xfa, 0x28, 0x36, 0xa1, 0xb9, 0x0f, 0xfe, 0x14, 0xb4, 0x3e, 0xa5, 0xe3, 0x13, 0x32, 0xf3, 0x1c,
|
0x4f, 0x62, 0x11, 0x4a, 0x74, 0xf0, 0x27, 0xa0, 0xf3, 0x29, 0x9d, 0x9e, 0x10, 0xdb, 0xb5, 0x30,
|
||||||
0xcc, 0x49, 0xa7, 0x2e, 0xf3, 0xfc, 0x46, 0x69, 0x32, 0x8e, 0x52, 0x9c, 0xac, 0xc7, 0x9b, 0x6a,
|
0x23, 0xbd, 0xa6, 0xc8, 0xf3, 0x1b, 0x95, 0xc9, 0x38, 0xca, 0x70, 0xa2, 0x1e, 0x6f, 0xca, 0x20,
|
||||||
0x93, 0xad, 0x8c, 0x03, 0x65, 0xd9, 0xe0, 0xcf, 0xc1, 0x2e, 0x0b, 0x4c, 0x93, 0x30, 0x36, 0x09,
|
0x3b, 0x39, 0x05, 0xca, 0xb3, 0xc1, 0x9f, 0x81, 0x5d, 0x3f, 0xd0, 0x75, 0xe2, 0xfb, 0xb3, 0xc0,
|
||||||
0x9c, 0x23, 0x3a, 0x66, 0x3f, 0xb0, 0x19, 0xa7, 0xfe, 0xf9, 0xc0, 0x9e, 0xd9, 0xbc, 0xb3, 0xb6,
|
0x3a, 0xa2, 0x53, 0xff, 0xfb, 0xa6, 0xcf, 0xa8, 0x77, 0x3e, 0x32, 0x6d, 0x93, 0xf5, 0xd6, 0xf6,
|
||||||
0x57, 0xdb, 0xaf, 0x1b, 0xdd, 0x28, 0xd4, 0x76, 0x47, 0x95, 0x28, 0x74, 0x05, 0x03, 0x44, 0xe0,
|
0x1a, 0xfb, 0x4d, 0xad, 0x1f, 0x85, 0xca, 0xee, 0xa4, 0x16, 0x85, 0xae, 0x60, 0x80, 0x08, 0xdc,
|
||||||
0xf6, 0x04, 0xdb, 0x0e, 0xb1, 0x16, 0xb8, 0xd7, 0x25, 0xf7, 0x6e, 0x14, 0x6a, 0xb7, 0x1f, 0x95,
|
0x9e, 0x61, 0xd3, 0x22, 0xc6, 0x02, 0xf7, 0xba, 0xe0, 0xde, 0x8d, 0x42, 0xe5, 0xf6, 0xa3, 0x4a,
|
||||||
0x22, 0x50, 0x45, 0x64, 0xef, 0xcf, 0xcb, 0x60, 0x33, 0xf7, 0x5e, 0xe0, 0x0f, 0xc1, 0x1a, 0x36,
|
0x04, 0xaa, 0xb1, 0x1c, 0xfc, 0x69, 0x19, 0x6c, 0x16, 0xde, 0x0b, 0xfc, 0x01, 0x58, 0xc3, 0x3a,
|
||||||
0xb9, 0x7d, 0x26, 0x8a, 0x4a, 0x94, 0xea, 0xdd, 0xec, 0xed, 0x08, 0xa5, 0x4b, 0x5f, 0x3d, 0x22,
|
0x33, 0xcf, 0x78, 0x51, 0xf1, 0x52, 0xbd, 0x9b, 0xbf, 0x1d, 0xde, 0xe9, 0xb2, 0x57, 0x8f, 0xc8,
|
||||||
0x13, 0x22, 0x92, 0x40, 0xd2, 0x47, 0xf6, 0x40, 0x86, 0x22, 0x45, 0x01, 0x1d, 0xd0, 0x76, 0x30,
|
0x8c, 0xf0, 0x24, 0x90, 0xec, 0x91, 0x3d, 0x10, 0xa6, 0x48, 0x52, 0x40, 0x0b, 0x74, 0x2d, 0xec,
|
||||||
0xe3, 0xf3, 0x7a, 0x14, 0xd5, 0x26, 0xf3, 0xd3, 0x3a, 0xf8, 0xda, 0xf5, 0x1e, 0x97, 0x88, 0x30,
|
0xb3, 0xa4, 0x1e, 0x79, 0xb5, 0x89, 0xfc, 0x74, 0x0e, 0xbe, 0x72, 0xbd, 0xc7, 0xc5, 0x2d, 0xb4,
|
||||||
0x76, 0xa2, 0x50, 0x6b, 0x0f, 0x0a, 0x3c, 0x68, 0x81, 0x19, 0xfa, 0x00, 0x4a, 0x5b, 0x72, 0x85,
|
0x9d, 0x28, 0x54, 0xba, 0xa3, 0x12, 0x0f, 0x5a, 0x60, 0x86, 0x1e, 0x80, 0x42, 0x96, 0x5e, 0xa1,
|
||||||
0x72, 0xbd, 0xfa, 0x4b, 0xaf, 0x77, 0x3b, 0x0a, 0x35, 0x38, 0x58, 0x60, 0x42, 0x25, 0xec, 0xbd,
|
0xf0, 0xd7, 0x7c, 0x69, 0x7f, 0xb7, 0xa3, 0x50, 0x81, 0xa3, 0x05, 0x26, 0x54, 0xc1, 0x3e, 0xf8,
|
||||||
0x7f, 0xd5, 0xc0, 0xca, 0xab, 0x11, 0xd0, 0xef, 0xe5, 0x04, 0xf4, 0xb5, 0xaa, 0xa2, 0xad, 0x14,
|
0x67, 0x03, 0xac, 0xbc, 0x9a, 0x06, 0xfa, 0xdd, 0x42, 0x03, 0x7d, 0xad, 0xae, 0x68, 0x6b, 0x9b,
|
||||||
0xcf, 0x47, 0x05, 0xf1, 0xec, 0x56, 0x32, 0x5c, 0x2d, 0x9c, 0x7f, 0x5d, 0x01, 0x1b, 0x47, 0x74,
|
0xe7, 0xa3, 0x52, 0xf3, 0xec, 0xd7, 0x32, 0x5c, 0xdd, 0x38, 0xff, 0xb2, 0x02, 0x36, 0x8e, 0xe8,
|
||||||
0x7c, 0x48, 0x5d, 0xcb, 0xe6, 0x36, 0x75, 0xe1, 0x7d, 0xb0, 0xca, 0xcf, 0xbd, 0xb9, 0x08, 0xed,
|
0xf4, 0x90, 0x3a, 0x86, 0xc9, 0x4c, 0xea, 0xc0, 0xfb, 0x60, 0x95, 0x9d, 0xbb, 0x49, 0x13, 0xda,
|
||||||
0xcd, 0x97, 0x3e, 0x39, 0xf7, 0xc8, 0x65, 0xa8, 0xb5, 0xb3, 0x58, 0x61, 0x43, 0x12, 0x0d, 0x07,
|
0x4b, 0x5c, 0x9f, 0x9c, 0xbb, 0xe4, 0x32, 0x54, 0xba, 0x79, 0x2c, 0x97, 0x21, 0x81, 0x86, 0xa3,
|
||||||
0xc9, 0x76, 0x96, 0x65, 0xdc, 0xfd, 0xfc, 0x72, 0x97, 0xa1, 0x56, 0xd2, 0x62, 0xf5, 0x84, 0x29,
|
0x34, 0x9c, 0x65, 0x61, 0x77, 0xbf, 0xe8, 0xee, 0x32, 0x54, 0x2a, 0x46, 0xac, 0x9a, 0x32, 0x15,
|
||||||
0xbf, 0x29, 0x38, 0x05, 0x9b, 0x22, 0x39, 0x43, 0x9f, 0x8e, 0xe3, 0x2a, 0x5b, 0x79, 0xe9, 0xac,
|
0x83, 0x82, 0x73, 0xb0, 0xc9, 0x93, 0x33, 0xf6, 0xe8, 0x34, 0xae, 0xb2, 0x95, 0x97, 0xce, 0xfa,
|
||||||
0xdf, 0x52, 0x1b, 0xd8, 0x1c, 0x64, 0x89, 0x50, 0x9e, 0x17, 0x9e, 0xc5, 0x35, 0x76, 0xe2, 0x63,
|
0x2d, 0x19, 0xc0, 0xe6, 0x28, 0x4f, 0x84, 0x8a, 0xbc, 0xf0, 0x2c, 0xae, 0xb1, 0x13, 0x0f, 0x3b,
|
||||||
0x97, 0xc5, 0x47, 0xfa, 0x62, 0x35, 0xbd, 0xab, 0x56, 0x93, 0x75, 0x96, 0x67, 0x43, 0x25, 0x2b,
|
0x7e, 0x7c, 0xa4, 0x2f, 0x56, 0xd3, 0xbb, 0xd2, 0x9b, 0xa8, 0xb3, 0x22, 0x1b, 0xaa, 0xf0, 0x00,
|
||||||
0xc0, 0xb7, 0xc0, 0x9a, 0x4f, 0x30, 0xa3, 0xae, 0xac, 0xe7, 0x66, 0x9a, 0x1d, 0x24, 0xad, 0x48,
|
0xdf, 0x02, 0x6b, 0x1e, 0xc1, 0x3e, 0x75, 0x44, 0x3d, 0xb7, 0xb3, 0xec, 0x20, 0x21, 0x45, 0x52,
|
||||||
0x79, 0xe1, 0xdb, 0x60, 0x7d, 0x46, 0x18, 0xc3, 0x53, 0x22, 0x15, 0xa7, 0x69, 0x6c, 0x2b, 0xe0,
|
0x0b, 0xdf, 0x06, 0xeb, 0x36, 0xf1, 0x7d, 0x3c, 0x27, 0xa2, 0xe3, 0xb4, 0xb5, 0x6d, 0x09, 0x5c,
|
||||||
0xfa, 0x71, 0x6c, 0x46, 0x73, 0x7f, 0xef, 0xf7, 0x35, 0xb0, 0xfe, 0x6a, 0xba, 0xdf, 0x77, 0xf3,
|
0x3f, 0x8e, 0xc5, 0x28, 0xd1, 0x0f, 0x7e, 0xdb, 0x00, 0xeb, 0xaf, 0x66, 0xfa, 0x7d, 0xa7, 0x38,
|
||||||
0xdd, 0xaf, 0x53, 0x55, 0x79, 0x15, 0x9d, 0xef, 0xb7, 0x0d, 0xb9, 0x51, 0xd9, 0xf5, 0xee, 0x81,
|
0xfd, 0x7a, 0x75, 0x95, 0x57, 0x33, 0xf9, 0xfe, 0xd8, 0x12, 0x81, 0x8a, 0xa9, 0x77, 0x0f, 0x74,
|
||||||
0x96, 0x87, 0x7d, 0xec, 0x38, 0xc4, 0xb1, 0xd9, 0x4c, 0xee, 0xb5, 0x6e, 0x6c, 0x0b, 0x5d, 0x1e,
|
0x5c, 0xec, 0x61, 0xcb, 0x22, 0x96, 0xe9, 0xdb, 0x22, 0xd6, 0xa6, 0xb6, 0xcd, 0xfb, 0xf2, 0x38,
|
||||||
0xa6, 0x66, 0x94, 0xc5, 0x88, 0x10, 0x93, 0xce, 0x3c, 0x87, 0x88, 0xcb, 0x8c, 0xcb, 0x4d, 0x85,
|
0x13, 0xa3, 0x3c, 0x86, 0x9b, 0xe8, 0xd4, 0x76, 0x2d, 0xc2, 0x2f, 0x33, 0x2e, 0x37, 0x69, 0x72,
|
||||||
0x1c, 0xa6, 0x66, 0x94, 0xc5, 0xc0, 0x27, 0xe0, 0x56, 0xac, 0x60, 0xc5, 0x0e, 0xb8, 0x22, 0x3b,
|
0x98, 0x89, 0x51, 0x1e, 0x03, 0x9f, 0x80, 0x5b, 0x71, 0x07, 0x2b, 0x4f, 0xc0, 0x15, 0x31, 0x01,
|
||||||
0xe0, 0x57, 0xa2, 0x50, 0xbb, 0xf5, 0xa0, 0x0c, 0x80, 0xca, 0xe3, 0xe0, 0x14, 0xb4, 0x3d, 0x6a,
|
0xbf, 0x14, 0x85, 0xca, 0xad, 0x07, 0x55, 0x00, 0x54, 0x6d, 0x07, 0xe7, 0xa0, 0xeb, 0x52, 0x83,
|
||||||
0x09, 0x71, 0x0e, 0x7c, 0xa2, 0x9a, 0x5f, 0x4b, 0xde, 0xf3, 0x9b, 0xa5, 0x97, 0x31, 0x2c, 0x80,
|
0x37, 0xe7, 0xc0, 0x23, 0x72, 0xf8, 0x75, 0xc4, 0x3d, 0xbf, 0x59, 0x79, 0x19, 0xe3, 0x12, 0x38,
|
||||||
0x63, 0x0d, 0x2c, 0x5a, 0xd1, 0x02, 0x29, 0xbc, 0x0f, 0x36, 0xc6, 0xd8, 0x7c, 0x46, 0x27, 0x93,
|
0xee, 0x81, 0x65, 0x29, 0x5a, 0x20, 0x85, 0xf7, 0xc1, 0xc6, 0x14, 0xeb, 0xcf, 0xe8, 0x6c, 0x96,
|
||||||
0x6c, 0x6b, 0x68, 0x47, 0xa1, 0xb6, 0x61, 0x64, 0xec, 0x28, 0x87, 0x82, 0x03, 0xb0, 0x93, 0xfd,
|
0x1f, 0x0d, 0xdd, 0x28, 0x54, 0x36, 0xb4, 0x9c, 0x1c, 0x15, 0x50, 0x70, 0x04, 0x76, 0xf2, 0xdf,
|
||||||
0x1e, 0x12, 0xff, 0xb1, 0x6b, 0x91, 0xe7, 0x9d, 0x0d, 0x19, 0xdd, 0x89, 0x42, 0x6d, 0xc7, 0x28,
|
0x63, 0xe2, 0x3d, 0x76, 0x0c, 0xf2, 0xbc, 0xb7, 0x21, 0xac, 0x7b, 0x51, 0xa8, 0xec, 0x68, 0x15,
|
||||||
0xf1, 0xa3, 0xd2, 0x28, 0xf8, 0x01, 0x68, 0xcf, 0xf0, 0xf3, 0xb8, 0x13, 0x49, 0x0b, 0x61, 0x9d,
|
0x7a, 0x54, 0x69, 0x05, 0x3f, 0x00, 0x5d, 0x1b, 0x3f, 0x8f, 0x27, 0x91, 0x90, 0x10, 0xbf, 0xb7,
|
||||||
0x4d, 0xc9, 0x24, 0x4f, 0x71, 0x5c, 0xf0, 0xa1, 0x05, 0x34, 0xfc, 0x19, 0x68, 0x30, 0xe2, 0x10,
|
0x29, 0x98, 0xc4, 0x29, 0x8e, 0x4b, 0x3a, 0xb4, 0x80, 0x86, 0x3f, 0x05, 0x2d, 0x9f, 0x58, 0x44,
|
||||||
0x93, 0x53, 0x5f, 0xbd, 0xad, 0xf7, 0xae, 0x59, 0x8e, 0x78, 0x4c, 0x9c, 0x91, 0x0a, 0x8d, 0x47,
|
0x67, 0xd4, 0x93, 0x6f, 0xeb, 0xbd, 0x6b, 0x96, 0x23, 0x9e, 0x12, 0x6b, 0x22, 0x4d, 0xe3, 0x15,
|
||||||
0x9c, 0xf9, 0x17, 0x4a, 0x28, 0xe1, 0xb7, 0xc1, 0xd6, 0x0c, 0xbb, 0x01, 0x4e, 0x90, 0xf2, 0x51,
|
0x27, 0xf9, 0x42, 0x29, 0x25, 0xfc, 0x16, 0xd8, 0xb2, 0xb1, 0x13, 0xe0, 0x14, 0x29, 0x1e, 0x55,
|
||||||
0x35, 0x0c, 0x18, 0x85, 0xda, 0xd6, 0x71, 0xce, 0x83, 0x0a, 0x48, 0xf8, 0x23, 0xd0, 0xe0, 0xf3,
|
0x4b, 0x83, 0x51, 0xa8, 0x6c, 0x1d, 0x17, 0x34, 0xa8, 0x84, 0x84, 0x3f, 0x04, 0x2d, 0x96, 0xec,
|
||||||
0xf9, 0x61, 0x4d, 0x6e, 0xad, 0xb4, 0x43, 0x0e, 0xa9, 0x95, 0x1b, 0x1f, 0x92, 0xe7, 0x91, 0xcc,
|
0x0f, 0x6b, 0x22, 0xb4, 0xca, 0x09, 0x39, 0xa6, 0x46, 0x61, 0x7d, 0x48, 0x9f, 0x47, 0xba, 0x3b,
|
||||||
0x0e, 0x09, 0x8d, 0x98, 0xb8, 0x38, 0x77, 0x54, 0xa9, 0x3c, 0x98, 0x70, 0xe2, 0x3f, 0xb2, 0x5d,
|
0xa4, 0x34, 0x7c, 0xe3, 0x62, 0xcc, 0x92, 0xa5, 0xf2, 0x60, 0xc6, 0x88, 0xf7, 0xc8, 0x74, 0x4c,
|
||||||
0x9b, 0x9d, 0x12, 0x4b, 0x8e, 0x6a, 0xf5, 0x78, 0xe2, 0x3a, 0x39, 0x19, 0x94, 0x41, 0x50, 0x55,
|
0xff, 0x94, 0x18, 0x62, 0x55, 0x6b, 0xc6, 0x1b, 0xd7, 0xc9, 0xc9, 0xa8, 0x0a, 0x82, 0xea, 0x6c,
|
||||||
0x2c, 0x1c, 0x80, 0xad, 0xb4, 0xa6, 0x8f, 0xa9, 0x45, 0x3a, 0x4d, 0xa9, 0x08, 0x6f, 0x88, 0x53,
|
0xe1, 0x08, 0x6c, 0x65, 0x35, 0x7d, 0x4c, 0x0d, 0xd2, 0x6b, 0x8b, 0x8e, 0xf0, 0x06, 0x3f, 0xe5,
|
||||||
0x1e, 0xe6, 0x3c, 0x97, 0x0b, 0x16, 0x54, 0x88, 0xcd, 0x4e, 0x58, 0xe0, 0x8a, 0x09, 0xcb, 0x02,
|
0x61, 0x41, 0x73, 0xb9, 0x20, 0x41, 0x25, 0xdb, 0xfc, 0x86, 0x05, 0xae, 0xd8, 0xb0, 0x0c, 0xb0,
|
||||||
0x3b, 0x1e, 0xb5, 0x10, 0xf1, 0x1c, 0x6c, 0x92, 0x19, 0x71, 0xb9, 0x2a, 0xf6, 0x2d, 0xb9, 0xf4,
|
0xe3, 0x52, 0x03, 0x11, 0xd7, 0xc2, 0x3a, 0xb1, 0x89, 0xc3, 0x64, 0xb1, 0x6f, 0x09, 0xd7, 0xef,
|
||||||
0xbb, 0xa2, 0x92, 0x86, 0x25, 0xfe, 0xcb, 0x0a, 0x3b, 0x2a, 0x65, 0xeb, 0xfd, 0xa7, 0x0e, 0x9a,
|
0xf2, 0x4a, 0x1a, 0x57, 0xe8, 0x2f, 0x6b, 0xe4, 0xa8, 0x92, 0x0d, 0x7e, 0x15, 0xb4, 0x6d, 0xec,
|
||||||
0xe9, 0xc8, 0xf2, 0x14, 0x00, 0x73, 0xde, 0x17, 0x98, 0x1a, 0x5b, 0x5e, 0xaf, 0xd2, 0x98, 0xa4,
|
0xe0, 0x39, 0x31, 0xb4, 0xf3, 0xde, 0xb6, 0xa0, 0xde, 0x8c, 0x42, 0xa5, 0x7d, 0x9c, 0x08, 0x51,
|
||||||
0x83, 0xa4, 0xed, 0x36, 0x31, 0x31, 0x94, 0x21, 0x82, 0x3f, 0x01, 0x4d, 0x39, 0xcc, 0x4a, 0x85,
|
0xa6, 0x1f, 0xfc, 0xbb, 0x09, 0xda, 0xd9, 0x7e, 0xf3, 0x14, 0x00, 0x3d, 0x19, 0x22, 0xbe, 0xdc,
|
||||||
0x5f, 0x7e, 0x69, 0x85, 0xdf, 0x8c, 0x42, 0xad, 0x39, 0x9a, 0x13, 0xa0, 0x94, 0x0b, 0x4e, 0xb2,
|
0x71, 0x5e, 0xaf, 0x6b, 0x48, 0xe9, 0xb8, 0xc9, 0x66, 0x73, 0x2a, 0xf2, 0x51, 0x8e, 0x08, 0xfe,
|
||||||
0x89, 0xf9, 0x82, 0xdd, 0x0a, 0xe6, 0x93, 0x28, 0x97, 0x28, 0xb0, 0x8a, 0x9e, 0xa1, 0x46, 0xb9,
|
0x18, 0xb4, 0xc5, 0xe6, 0x2b, 0xc6, 0xc1, 0xf2, 0x4b, 0x8f, 0x03, 0x11, 0xfd, 0x24, 0x21, 0x40,
|
||||||
0x55, 0x59, 0x46, 0x55, 0x53, 0x5a, 0x1f, 0x34, 0xe5, 0xd8, 0x49, 0x2c, 0x62, 0xc9, 0x97, 0x50,
|
0x19, 0x17, 0x9c, 0xe5, 0xb3, 0xf8, 0x05, 0x47, 0x1b, 0x2c, 0x66, 0x5c, 0xb8, 0x28, 0xb1, 0xf2,
|
||||||
0x37, 0x6e, 0x28, 0x68, 0x73, 0x34, 0x77, 0xa0, 0x14, 0x23, 0x88, 0xe3, 0x79, 0x52, 0x4d, 0xb5,
|
0x01, 0x23, 0xf7, 0xbe, 0x55, 0x51, 0x73, 0x75, 0x2b, 0xdd, 0x10, 0xb4, 0xc5, 0x8e, 0x4a, 0x0c,
|
||||||
0x09, 0x71, 0xfc, 0x8a, 0x91, 0xf2, 0x0a, 0xe5, 0xe5, 0xc4, 0x9f, 0xd9, 0x2e, 0x16, 0xff, 0x11,
|
0x62, 0x88, 0x67, 0xd3, 0xd4, 0x6e, 0x48, 0x68, 0x7b, 0x92, 0x28, 0x50, 0x86, 0xe1, 0xc4, 0xf1,
|
||||||
0x48, 0xc1, 0x53, 0xca, 0x7b, 0x92, 0x9a, 0x51, 0x16, 0x03, 0x1f, 0x82, 0xb6, 0x3a, 0x45, 0xaa,
|
0xf2, 0x29, 0x57, 0xe0, 0x94, 0x38, 0x7e, 0xf2, 0x48, 0x6a, 0x79, 0x9b, 0x66, 0xc4, 0xb3, 0x4d,
|
||||||
0x1d, 0xeb, 0xb2, 0x76, 0x3a, 0x6a, 0x91, 0xf6, 0x61, 0xc1, 0x8f, 0x16, 0x22, 0xe0, 0xfb, 0x60,
|
0x07, 0xf3, 0x7f, 0x1f, 0x44, 0x77, 0x94, 0x6d, 0xfa, 0x24, 0x13, 0xa3, 0x3c, 0x06, 0x3e, 0x04,
|
||||||
0x73, 0x92, 0x93, 0x1f, 0x20, 0x29, 0x6e, 0x88, 0xf6, 0x9e, 0xd7, 0x9e, 0x3c, 0x0e, 0xfe, 0xa6,
|
0x5d, 0x79, 0x8a, 0xac, 0xd1, 0xac, 0x8b, 0x6a, 0xe8, 0x49, 0x27, 0xdd, 0xc3, 0x92, 0x1e, 0x2d,
|
||||||
0x06, 0xee, 0x04, 0xae, 0x49, 0x03, 0x97, 0x13, 0x6b, 0xbe, 0x49, 0x62, 0x0d, 0xa9, 0xc5, 0xe4,
|
0x58, 0xc0, 0xf7, 0xc1, 0xe6, 0xac, 0xd0, 0xab, 0x80, 0xa0, 0xb8, 0xc1, 0x77, 0x81, 0x62, 0xa3,
|
||||||
0x5b, 0x6c, 0x1d, 0xbc, 0x53, 0x5a, 0x58, 0x4f, 0xcb, 0x63, 0xe2, 0x97, 0x5b, 0xe1, 0x44, 0x55,
|
0x2a, 0xe2, 0xe0, 0xaf, 0x1b, 0xe0, 0x4e, 0xe0, 0xe8, 0x34, 0x70, 0x18, 0x31, 0x92, 0x20, 0x89,
|
||||||
0x2b, 0x41, 0x0d, 0xd4, 0x7d, 0x82, 0xad, 0x73, 0xf9, 0x60, 0xeb, 0x46, 0x53, 0x74, 0x44, 0x24,
|
0x31, 0xa6, 0x86, 0x2f, 0x1e, 0x6e, 0xe7, 0xe0, 0x9d, 0xca, 0xc2, 0x7a, 0x5a, 0x6d, 0x13, 0x3f,
|
||||||
0x0c, 0x28, 0xb6, 0xf7, 0xfe, 0x58, 0x03, 0xdb, 0x85, 0x7f, 0x50, 0xbe, 0xfc, 0x13, 0x68, 0x6f,
|
0xf3, 0x1a, 0x25, 0xaa, 0xf3, 0x04, 0x15, 0xd0, 0xf4, 0x08, 0x36, 0xce, 0xc5, 0xeb, 0x6e, 0x6a,
|
||||||
0x0c, 0x16, 0x3a, 0x18, 0xfc, 0x08, 0xd4, 0xfd, 0xc0, 0x21, 0xf3, 0x67, 0xfb, 0xf6, 0xb5, 0xba,
|
0x6d, 0x3e, 0x3e, 0x11, 0x17, 0xa0, 0x58, 0x3e, 0xf8, 0x7d, 0x03, 0x6c, 0x97, 0xfe, 0x9b, 0xf9,
|
||||||
0x21, 0x0a, 0x1c, 0x92, 0xce, 0x0a, 0xe2, 0x8b, 0xa1, 0x98, 0xa6, 0xf7, 0xf7, 0x1a, 0x78, 0xab,
|
0xff, 0x5f, 0x57, 0x07, 0x53, 0xb0, 0x30, 0xee, 0xe0, 0x47, 0xa0, 0xe9, 0x05, 0x16, 0x49, 0x9e,
|
||||||
0x08, 0x7f, 0xe2, 0x7e, 0xff, 0xb9, 0xcd, 0x0f, 0xa9, 0x45, 0x18, 0x22, 0xbf, 0x0c, 0x6c, 0x5f,
|
0xed, 0xdb, 0xd7, 0x1a, 0x9d, 0x28, 0xb0, 0x48, 0xb6, 0x58, 0xf0, 0x2f, 0x1f, 0xc5, 0x34, 0x83,
|
||||||
0x4a, 0x89, 0x28, 0x12, 0x93, 0xba, 0x1c, 0x8b, 0x6b, 0xf9, 0x08, 0xcf, 0xe6, 0x03, 0xac, 0x2c,
|
0xbf, 0x35, 0xc0, 0x5b, 0x65, 0xf8, 0x13, 0xe7, 0x7b, 0xcf, 0x4d, 0x76, 0x48, 0x0d, 0xe2, 0x23,
|
||||||
0x92, 0xc3, 0xac, 0x03, 0xe5, 0x71, 0x70, 0x04, 0x1a, 0xd4, 0x23, 0x3e, 0x16, 0x8d, 0x23, 0x1e,
|
0xf2, 0x8b, 0xc0, 0xf4, 0x44, 0xdf, 0xe1, 0x45, 0xa2, 0x53, 0x87, 0x61, 0x7e, 0x2d, 0x1f, 0x61,
|
||||||
0x5e, 0xdf, 0x9f, 0xab, 0xfb, 0x13, 0x65, 0xbf, 0x0c, 0xb5, 0xbb, 0x57, 0x6c, 0x63, 0x0e, 0x43,
|
0x3b, 0xd9, 0x76, 0x45, 0x91, 0x1c, 0xe6, 0x15, 0xa8, 0x88, 0x83, 0x13, 0xd0, 0xa2, 0x2e, 0xf1,
|
||||||
0x09, 0x11, 0xec, 0x81, 0xb5, 0x33, 0xec, 0x04, 0x44, 0xcc, 0x18, 0x2b, 0xfb, 0x75, 0x03, 0x88,
|
0x30, 0x9f, 0x32, 0xf1, 0xa6, 0xfb, 0x7e, 0x32, 0x0a, 0x9e, 0x48, 0xf9, 0x65, 0xa8, 0xdc, 0xbd,
|
||||||
0xf7, 0xf4, 0x63, 0x69, 0x41, 0xca, 0xd3, 0xfb, 0x4b, 0xe9, 0xe1, 0x86, 0xd4, 0x4a, 0x15, 0x6c,
|
0x22, 0x8c, 0x04, 0x86, 0x52, 0x22, 0x38, 0x00, 0x6b, 0x67, 0xd8, 0x0a, 0x08, 0x5f, 0x48, 0x56,
|
||||||
0x88, 0x39, 0x27, 0xbe, 0x0b, 0x3f, 0xcc, 0x0d, 0xe5, 0xef, 0x15, 0x86, 0xf2, 0xbb, 0x25, 0xa3,
|
0xf6, 0x9b, 0x1a, 0xe0, 0xef, 0xe9, 0x47, 0x42, 0x82, 0xa4, 0x66, 0xf0, 0xe7, 0xca, 0xc3, 0x8d,
|
||||||
0x75, 0x96, 0xe6, 0xff, 0x35, 0xa7, 0xf7, 0x2e, 0x96, 0xc1, 0x4e, 0x59, 0x36, 0xe1, 0x07, 0xb1,
|
0xa9, 0x91, 0x75, 0xb0, 0x31, 0x66, 0x8c, 0x78, 0x0e, 0xfc, 0xb0, 0xb0, 0xc1, 0xbf, 0x57, 0xda,
|
||||||
0x56, 0x51, 0x57, 0xed, 0x78, 0x3f, 0xab, 0x55, 0xd4, 0xbd, 0x0c, 0xb5, 0xdb, 0xc5, 0xb8, 0xd8,
|
0xe0, 0xef, 0x56, 0xec, 0xe1, 0x79, 0x9a, 0xff, 0xd5, 0x52, 0x3f, 0xb8, 0x58, 0x06, 0x3b, 0x55,
|
||||||
0x83, 0x54, 0x1c, 0x74, 0x41, 0x8b, 0xa6, 0x37, 0xac, 0x8a, 0xf4, 0x3b, 0xd7, 0xaa, 0xa7, 0xf2,
|
0xd9, 0x84, 0x1f, 0xc4, 0xbd, 0x8a, 0x3a, 0x32, 0xe2, 0xfd, 0x7c, 0xaf, 0xa2, 0xce, 0x65, 0xa8,
|
||||||
0x02, 0x89, 0x95, 0x2a, 0xeb, 0xcb, 0x2e, 0x00, 0x7f, 0x05, 0xb6, 0x69, 0xfe, 0xee, 0x65, 0xe6,
|
0xdc, 0x2e, 0xdb, 0xc5, 0x1a, 0x24, 0xed, 0xa0, 0x03, 0x3a, 0x34, 0xbb, 0x61, 0x59, 0xa4, 0xdf,
|
||||||
0xae, 0xbf, 0x66, 0x59, 0xde, 0x8c, 0x3b, 0xea, 0xdc, 0xdb, 0x05, 0x3f, 0x2a, 0x2e, 0xd6, 0xfb,
|
0xbe, 0x56, 0x3d, 0x55, 0x17, 0x48, 0xdc, 0xa9, 0xf2, 0xba, 0xbc, 0x03, 0xf8, 0x4b, 0xb0, 0x4d,
|
||||||
0x53, 0x0d, 0x54, 0x29, 0x0b, 0x1c, 0x66, 0x15, 0x5d, 0xbc, 0xac, 0xa6, 0x71, 0x90, 0x53, 0xf3,
|
0x8b, 0x77, 0x2f, 0x32, 0x77, 0x7d, 0x9f, 0x55, 0x79, 0xd3, 0xee, 0xc8, 0x73, 0x6f, 0x97, 0xf4,
|
||||||
0xcb, 0x50, 0x7b, 0xbd, 0xea, 0x67, 0x43, 0x91, 0x76, 0xa6, 0x3f, 0x7d, 0xfc, 0x30, 0x2b, 0xf9,
|
0xa8, 0xec, 0x6c, 0xf0, 0x87, 0x06, 0xa8, 0xeb, 0x2c, 0x70, 0x9c, 0xef, 0xe8, 0xfc, 0x65, 0xb5,
|
||||||
0x1f, 0x26, 0x92, 0xbf, 0x2c, 0xe9, 0xfa, 0xa9, 0xdc, 0x5f, 0x8f, 0x4b, 0x85, 0x1b, 0xdf, 0xba,
|
0xb5, 0x83, 0x42, 0x37, 0xbf, 0x0c, 0x95, 0xd7, 0xeb, 0x7e, 0x63, 0xe4, 0x69, 0xf7, 0xd5, 0xa7,
|
||||||
0x78, 0xd1, 0x5d, 0xfa, 0xec, 0x45, 0x77, 0xe9, 0xf3, 0x17, 0xdd, 0xa5, 0x5f, 0x47, 0xdd, 0xda,
|
0x8f, 0x1f, 0xe6, 0x5b, 0xfe, 0x87, 0x69, 0xcb, 0x5f, 0x16, 0x74, 0xc3, 0xac, 0xdd, 0x5f, 0x8f,
|
||||||
0x45, 0xd4, 0xad, 0x7d, 0x16, 0x75, 0x6b, 0x9f, 0x47, 0xdd, 0xda, 0x3f, 0xa2, 0x6e, 0xed, 0x77,
|
0x4b, 0x9a, 0x6b, 0xdf, 0xbc, 0x78, 0xd1, 0x5f, 0xfa, 0xec, 0x45, 0x7f, 0xe9, 0xf3, 0x17, 0xfd,
|
||||||
0xff, 0xec, 0x2e, 0x7d, 0x7c, 0xb3, 0xe4, 0x77, 0xdc, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0x6d,
|
0xa5, 0x5f, 0x45, 0xfd, 0xc6, 0x45, 0xd4, 0x6f, 0x7c, 0x16, 0xf5, 0x1b, 0x9f, 0x47, 0xfd, 0xc6,
|
||||||
0xdd, 0x3b, 0x38, 0xdd, 0x15, 0x00, 0x00,
|
0xdf, 0xa3, 0x7e, 0xe3, 0x37, 0xff, 0xe8, 0x2f, 0x7d, 0x7c, 0xb3, 0xe2, 0x47, 0xdf, 0xff, 0x04,
|
||||||
|
0x00, 0x00, 0xff, 0xff, 0xe0, 0x48, 0x1b, 0x03, 0x0a, 0x16, 0x00, 0x00,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CronJob) Marshal() (dAtA []byte, err error) {
|
func (m *CronJob) Marshal() (dAtA []byte, err error) {
|
||||||
@@ -1029,6 +1030,13 @@ func (m *JobSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|||||||
_ = i
|
_ = i
|
||||||
var l int
|
var l int
|
||||||
_ = l
|
_ = l
|
||||||
|
if m.ManagedBy != nil {
|
||||||
|
i -= len(*m.ManagedBy)
|
||||||
|
copy(dAtA[i:], *m.ManagedBy)
|
||||||
|
i = encodeVarintGenerated(dAtA, i, uint64(len(*m.ManagedBy)))
|
||||||
|
i--
|
||||||
|
dAtA[i] = 0x7a
|
||||||
|
}
|
||||||
if m.PodReplacementPolicy != nil {
|
if m.PodReplacementPolicy != nil {
|
||||||
i -= len(*m.PodReplacementPolicy)
|
i -= len(*m.PodReplacementPolicy)
|
||||||
copy(dAtA[i:], *m.PodReplacementPolicy)
|
copy(dAtA[i:], *m.PodReplacementPolicy)
|
||||||
@@ -1690,6 +1698,10 @@ func (m *JobSpec) Size() (n int) {
|
|||||||
l = len(*m.PodReplacementPolicy)
|
l = len(*m.PodReplacementPolicy)
|
||||||
n += 1 + l + sovGenerated(uint64(l))
|
n += 1 + l + sovGenerated(uint64(l))
|
||||||
}
|
}
|
||||||
|
if m.ManagedBy != nil {
|
||||||
|
l = len(*m.ManagedBy)
|
||||||
|
n += 1 + l + sovGenerated(uint64(l))
|
||||||
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1968,6 +1980,7 @@ func (this *JobSpec) String() string {
|
|||||||
`BackoffLimitPerIndex:` + valueToStringGenerated(this.BackoffLimitPerIndex) + `,`,
|
`BackoffLimitPerIndex:` + valueToStringGenerated(this.BackoffLimitPerIndex) + `,`,
|
||||||
`MaxFailedIndexes:` + valueToStringGenerated(this.MaxFailedIndexes) + `,`,
|
`MaxFailedIndexes:` + valueToStringGenerated(this.MaxFailedIndexes) + `,`,
|
||||||
`PodReplacementPolicy:` + valueToStringGenerated(this.PodReplacementPolicy) + `,`,
|
`PodReplacementPolicy:` + valueToStringGenerated(this.PodReplacementPolicy) + `,`,
|
||||||
|
`ManagedBy:` + valueToStringGenerated(this.ManagedBy) + `,`,
|
||||||
`}`,
|
`}`,
|
||||||
}, "")
|
}, "")
|
||||||
return s
|
return s
|
||||||
@@ -3657,6 +3670,39 @@ func (m *JobSpec) Unmarshal(dAtA []byte) error {
|
|||||||
s := PodReplacementPolicy(dAtA[iNdEx:postIndex])
|
s := PodReplacementPolicy(dAtA[iNdEx:postIndex])
|
||||||
m.PodReplacementPolicy = &s
|
m.PodReplacementPolicy = &s
|
||||||
iNdEx = postIndex
|
iNdEx = postIndex
|
||||||
|
case 15:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field ManagedBy", wireType)
|
||||||
|
}
|
||||||
|
var stringLen uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowGenerated
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
stringLen |= uint64(b&0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intStringLen := int(stringLen)
|
||||||
|
if intStringLen < 0 {
|
||||||
|
return ErrInvalidLengthGenerated
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + intStringLen
|
||||||
|
if postIndex < 0 {
|
||||||
|
return ErrInvalidLengthGenerated
|
||||||
|
}
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
s := string(dAtA[iNdEx:postIndex])
|
||||||
|
m.ManagedBy = &s
|
||||||
|
iNdEx = postIndex
|
||||||
default:
|
default:
|
||||||
iNdEx = preIndex
|
iNdEx = preIndex
|
||||||
skippy, err := skipGenerated(dAtA[iNdEx:])
|
skippy, err := skipGenerated(dAtA[iNdEx:])
|
||||||
|
@@ -330,6 +330,20 @@ message JobSpec {
|
|||||||
// This is on by default.
|
// This is on by default.
|
||||||
// +optional
|
// +optional
|
||||||
optional string podReplacementPolicy = 14;
|
optional string podReplacementPolicy = 14;
|
||||||
|
|
||||||
|
// ManagedBy field indicates the controller that manages a Job. The k8s Job
|
||||||
|
// controller reconciles jobs which don't have this field at all or the field
|
||||||
|
// value is the reserved string `kubernetes.io/job-controller`, but skips
|
||||||
|
// reconciling Jobs with a custom value for this field.
|
||||||
|
// The value must be a valid domain-prefixed path (e.g. acme.io/foo) -
|
||||||
|
// all characters before the first "/" must be a valid subdomain as defined
|
||||||
|
// by RFC 1123. All characters trailing the first "/" must be valid HTTP Path
|
||||||
|
// characters as defined by RFC 3986. The value cannot exceed 64 characters.
|
||||||
|
//
|
||||||
|
// This field is alpha-level. The job controller accepts setting the field
|
||||||
|
// when the feature gate JobManagedBy is enabled (disabled by default).
|
||||||
|
// +optional
|
||||||
|
optional string managedBy = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobStatus represents the current state of a Job.
|
// JobStatus represents the current state of a Job.
|
||||||
@@ -340,6 +354,13 @@ message JobStatus {
|
|||||||
// status true; when the Job is resumed, the status of this condition will
|
// status true; when the Job is resumed, the status of this condition will
|
||||||
// become false. When a Job is completed, one of the conditions will have
|
// become false. When a Job is completed, one of the conditions will have
|
||||||
// type "Complete" and status true.
|
// type "Complete" and status true.
|
||||||
|
//
|
||||||
|
// A job is considered finished when it is in a terminal condition, either
|
||||||
|
// "Complete" or "Failed". At that point, all pods of the job are in terminal
|
||||||
|
// phase. Job cannot be both in the "Complete" and "Failed" conditions.
|
||||||
|
// Additionally, it cannot be in the "Complete" and "FailureTarget" conditions.
|
||||||
|
// The "Complete", "Failed" and "FailureTarget" conditions cannot be disabled.
|
||||||
|
//
|
||||||
// More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
|
// More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
|
||||||
// +optional
|
// +optional
|
||||||
// +patchMergeKey=type
|
// +patchMergeKey=type
|
||||||
@@ -351,31 +372,42 @@ message JobStatus {
|
|||||||
// Job is created in the suspended state, this field is not set until the
|
// Job is created in the suspended state, this field is not set until the
|
||||||
// first time it is resumed. This field is reset every time a Job is resumed
|
// first time it is resumed. This field is reset every time a Job is resumed
|
||||||
// from suspension. It is represented in RFC3339 form and is in UTC.
|
// from suspension. It is represented in RFC3339 form and is in UTC.
|
||||||
|
//
|
||||||
|
// Once set, the field can only be removed when the job is suspended.
|
||||||
|
// The field cannot be modified while the job is unsuspended or finished.
|
||||||
|
//
|
||||||
// +optional
|
// +optional
|
||||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time startTime = 2;
|
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time startTime = 2;
|
||||||
|
|
||||||
// Represents time when the job was completed. It is not guaranteed to
|
// Represents time when the job was completed. It is not guaranteed to
|
||||||
// be set in happens-before order across separate operations.
|
// be set in happens-before order across separate operations.
|
||||||
// It is represented in RFC3339 form and is in UTC.
|
// It is represented in RFC3339 form and is in UTC.
|
||||||
// The completion time is only set when the job finishes successfully.
|
// The completion time is set when the job finishes successfully, and only then.
|
||||||
|
// The value cannot be updated or removed. The value indicates the same or
|
||||||
|
// later point in time as the startTime field.
|
||||||
// +optional
|
// +optional
|
||||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time completionTime = 3;
|
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time completionTime = 3;
|
||||||
|
|
||||||
// The number of pending and running pods which are not terminating (without
|
// The number of pending and running pods which are not terminating (without
|
||||||
// a deletionTimestamp).
|
// a deletionTimestamp).
|
||||||
|
// The value is zero for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
optional int32 active = 4;
|
optional int32 active = 4;
|
||||||
|
|
||||||
// The number of pods which reached phase Succeeded.
|
// The number of pods which reached phase Succeeded.
|
||||||
|
// The value increases monotonically for a given spec. However, it may
|
||||||
|
// decrease in reaction to scale down of elastic indexed jobs.
|
||||||
// +optional
|
// +optional
|
||||||
optional int32 succeeded = 5;
|
optional int32 succeeded = 5;
|
||||||
|
|
||||||
// The number of pods which reached phase Failed.
|
// The number of pods which reached phase Failed.
|
||||||
|
// The value increases monotonically.
|
||||||
// +optional
|
// +optional
|
||||||
optional int32 failed = 6;
|
optional int32 failed = 6;
|
||||||
|
|
||||||
// The number of pods which are terminating (in phase Pending or Running
|
// The number of pods which are terminating (in phase Pending or Running
|
||||||
// and have a deletionTimestamp).
|
// and have a deletionTimestamp).
|
||||||
|
// The value is zero (or null) for finished jobs.
|
||||||
//
|
//
|
||||||
// This field is beta-level. The job controller populates the field when
|
// This field is beta-level. The job controller populates the field when
|
||||||
// the feature gate JobPodReplacementPolicy is enabled (enabled by default).
|
// the feature gate JobPodReplacementPolicy is enabled (enabled by default).
|
||||||
@@ -392,7 +424,7 @@ message JobStatus {
|
|||||||
// +optional
|
// +optional
|
||||||
optional string completedIndexes = 7;
|
optional string completedIndexes = 7;
|
||||||
|
|
||||||
// FailedIndexes holds the failed indexes when backoffLimitPerIndex=true.
|
// FailedIndexes holds the failed indexes when spec.backoffLimitPerIndex is set.
|
||||||
// The indexes are represented in the text format analogous as for the
|
// The indexes are represented in the text format analogous as for the
|
||||||
// `completedIndexes` field, ie. they are kept as decimal integers
|
// `completedIndexes` field, ie. they are kept as decimal integers
|
||||||
// separated by commas. The numbers are listed in increasing order. Three or
|
// separated by commas. The numbers are listed in increasing order. Three or
|
||||||
@@ -400,6 +432,8 @@ message JobStatus {
|
|||||||
// last element of the series, separated by a hyphen.
|
// last element of the series, separated by a hyphen.
|
||||||
// For example, if the failed indexes are 1, 3, 4, 5 and 7, they are
|
// For example, if the failed indexes are 1, 3, 4, 5 and 7, they are
|
||||||
// represented as "1,3-5,7".
|
// represented as "1,3-5,7".
|
||||||
|
// The set of failed indexes cannot overlap with the set of completed indexes.
|
||||||
|
//
|
||||||
// This field is beta-level. It can be used when the `JobBackoffLimitPerIndex`
|
// This field is beta-level. It can be used when the `JobBackoffLimitPerIndex`
|
||||||
// feature gate is enabled (enabled by default).
|
// feature gate is enabled (enabled by default).
|
||||||
// +optional
|
// +optional
|
||||||
@@ -419,10 +453,12 @@ message JobStatus {
|
|||||||
//
|
//
|
||||||
// Old jobs might not be tracked using this field, in which case the field
|
// Old jobs might not be tracked using this field, in which case the field
|
||||||
// remains null.
|
// remains null.
|
||||||
|
// The structure is empty for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
optional UncountedTerminatedPods uncountedTerminatedPods = 8;
|
optional UncountedTerminatedPods uncountedTerminatedPods = 8;
|
||||||
|
|
||||||
// The number of pods which have a Ready condition.
|
// The number of pods which have a Ready condition.
|
||||||
|
// The value is zero (or null) for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
optional int32 ready = 9;
|
optional int32 ready = 9;
|
||||||
}
|
}
|
||||||
|
@@ -57,6 +57,9 @@ const (
|
|||||||
// to the pod, which don't count towards the backoff limit, according to the
|
// to the pod, which don't count towards the backoff limit, according to the
|
||||||
// pod failure policy. When the annotation is absent zero is implied.
|
// pod failure policy. When the annotation is absent zero is implied.
|
||||||
JobIndexIgnoredFailureCountAnnotation = labelPrefix + "job-index-ignored-failure-count"
|
JobIndexIgnoredFailureCountAnnotation = labelPrefix + "job-index-ignored-failure-count"
|
||||||
|
// JobControllerName reserved value for the managedBy field for the built-in
|
||||||
|
// Job controller.
|
||||||
|
JobControllerName = "kubernetes.io/job-controller"
|
||||||
)
|
)
|
||||||
|
|
||||||
// +genclient
|
// +genclient
|
||||||
@@ -410,6 +413,20 @@ type JobSpec struct {
|
|||||||
// This is on by default.
|
// This is on by default.
|
||||||
// +optional
|
// +optional
|
||||||
PodReplacementPolicy *PodReplacementPolicy `json:"podReplacementPolicy,omitempty" protobuf:"bytes,14,opt,name=podReplacementPolicy,casttype=podReplacementPolicy"`
|
PodReplacementPolicy *PodReplacementPolicy `json:"podReplacementPolicy,omitempty" protobuf:"bytes,14,opt,name=podReplacementPolicy,casttype=podReplacementPolicy"`
|
||||||
|
|
||||||
|
// ManagedBy field indicates the controller that manages a Job. The k8s Job
|
||||||
|
// controller reconciles jobs which don't have this field at all or the field
|
||||||
|
// value is the reserved string `kubernetes.io/job-controller`, but skips
|
||||||
|
// reconciling Jobs with a custom value for this field.
|
||||||
|
// The value must be a valid domain-prefixed path (e.g. acme.io/foo) -
|
||||||
|
// all characters before the first "/" must be a valid subdomain as defined
|
||||||
|
// by RFC 1123. All characters trailing the first "/" must be valid HTTP Path
|
||||||
|
// characters as defined by RFC 3986. The value cannot exceed 64 characters.
|
||||||
|
//
|
||||||
|
// This field is alpha-level. The job controller accepts setting the field
|
||||||
|
// when the feature gate JobManagedBy is enabled (disabled by default).
|
||||||
|
// +optional
|
||||||
|
ManagedBy *string `json:"managedBy,omitempty" protobuf:"bytes,15,opt,name=managedBy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobStatus represents the current state of a Job.
|
// JobStatus represents the current state of a Job.
|
||||||
@@ -420,6 +437,13 @@ type JobStatus struct {
|
|||||||
// status true; when the Job is resumed, the status of this condition will
|
// status true; when the Job is resumed, the status of this condition will
|
||||||
// become false. When a Job is completed, one of the conditions will have
|
// become false. When a Job is completed, one of the conditions will have
|
||||||
// type "Complete" and status true.
|
// type "Complete" and status true.
|
||||||
|
//
|
||||||
|
// A job is considered finished when it is in a terminal condition, either
|
||||||
|
// "Complete" or "Failed". At that point, all pods of the job are in terminal
|
||||||
|
// phase. Job cannot be both in the "Complete" and "Failed" conditions.
|
||||||
|
// Additionally, it cannot be in the "Complete" and "FailureTarget" conditions.
|
||||||
|
// The "Complete", "Failed" and "FailureTarget" conditions cannot be disabled.
|
||||||
|
//
|
||||||
// More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
|
// More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/
|
||||||
// +optional
|
// +optional
|
||||||
// +patchMergeKey=type
|
// +patchMergeKey=type
|
||||||
@@ -431,31 +455,42 @@ type JobStatus struct {
|
|||||||
// Job is created in the suspended state, this field is not set until the
|
// Job is created in the suspended state, this field is not set until the
|
||||||
// first time it is resumed. This field is reset every time a Job is resumed
|
// first time it is resumed. This field is reset every time a Job is resumed
|
||||||
// from suspension. It is represented in RFC3339 form and is in UTC.
|
// from suspension. It is represented in RFC3339 form and is in UTC.
|
||||||
|
//
|
||||||
|
// Once set, the field can only be removed when the job is suspended.
|
||||||
|
// The field cannot be modified while the job is unsuspended or finished.
|
||||||
|
//
|
||||||
// +optional
|
// +optional
|
||||||
StartTime *metav1.Time `json:"startTime,omitempty" protobuf:"bytes,2,opt,name=startTime"`
|
StartTime *metav1.Time `json:"startTime,omitempty" protobuf:"bytes,2,opt,name=startTime"`
|
||||||
|
|
||||||
// Represents time when the job was completed. It is not guaranteed to
|
// Represents time when the job was completed. It is not guaranteed to
|
||||||
// be set in happens-before order across separate operations.
|
// be set in happens-before order across separate operations.
|
||||||
// It is represented in RFC3339 form and is in UTC.
|
// It is represented in RFC3339 form and is in UTC.
|
||||||
// The completion time is only set when the job finishes successfully.
|
// The completion time is set when the job finishes successfully, and only then.
|
||||||
|
// The value cannot be updated or removed. The value indicates the same or
|
||||||
|
// later point in time as the startTime field.
|
||||||
// +optional
|
// +optional
|
||||||
CompletionTime *metav1.Time `json:"completionTime,omitempty" protobuf:"bytes,3,opt,name=completionTime"`
|
CompletionTime *metav1.Time `json:"completionTime,omitempty" protobuf:"bytes,3,opt,name=completionTime"`
|
||||||
|
|
||||||
// The number of pending and running pods which are not terminating (without
|
// The number of pending and running pods which are not terminating (without
|
||||||
// a deletionTimestamp).
|
// a deletionTimestamp).
|
||||||
|
// The value is zero for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
Active int32 `json:"active,omitempty" protobuf:"varint,4,opt,name=active"`
|
Active int32 `json:"active,omitempty" protobuf:"varint,4,opt,name=active"`
|
||||||
|
|
||||||
// The number of pods which reached phase Succeeded.
|
// The number of pods which reached phase Succeeded.
|
||||||
|
// The value increases monotonically for a given spec. However, it may
|
||||||
|
// decrease in reaction to scale down of elastic indexed jobs.
|
||||||
// +optional
|
// +optional
|
||||||
Succeeded int32 `json:"succeeded,omitempty" protobuf:"varint,5,opt,name=succeeded"`
|
Succeeded int32 `json:"succeeded,omitempty" protobuf:"varint,5,opt,name=succeeded"`
|
||||||
|
|
||||||
// The number of pods which reached phase Failed.
|
// The number of pods which reached phase Failed.
|
||||||
|
// The value increases monotonically.
|
||||||
// +optional
|
// +optional
|
||||||
Failed int32 `json:"failed,omitempty" protobuf:"varint,6,opt,name=failed"`
|
Failed int32 `json:"failed,omitempty" protobuf:"varint,6,opt,name=failed"`
|
||||||
|
|
||||||
// The number of pods which are terminating (in phase Pending or Running
|
// The number of pods which are terminating (in phase Pending or Running
|
||||||
// and have a deletionTimestamp).
|
// and have a deletionTimestamp).
|
||||||
|
// The value is zero (or null) for finished jobs.
|
||||||
//
|
//
|
||||||
// This field is beta-level. The job controller populates the field when
|
// This field is beta-level. The job controller populates the field when
|
||||||
// the feature gate JobPodReplacementPolicy is enabled (enabled by default).
|
// the feature gate JobPodReplacementPolicy is enabled (enabled by default).
|
||||||
@@ -472,7 +507,7 @@ type JobStatus struct {
|
|||||||
// +optional
|
// +optional
|
||||||
CompletedIndexes string `json:"completedIndexes,omitempty" protobuf:"bytes,7,opt,name=completedIndexes"`
|
CompletedIndexes string `json:"completedIndexes,omitempty" protobuf:"bytes,7,opt,name=completedIndexes"`
|
||||||
|
|
||||||
// FailedIndexes holds the failed indexes when backoffLimitPerIndex=true.
|
// FailedIndexes holds the failed indexes when spec.backoffLimitPerIndex is set.
|
||||||
// The indexes are represented in the text format analogous as for the
|
// The indexes are represented in the text format analogous as for the
|
||||||
// `completedIndexes` field, ie. they are kept as decimal integers
|
// `completedIndexes` field, ie. they are kept as decimal integers
|
||||||
// separated by commas. The numbers are listed in increasing order. Three or
|
// separated by commas. The numbers are listed in increasing order. Three or
|
||||||
@@ -480,6 +515,8 @@ type JobStatus struct {
|
|||||||
// last element of the series, separated by a hyphen.
|
// last element of the series, separated by a hyphen.
|
||||||
// For example, if the failed indexes are 1, 3, 4, 5 and 7, they are
|
// For example, if the failed indexes are 1, 3, 4, 5 and 7, they are
|
||||||
// represented as "1,3-5,7".
|
// represented as "1,3-5,7".
|
||||||
|
// The set of failed indexes cannot overlap with the set of completed indexes.
|
||||||
|
//
|
||||||
// This field is beta-level. It can be used when the `JobBackoffLimitPerIndex`
|
// This field is beta-level. It can be used when the `JobBackoffLimitPerIndex`
|
||||||
// feature gate is enabled (enabled by default).
|
// feature gate is enabled (enabled by default).
|
||||||
// +optional
|
// +optional
|
||||||
@@ -499,10 +536,12 @@ type JobStatus struct {
|
|||||||
//
|
//
|
||||||
// Old jobs might not be tracked using this field, in which case the field
|
// Old jobs might not be tracked using this field, in which case the field
|
||||||
// remains null.
|
// remains null.
|
||||||
|
// The structure is empty for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
UncountedTerminatedPods *UncountedTerminatedPods `json:"uncountedTerminatedPods,omitempty" protobuf:"bytes,8,opt,name=uncountedTerminatedPods"`
|
UncountedTerminatedPods *UncountedTerminatedPods `json:"uncountedTerminatedPods,omitempty" protobuf:"bytes,8,opt,name=uncountedTerminatedPods"`
|
||||||
|
|
||||||
// The number of pods which have a Ready condition.
|
// The number of pods which have a Ready condition.
|
||||||
|
// The value is zero (or null) for finished jobs.
|
||||||
// +optional
|
// +optional
|
||||||
Ready *int32 `json:"ready,omitempty" protobuf:"varint,9,opt,name=ready"`
|
Ready *int32 `json:"ready,omitempty" protobuf:"varint,9,opt,name=ready"`
|
||||||
}
|
}
|
||||||
|
@@ -126,6 +126,7 @@ var map_JobSpec = map[string]string{
|
|||||||
"completionMode": "completionMode specifies how Pod completions are tracked. It can be `NonIndexed` (default) or `Indexed`.\n\n`NonIndexed` means that the Job is considered complete when there have been .spec.completions successfully completed Pods. Each Pod completion is homologous to each other.\n\n`Indexed` means that the Pods of a Job get an associated completion index from 0 to (.spec.completions - 1), available in the annotation batch.kubernetes.io/job-completion-index. The Job is considered complete when there is one successfully completed Pod for each index. When value is `Indexed`, .spec.completions must be specified and `.spec.parallelism` must be less than or equal to 10^5. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nMore completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller skips updates for the Job.",
|
"completionMode": "completionMode specifies how Pod completions are tracked. It can be `NonIndexed` (default) or `Indexed`.\n\n`NonIndexed` means that the Job is considered complete when there have been .spec.completions successfully completed Pods. Each Pod completion is homologous to each other.\n\n`Indexed` means that the Pods of a Job get an associated completion index from 0 to (.spec.completions - 1), available in the annotation batch.kubernetes.io/job-completion-index. The Job is considered complete when there is one successfully completed Pod for each index. When value is `Indexed`, .spec.completions must be specified and `.spec.parallelism` must be less than or equal to 10^5. In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`.\n\nMore completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller skips updates for the Job.",
|
||||||
"suspend": "suspend specifies whether the Job controller should create Pods or not. If a Job is created with suspend set to true, no Pods are created by the Job controller. If a Job is suspended after creation (i.e. the flag goes from false to true), the Job controller will delete all active Pods associated with this Job. Users must design their workload to gracefully handle this. Suspending a Job will reset the StartTime field of the Job, effectively resetting the ActiveDeadlineSeconds timer too. Defaults to false.",
|
"suspend": "suspend specifies whether the Job controller should create Pods or not. If a Job is created with suspend set to true, no Pods are created by the Job controller. If a Job is suspended after creation (i.e. the flag goes from false to true), the Job controller will delete all active Pods associated with this Job. Users must design their workload to gracefully handle this. Suspending a Job will reset the StartTime field of the Job, effectively resetting the ActiveDeadlineSeconds timer too. Defaults to false.",
|
||||||
"podReplacementPolicy": "podReplacementPolicy specifies when to create replacement Pods. Possible values are: - TerminatingOrFailed means that we recreate pods\n when they are terminating (has a metadata.deletionTimestamp) or failed.\n- Failed means to wait until a previously created Pod is fully terminated (has phase\n Failed or Succeeded) before creating a replacement Pod.\n\nWhen using podFailurePolicy, Failed is the the only allowed value. TerminatingOrFailed and Failed are allowed values when podFailurePolicy is not in use. This is an beta field. To use this, enable the JobPodReplacementPolicy feature toggle. This is on by default.",
|
"podReplacementPolicy": "podReplacementPolicy specifies when to create replacement Pods. Possible values are: - TerminatingOrFailed means that we recreate pods\n when they are terminating (has a metadata.deletionTimestamp) or failed.\n- Failed means to wait until a previously created Pod is fully terminated (has phase\n Failed or Succeeded) before creating a replacement Pod.\n\nWhen using podFailurePolicy, Failed is the the only allowed value. TerminatingOrFailed and Failed are allowed values when podFailurePolicy is not in use. This is an beta field. To use this, enable the JobPodReplacementPolicy feature toggle. This is on by default.",
|
||||||
|
"managedBy": "ManagedBy field indicates the controller that manages a Job. The k8s Job controller reconciles jobs which don't have this field at all or the field value is the reserved string `kubernetes.io/job-controller`, but skips reconciling Jobs with a custom value for this field. The value must be a valid domain-prefixed path (e.g. acme.io/foo) - all characters before the first \"/\" must be a valid subdomain as defined by RFC 1123. All characters trailing the first \"/\" must be valid HTTP Path characters as defined by RFC 3986. The value cannot exceed 64 characters.\n\nThis field is alpha-level. The job controller accepts setting the field when the feature gate JobManagedBy is enabled (disabled by default).",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (JobSpec) SwaggerDoc() map[string]string {
|
func (JobSpec) SwaggerDoc() map[string]string {
|
||||||
@@ -134,17 +135,17 @@ func (JobSpec) SwaggerDoc() map[string]string {
|
|||||||
|
|
||||||
var map_JobStatus = map[string]string{
|
var map_JobStatus = map[string]string{
|
||||||
"": "JobStatus represents the current state of a Job.",
|
"": "JobStatus represents the current state of a Job.",
|
||||||
"conditions": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
|
"conditions": "The latest available observations of an object's current state. When a Job fails, one of the conditions will have type \"Failed\" and status true. When a Job is suspended, one of the conditions will have type \"Suspended\" and status true; when the Job is resumed, the status of this condition will become false. When a Job is completed, one of the conditions will have type \"Complete\" and status true.\n\nA job is considered finished when it is in a terminal condition, either \"Complete\" or \"Failed\". At that point, all pods of the job are in terminal phase. Job cannot be both in the \"Complete\" and \"Failed\" conditions. Additionally, it cannot be in the \"Complete\" and \"FailureTarget\" conditions. The \"Complete\", \"Failed\" and \"FailureTarget\" conditions cannot be disabled.\n\nMore info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
|
||||||
"startTime": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC.",
|
"startTime": "Represents time when the job controller started processing a job. When a Job is created in the suspended state, this field is not set until the first time it is resumed. This field is reset every time a Job is resumed from suspension. It is represented in RFC3339 form and is in UTC.\n\nOnce set, the field can only be removed when the job is suspended. The field cannot be modified while the job is unsuspended or finished.",
|
||||||
"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. The completion time is only set when the job finishes successfully.",
|
"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. The completion time is set when the job finishes successfully, and only then. The value cannot be updated or removed. The value indicates the same or later point in time as the startTime field.",
|
||||||
"active": "The number of pending and running pods which are not terminating (without a deletionTimestamp).",
|
"active": "The number of pending and running pods which are not terminating (without a deletionTimestamp). The value is zero for finished jobs.",
|
||||||
"succeeded": "The number of pods which reached phase Succeeded.",
|
"succeeded": "The number of pods which reached phase Succeeded. The value increases monotonically for a given spec. However, it may decrease in reaction to scale down of elastic indexed jobs.",
|
||||||
"failed": "The number of pods which reached phase Failed.",
|
"failed": "The number of pods which reached phase Failed. The value increases monotonically.",
|
||||||
"terminating": "The number of pods which are terminating (in phase Pending or Running and have a deletionTimestamp).\n\nThis field is beta-level. The job controller populates the field when the feature gate JobPodReplacementPolicy is enabled (enabled by default).",
|
"terminating": "The number of pods which are terminating (in phase Pending or Running and have a deletionTimestamp). The value is zero (or null) for finished jobs.\n\nThis field is beta-level. The job controller populates the field when the feature gate JobPodReplacementPolicy is enabled (enabled by default).",
|
||||||
"completedIndexes": "completedIndexes holds the completed indexes when .spec.completionMode = \"Indexed\" in a text format. The indexes are represented as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the completed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\".",
|
"completedIndexes": "completedIndexes holds the completed indexes when .spec.completionMode = \"Indexed\" in a text format. The indexes are represented as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the completed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\".",
|
||||||
"failedIndexes": "FailedIndexes holds the failed indexes when backoffLimitPerIndex=true. The indexes are represented in the text format analogous as for the `completedIndexes` field, ie. they are kept as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the failed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\". This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` feature gate is enabled (enabled by default).",
|
"failedIndexes": "FailedIndexes holds the failed indexes when spec.backoffLimitPerIndex is set. The indexes are represented in the text format analogous as for the `completedIndexes` field, ie. they are kept as decimal integers separated by commas. The numbers are listed in increasing order. Three or more consecutive numbers are compressed and represented by the first and last element of the series, separated by a hyphen. For example, if the failed indexes are 1, 3, 4, 5 and 7, they are represented as \"1,3-5,7\". The set of failed indexes cannot overlap with the set of completed indexes.\n\nThis field is beta-level. It can be used when the `JobBackoffLimitPerIndex` feature gate is enabled (enabled by default).",
|
||||||
"uncountedTerminatedPods": "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null.",
|
"uncountedTerminatedPods": "uncountedTerminatedPods holds the UIDs of Pods that have terminated but the job controller hasn't yet accounted for in the status counters.\n\nThe job controller creates pods with a finalizer. When a pod terminates (succeeded or failed), the controller does three steps to account for it in the job status:\n\n1. Add the pod UID to the arrays in this field. 2. Remove the pod finalizer. 3. Remove the pod UID from the arrays while increasing the corresponding\n counter.\n\nOld jobs might not be tracked using this field, in which case the field remains null. The structure is empty for finished jobs.",
|
||||||
"ready": "The number of pods which have a Ready condition.",
|
"ready": "The number of pods which have a Ready condition. The value is zero (or null) for finished jobs.",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (JobStatus) SwaggerDoc() map[string]string {
|
func (JobStatus) SwaggerDoc() map[string]string {
|
||||||
|
@@ -308,6 +308,11 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) {
|
|||||||
*out = new(PodReplacementPolicy)
|
*out = new(PodReplacementPolicy)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.ManagedBy != nil {
|
||||||
|
in, out := &in.ManagedBy, &out.ManagedBy
|
||||||
|
*out = new(string)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1822,7 +1822,8 @@
|
|||||||
"ttlSecondsAfterFinished": 8,
|
"ttlSecondsAfterFinished": 8,
|
||||||
"completionMode": "completionModeValue",
|
"completionMode": "completionModeValue",
|
||||||
"suspend": true,
|
"suspend": true,
|
||||||
"podReplacementPolicy": "podReplacementPolicyValue"
|
"podReplacementPolicy": "podReplacementPolicyValue",
|
||||||
|
"managedBy": "managedByValue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"successfulJobsHistoryLimit": 6,
|
"successfulJobsHistoryLimit": 6,
|
||||||
|
Binary file not shown.
@@ -74,6 +74,7 @@ spec:
|
|||||||
backoffLimitPerIndex: 12
|
backoffLimitPerIndex: 12
|
||||||
completionMode: completionModeValue
|
completionMode: completionModeValue
|
||||||
completions: 2
|
completions: 2
|
||||||
|
managedBy: managedByValue
|
||||||
manualSelector: true
|
manualSelector: true
|
||||||
maxFailedIndexes: 13
|
maxFailedIndexes: 13
|
||||||
parallelism: 1
|
parallelism: 1
|
||||||
|
@@ -1773,7 +1773,8 @@
|
|||||||
"ttlSecondsAfterFinished": 8,
|
"ttlSecondsAfterFinished": 8,
|
||||||
"completionMode": "completionModeValue",
|
"completionMode": "completionModeValue",
|
||||||
"suspend": true,
|
"suspend": true,
|
||||||
"podReplacementPolicy": "podReplacementPolicyValue"
|
"podReplacementPolicy": "podReplacementPolicyValue",
|
||||||
|
"managedBy": "managedByValue"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"conditions": [
|
"conditions": [
|
||||||
|
BIN
staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.pb
vendored
BIN
staging/src/k8s.io/api/testdata/HEAD/batch.v1.Job.pb
vendored
Binary file not shown.
@@ -38,6 +38,7 @@ spec:
|
|||||||
backoffLimitPerIndex: 12
|
backoffLimitPerIndex: 12
|
||||||
completionMode: completionModeValue
|
completionMode: completionModeValue
|
||||||
completions: 2
|
completions: 2
|
||||||
|
managedBy: managedByValue
|
||||||
manualSelector: true
|
manualSelector: true
|
||||||
maxFailedIndexes: 13
|
maxFailedIndexes: 13
|
||||||
parallelism: 1
|
parallelism: 1
|
||||||
|
@@ -1822,7 +1822,8 @@
|
|||||||
"ttlSecondsAfterFinished": 8,
|
"ttlSecondsAfterFinished": 8,
|
||||||
"completionMode": "completionModeValue",
|
"completionMode": "completionModeValue",
|
||||||
"suspend": true,
|
"suspend": true,
|
||||||
"podReplacementPolicy": "podReplacementPolicyValue"
|
"podReplacementPolicy": "podReplacementPolicyValue",
|
||||||
|
"managedBy": "managedByValue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"successfulJobsHistoryLimit": 6,
|
"successfulJobsHistoryLimit": 6,
|
||||||
|
Binary file not shown.
@@ -74,6 +74,7 @@ spec:
|
|||||||
backoffLimitPerIndex: 12
|
backoffLimitPerIndex: 12
|
||||||
completionMode: completionModeValue
|
completionMode: completionModeValue
|
||||||
completions: 2
|
completions: 2
|
||||||
|
managedBy: managedByValue
|
||||||
manualSelector: true
|
manualSelector: true
|
||||||
maxFailedIndexes: 13
|
maxFailedIndexes: 13
|
||||||
parallelism: 1
|
parallelism: 1
|
||||||
|
@@ -41,6 +41,7 @@ type JobSpecApplyConfiguration struct {
|
|||||||
CompletionMode *batchv1.CompletionMode `json:"completionMode,omitempty"`
|
CompletionMode *batchv1.CompletionMode `json:"completionMode,omitempty"`
|
||||||
Suspend *bool `json:"suspend,omitempty"`
|
Suspend *bool `json:"suspend,omitempty"`
|
||||||
PodReplacementPolicy *batchv1.PodReplacementPolicy `json:"podReplacementPolicy,omitempty"`
|
PodReplacementPolicy *batchv1.PodReplacementPolicy `json:"podReplacementPolicy,omitempty"`
|
||||||
|
ManagedBy *string `json:"managedBy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobSpecApplyConfiguration constructs an declarative configuration of the JobSpec type for use with
|
// JobSpecApplyConfiguration constructs an declarative configuration of the JobSpec type for use with
|
||||||
@@ -160,3 +161,11 @@ func (b *JobSpecApplyConfiguration) WithPodReplacementPolicy(value batchv1.PodRe
|
|||||||
b.PodReplacementPolicy = &value
|
b.PodReplacementPolicy = &value
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithManagedBy sets the ManagedBy field in the declarative configuration to the given value
|
||||||
|
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||||
|
// If called multiple times, the ManagedBy field is set to the value of the last call.
|
||||||
|
func (b *JobSpecApplyConfiguration) WithManagedBy(value string) *JobSpecApplyConfiguration {
|
||||||
|
b.ManagedBy = &value
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
@@ -3599,6 +3599,9 @@ var schemaYAML = typed.YAMLObject(`types:
|
|||||||
- name: completions
|
- name: completions
|
||||||
type:
|
type:
|
||||||
scalar: numeric
|
scalar: numeric
|
||||||
|
- name: managedBy
|
||||||
|
type:
|
||||||
|
scalar: string
|
||||||
- name: manualSelector
|
- name: manualSelector
|
||||||
type:
|
type:
|
||||||
scalar: boolean
|
scalar: boolean
|
||||||
|
@@ -1174,6 +1174,335 @@ func TestBackoffLimitPerIndex(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestManagedBy verifies the Job controller correctly makes a decision to
|
||||||
|
// reconcile or skip reconciliation of the Job depending on the Job's managedBy
|
||||||
|
// field, and the enablement of the JobManagedBy feature gate.
|
||||||
|
func TestManagedBy(t *testing.T) {
|
||||||
|
customControllerName := "example.com/custom-job-controller"
|
||||||
|
podTemplateSpec := v1.PodTemplateSpec{
|
||||||
|
Spec: v1.PodSpec{
|
||||||
|
Containers: []v1.Container{
|
||||||
|
{
|
||||||
|
Name: "main-container",
|
||||||
|
Image: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testCases := map[string]struct {
|
||||||
|
enableJobManagedBy bool
|
||||||
|
job batchv1.Job
|
||||||
|
wantReconciledByBuiltInController bool
|
||||||
|
wantJobByExternalControllerTotalMetric metricLabelsWithValue
|
||||||
|
}{
|
||||||
|
"the Job controller reconciles jobs without the managedBy": {
|
||||||
|
enableJobManagedBy: true,
|
||||||
|
job: batchv1.Job{
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Template: podTemplateSpec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReconciledByBuiltInController: true,
|
||||||
|
wantJobByExternalControllerTotalMetric: metricLabelsWithValue{
|
||||||
|
// There is no good label value choice to check here, since the
|
||||||
|
// values wasn't specified. Let's go with checking for the reserved
|
||||||
|
// value just so that all test cases verify the metric.
|
||||||
|
Labels: []string{batchv1.JobControllerName},
|
||||||
|
Value: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"the Job controller reconciles jobs with the well known value of the managedBy field": {
|
||||||
|
enableJobManagedBy: true,
|
||||||
|
job: batchv1.Job{
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Template: podTemplateSpec,
|
||||||
|
ManagedBy: ptr.To(batchv1.JobControllerName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReconciledByBuiltInController: true,
|
||||||
|
wantJobByExternalControllerTotalMetric: metricLabelsWithValue{
|
||||||
|
Labels: []string{batchv1.JobControllerName},
|
||||||
|
Value: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"the Job controller reconciles an unsuspended with the custom value of managedBy; feature disabled": {
|
||||||
|
enableJobManagedBy: false,
|
||||||
|
job: batchv1.Job{
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Template: podTemplateSpec,
|
||||||
|
ManagedBy: ptr.To(customControllerName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReconciledByBuiltInController: true,
|
||||||
|
wantJobByExternalControllerTotalMetric: metricLabelsWithValue{
|
||||||
|
Labels: []string{customControllerName},
|
||||||
|
Value: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"the Job controller does not reconcile an unsuspended with the custom value of managedBy": {
|
||||||
|
enableJobManagedBy: true,
|
||||||
|
job: batchv1.Job{
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Suspend: ptr.To(false),
|
||||||
|
Template: podTemplateSpec,
|
||||||
|
ManagedBy: ptr.To(customControllerName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReconciledByBuiltInController: false,
|
||||||
|
wantJobByExternalControllerTotalMetric: metricLabelsWithValue{
|
||||||
|
Labels: []string{customControllerName},
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"the Job controller does not reconcile a suspended with the custom value of managedBy": {
|
||||||
|
enableJobManagedBy: true,
|
||||||
|
job: batchv1.Job{
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Suspend: ptr.To(true),
|
||||||
|
Template: podTemplateSpec,
|
||||||
|
ManagedBy: ptr.To(customControllerName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantReconciledByBuiltInController: false,
|
||||||
|
wantJobByExternalControllerTotalMetric: metricLabelsWithValue{
|
||||||
|
Labels: []string{customControllerName},
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, test := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
resetMetrics()
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobManagedBy, test.enableJobManagedBy)()
|
||||||
|
|
||||||
|
closeFn, restConfig, clientSet, ns := setup(t, "managed-by")
|
||||||
|
defer closeFn()
|
||||||
|
ctx, cancel := startJobControllerAndWaitForCaches(t, restConfig)
|
||||||
|
defer cancel()
|
||||||
|
jobObj, err := createJobWithDefaults(ctx, clientSet, ns.Name, &test.job)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error %v while creating the job %q", err, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.wantReconciledByBuiltInController {
|
||||||
|
validateJobPodsStatus(ctx, t, clientSet, jobObj, podsByStatus{
|
||||||
|
Active: int(*jobObj.Spec.Parallelism),
|
||||||
|
Ready: ptr.To[int32](0),
|
||||||
|
Terminating: ptr.To[int32](0),
|
||||||
|
})
|
||||||
|
validateCounterMetric(ctx, t, metrics.JobByExternalControllerTotal, test.wantJobByExternalControllerTotalMetric)
|
||||||
|
} else {
|
||||||
|
validateCounterMetric(ctx, t, metrics.JobByExternalControllerTotal, test.wantJobByExternalControllerTotalMetric)
|
||||||
|
|
||||||
|
// Await for a little bit to verify the reconciliation does not
|
||||||
|
// happen. We wait 100ms for the sync itself, because we already
|
||||||
|
// checked the metric is incremented so the sync would start
|
||||||
|
// immediately if it was queued.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
jobObj, err = clientSet.BatchV1().Jobs(jobObj.Namespace).Get(ctx, jobObj.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error %v when getting the latest job %v", err, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(batchv1.JobStatus{}, jobObj.Status); diff != "" {
|
||||||
|
t.Fatalf("Unexpected status (-want/+got): %s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagedBy_Reenabling verifies handling a Job with a custom value of the
|
||||||
|
// managedBy field by the Job controller, as the JobManagedBy feature gate is
|
||||||
|
// disabled and reenabled again. First, when the feature gate is enabled, the
|
||||||
|
// synchronization is skipped, when it is disabled the synchronization is starts,
|
||||||
|
// and is disabled again with re-enabling of the feature gate.
|
||||||
|
func TestManagedBy_Reenabling(t *testing.T) {
|
||||||
|
customControllerName := "example.com/custom-job-controller"
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobManagedBy, true)()
|
||||||
|
|
||||||
|
closeFn, restConfig, clientSet, ns := setup(t, "managed-by-reenabling")
|
||||||
|
defer closeFn()
|
||||||
|
ctx, cancel := startJobControllerAndWaitForCaches(t, restConfig)
|
||||||
|
defer func() {
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
resetMetrics()
|
||||||
|
|
||||||
|
baseJob := batchv1.Job{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "custom-job-test",
|
||||||
|
Namespace: ns.Name,
|
||||||
|
},
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Completions: ptr.To[int32](1),
|
||||||
|
Parallelism: ptr.To[int32](1),
|
||||||
|
Template: v1.PodTemplateSpec{
|
||||||
|
Spec: v1.PodSpec{
|
||||||
|
Containers: []v1.Container{
|
||||||
|
{
|
||||||
|
Name: "main-container",
|
||||||
|
Image: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ManagedBy: &customControllerName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jobObj, err := createJobWithDefaults(ctx, clientSet, ns.Name, &baseJob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error %v when creating the job %q", err, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
jobClient := clientSet.BatchV1().Jobs(jobObj.Namespace)
|
||||||
|
|
||||||
|
validateCounterMetric(ctx, t, metrics.JobByExternalControllerTotal, metricLabelsWithValue{
|
||||||
|
Labels: []string{customControllerName},
|
||||||
|
Value: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Await for a little bit to verify the reconciliation does not happen.
|
||||||
|
// We wait 1s to account for queued sync delay plus 100ms for the sync itself.
|
||||||
|
time.Sleep(time.Second + 100*time.Millisecond)
|
||||||
|
jobObj, err = jobClient.Get(ctx, jobObj.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error %v when getting the latest job %v", err, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(batchv1.JobStatus{}, jobObj.Status); diff != "" {
|
||||||
|
t.Fatalf("Unexpected status (-want/+got): %s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the feature gate and restart the controller
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobManagedBy, false)()
|
||||||
|
cancel()
|
||||||
|
resetMetrics()
|
||||||
|
ctx, cancel = startJobControllerAndWaitForCaches(t, restConfig)
|
||||||
|
|
||||||
|
// Verify the built-in controller reconciles the Job
|
||||||
|
validateJobsPodsStatusOnly(ctx, t, clientSet, jobObj, podsByStatus{
|
||||||
|
Active: 1,
|
||||||
|
Ready: ptr.To[int32](0),
|
||||||
|
Terminating: ptr.To[int32](0),
|
||||||
|
})
|
||||||
|
|
||||||
|
validateCounterMetric(ctx, t, metrics.JobByExternalControllerTotal, metricLabelsWithValue{
|
||||||
|
Labels: []string{customControllerName},
|
||||||
|
Value: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reenable the feature gate and restart the controller
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobManagedBy, true)()
|
||||||
|
cancel()
|
||||||
|
resetMetrics()
|
||||||
|
ctx, cancel = startJobControllerAndWaitForCaches(t, restConfig)
|
||||||
|
|
||||||
|
// Marking the pod as finished, but
|
||||||
|
if err, _ := setJobPodsPhase(ctx, clientSet, jobObj, v1.PodSucceeded, 1); err != nil {
|
||||||
|
t.Fatalf("Error %v when setting phase %s on the pod of job %v", err, v1.PodSucceeded, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Await for a little bit to verify the reconciliation does not happen.
|
||||||
|
// We wait 1s to account for queued sync delay plus 100ms for the sync itself.
|
||||||
|
time.Sleep(time.Second + 100*time.Millisecond)
|
||||||
|
|
||||||
|
validateCounterMetric(ctx, t, metrics.JobByExternalControllerTotal, metricLabelsWithValue{
|
||||||
|
Labels: []string{customControllerName},
|
||||||
|
Value: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify the built-in controller does not reconcile the Job. It is up to
|
||||||
|
// the external controller to update the status.
|
||||||
|
validateJobsPodsStatusOnly(ctx, t, clientSet, jobObj, podsByStatus{
|
||||||
|
Active: 1,
|
||||||
|
Ready: ptr.To[int32](0),
|
||||||
|
Terminating: ptr.To[int32](0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestManagedBy_RecreatedJob verifies that the Job controller skips
|
||||||
|
// reconciliation of a job with managedBy field, when this is a recreated job,
|
||||||
|
// and there is still a pending sync queued for the previous job.
|
||||||
|
// In this scenario we first create a job without managedBy field, and we mark
|
||||||
|
// its pod as succeeded. This queues the Job object sync with 1s delay. Then,
|
||||||
|
// without waiting for the Job status update we delete and recreate the job under
|
||||||
|
// the same name, but with managedBy field. The queued update starts to execute
|
||||||
|
// on the new job, but is skipped.
|
||||||
|
func TestManagedBy_RecreatedJob(t *testing.T) {
|
||||||
|
customControllerName := "example.com/custom-job-controller"
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.JobManagedBy, true)()
|
||||||
|
|
||||||
|
closeFn, restConfig, clientSet, ns := setup(t, "managed-by-recreate-job")
|
||||||
|
defer closeFn()
|
||||||
|
ctx, cancel := startJobControllerAndWaitForCaches(t, restConfig)
|
||||||
|
defer cancel()
|
||||||
|
resetMetrics()
|
||||||
|
|
||||||
|
baseJob := batchv1.Job{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "custom-job-test",
|
||||||
|
Namespace: ns.Name,
|
||||||
|
},
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Completions: ptr.To[int32](1),
|
||||||
|
Parallelism: ptr.To[int32](1),
|
||||||
|
Template: v1.PodTemplateSpec{
|
||||||
|
Spec: v1.PodSpec{
|
||||||
|
Containers: []v1.Container{
|
||||||
|
{
|
||||||
|
Name: "main-container",
|
||||||
|
Image: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jobObj, err := createJobWithDefaults(ctx, clientSet, ns.Name, &baseJob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error %v when creating the job %q", err, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
validateJobPodsStatus(ctx, t, clientSet, jobObj, podsByStatus{
|
||||||
|
Active: 1,
|
||||||
|
Ready: ptr.To[int32](0),
|
||||||
|
Terminating: ptr.To[int32](0),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Marking the pod as complete queues the job reconciliation
|
||||||
|
if err, _ := setJobPodsPhase(ctx, clientSet, jobObj, v1.PodSucceeded, 1); err != nil {
|
||||||
|
t.Fatalf("Error %v when setting phase %s on the pod of job %v", err, v1.PodSucceeded, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
jobClient := clientSet.BatchV1().Jobs(jobObj.Namespace)
|
||||||
|
if err = jobClient.Delete(ctx, jobObj.Name, metav1.DeleteOptions{
|
||||||
|
// Use propagationPolicy=background so that we don't need to wait for the job object to be gone.
|
||||||
|
PropagationPolicy: ptr.To(metav1.DeletePropagationBackground),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Error %v when deleting the job %v", err, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
jobWithManagedBy := baseJob.DeepCopy()
|
||||||
|
jobWithManagedBy.Spec.ManagedBy = ptr.To(customControllerName)
|
||||||
|
jobObj, err = createJobWithDefaults(ctx, clientSet, ns.Name, jobWithManagedBy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error %q while creating the job %q", err, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCounterMetric(ctx, t, metrics.JobByExternalControllerTotal, metricLabelsWithValue{
|
||||||
|
Labels: []string{customControllerName},
|
||||||
|
Value: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Await for a little bit to verify the reconciliation does not happen.
|
||||||
|
// We wait 1s to account for queued sync delay plus 100ms for the sync itself.
|
||||||
|
time.Sleep(time.Second + 100*time.Millisecond)
|
||||||
|
jobObj, err = jobClient.Get(ctx, jobObj.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error %v when getting the latest job %v", err, klog.KObj(jobObj))
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(batchv1.JobStatus{}, jobObj.Status); diff != "" {
|
||||||
|
t.Fatalf("Unexpected status (-want/+got): %s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getIndexFailureCount(p *v1.Pod) (int, error) {
|
func getIndexFailureCount(p *v1.Pod) (int, error) {
|
||||||
if p.Annotations == nil {
|
if p.Annotations == nil {
|
||||||
return 0, errors.New("no annotations found")
|
return 0, errors.New("no annotations found")
|
||||||
@@ -3155,6 +3484,7 @@ func resetMetrics() {
|
|||||||
metrics.PodFailuresHandledByFailurePolicy.Reset()
|
metrics.PodFailuresHandledByFailurePolicy.Reset()
|
||||||
metrics.JobFinishedIndexesTotal.Reset()
|
metrics.JobFinishedIndexesTotal.Reset()
|
||||||
metrics.JobPodsCreationTotal.Reset()
|
metrics.JobPodsCreationTotal.Reset()
|
||||||
|
metrics.JobByExternalControllerTotal.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createJobControllerWithSharedInformers(tb testing.TB, restConfig *restclient.Config, informerSet informers.SharedInformerFactory) (*jobcontroller.Controller, context.Context, context.CancelFunc) {
|
func createJobControllerWithSharedInformers(tb testing.TB, restConfig *restclient.Config, informerSet informers.SharedInformerFactory) (*jobcontroller.Controller, context.Context, context.CancelFunc) {
|
||||||
|
Reference in New Issue
Block a user