Merge pull request #66840 from janetkuo/job-ttl

Automatic merge from submit-queue (batch tested with PRs 66840, 68159). If you want to cherry-pick this change to another branch, please follow the instructions here: https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md.

TTL for cleaning up Jobs after they finish

**What this PR does / why we need it**: https://github.com/kubernetes/features/issues/592

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #64470 
For https://github.com/kubernetes/features/issues/592

**Special notes for your reviewer**: @kubernetes/sig-apps-pr-reviews 

**Release note**:

```release-note
Add a TTL machenism to clean up Jobs after they finish.
```
This commit is contained in:
Kubernetes Submit Queue 2018-09-04 20:49:35 -07:00 committed by GitHub
commit c50a347124
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1209 additions and 87 deletions

View File

@ -126,6 +126,7 @@ API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,K
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,ResourceQuotaController API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,ResourceQuotaController
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,SAController API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,SAController
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,ServiceController API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,ServiceController
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,TTLAfterFinishedController
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,NamespaceControllerConfiguration,NamespaceSyncPeriod API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,NamespaceControllerConfiguration,NamespaceSyncPeriod
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,NamespaceControllerConfiguration,ConcurrentNamespaceSyncs API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,NamespaceControllerConfiguration,ConcurrentNamespaceSyncs
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,NodeIPAMControllerConfiguration,ServiceCIDR API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,NodeIPAMControllerConfiguration,ServiceCIDR
@ -156,6 +157,7 @@ API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,S
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,SAControllerConfiguration,ConcurrentSATokenSyncs API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,SAControllerConfiguration,ConcurrentSATokenSyncs
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,SAControllerConfiguration,RootCAFile API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,SAControllerConfiguration,RootCAFile
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,ServiceControllerConfiguration,ConcurrentServiceSyncs API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,ServiceControllerConfiguration,ConcurrentServiceSyncs
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,TTLAfterFinishedControllerConfiguration,ConcurrentTTLSyncs
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,EnableHostPathProvisioning API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,EnableHostPathProvisioning
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,EnableDynamicProvisioning API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,EnableDynamicProvisioning
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,PersistentVolumeRecyclerConfiguration API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,PersistentVolumeRecyclerConfiguration

View File

@ -79306,6 +79306,11 @@
"template": { "template": {
"description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", "description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"$ref": "#/definitions/io.k8s.api.core.v1.PodTemplateSpec" "$ref": "#/definitions/io.k8s.api.core.v1.PodTemplateSpec"
},
"ttlSecondsAfterFinished": {
"description": "ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won't be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature.",
"type": "integer",
"format": "int32"
} }
} }
}, },

View File

@ -1536,6 +1536,11 @@
"template": { "template": {
"$ref": "v1.PodTemplateSpec", "$ref": "v1.PodTemplateSpec",
"description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/" "description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/"
},
"ttlSecondsAfterFinished": {
"type": "integer",
"format": "int32",
"description": "ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won't be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature."
} }
} }
}, },

View File

@ -1591,6 +1591,11 @@
"template": { "template": {
"$ref": "v1.PodTemplateSpec", "$ref": "v1.PodTemplateSpec",
"description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/" "description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/"
},
"ttlSecondsAfterFinished": {
"type": "integer",
"format": "int32",
"description": "ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won't be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature."
} }
} }
}, },

View File

@ -1591,6 +1591,11 @@
"template": { "template": {
"$ref": "v1.PodTemplateSpec", "$ref": "v1.PodTemplateSpec",
"description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/" "description": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/"
},
"ttlSecondsAfterFinished": {
"type": "integer",
"format": "int32",
"description": "ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won't be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature."
} }
} }
}, },

View File

@ -68,6 +68,7 @@ go_library(
"//pkg/controller/serviceaccount:go_default_library", "//pkg/controller/serviceaccount:go_default_library",
"//pkg/controller/statefulset:go_default_library", "//pkg/controller/statefulset:go_default_library",
"//pkg/controller/ttl:go_default_library", "//pkg/controller/ttl:go_default_library",
"//pkg/controller/ttlafterfinished:go_default_library",
"//pkg/controller/volume/attachdetach:go_default_library", "//pkg/controller/volume/attachdetach:go_default_library",
"//pkg/controller/volume/expand:go_default_library", "//pkg/controller/volume/expand:go_default_library",
"//pkg/controller/volume/persistentvolume:go_default_library", "//pkg/controller/volume/persistentvolume:go_default_library",

View File

@ -378,6 +378,7 @@ func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc
controllers["clusterrole-aggregation"] = startClusterRoleAggregrationController controllers["clusterrole-aggregation"] = startClusterRoleAggregrationController
controllers["pvc-protection"] = startPVCProtectionController controllers["pvc-protection"] = startPVCProtectionController
controllers["pv-protection"] = startPVProtectionController controllers["pv-protection"] = startPVProtectionController
controllers["ttl-after-finished"] = startTTLAfterFinishedController
return controllers return controllers
} }

View File

@ -52,6 +52,7 @@ import (
servicecontroller "k8s.io/kubernetes/pkg/controller/service" servicecontroller "k8s.io/kubernetes/pkg/controller/service"
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount" serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
ttlcontroller "k8s.io/kubernetes/pkg/controller/ttl" ttlcontroller "k8s.io/kubernetes/pkg/controller/ttl"
"k8s.io/kubernetes/pkg/controller/ttlafterfinished"
"k8s.io/kubernetes/pkg/controller/volume/attachdetach" "k8s.io/kubernetes/pkg/controller/volume/attachdetach"
"k8s.io/kubernetes/pkg/controller/volume/expand" "k8s.io/kubernetes/pkg/controller/volume/expand"
persistentvolumecontroller "k8s.io/kubernetes/pkg/controller/volume/persistentvolume" persistentvolumecontroller "k8s.io/kubernetes/pkg/controller/volume/persistentvolume"
@ -417,3 +418,14 @@ func startPVProtectionController(ctx ControllerContext) (http.Handler, bool, err
).Run(1, ctx.Stop) ).Run(1, ctx.Stop)
return nil, true, nil return nil, true, nil
} }
func startTTLAfterFinishedController(ctx ControllerContext) (http.Handler, bool, error) {
if !utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
return nil, false, nil
}
go ttlafterfinished.New(
ctx.InformerFactory.Batch().V1().Jobs(),
ctx.ClientBuilder.ClientOrDie("ttl-after-finished-controller"),
).Run(int(ctx.ComponentConfig.TTLAfterFinishedController.ConcurrentTTLSyncs), ctx.Stop)
return nil, true, nil
}

View File

@ -28,6 +28,7 @@ go_library(
"replicationcontroller.go", "replicationcontroller.go",
"resourcequotacontroller.go", "resourcequotacontroller.go",
"serviceaccountcontroller.go", "serviceaccountcontroller.go",
"ttlafterfinishedcontroller.go",
], ],
importpath = "k8s.io/kubernetes/cmd/kube-controller-manager/app/options", importpath = "k8s.io/kubernetes/cmd/kube-controller-manager/app/options",
deps = [ deps = [

View File

@ -77,6 +77,7 @@ type KubeControllerManagerOptions struct {
ReplicationController *ReplicationControllerOptions ReplicationController *ReplicationControllerOptions
ResourceQuotaController *ResourceQuotaControllerOptions ResourceQuotaController *ResourceQuotaControllerOptions
SAController *SAControllerOptions SAController *SAControllerOptions
TTLAfterFinishedController *TTLAfterFinishedControllerOptions
SecureServing *apiserveroptions.SecureServingOptionsWithLoopback SecureServing *apiserveroptions.SecureServingOptionsWithLoopback
// TODO: remove insecure serving mode // TODO: remove insecure serving mode
@ -172,6 +173,9 @@ func NewKubeControllerManagerOptions() (*KubeControllerManagerOptions, error) {
ServiceController: &cmoptions.ServiceControllerOptions{ ServiceController: &cmoptions.ServiceControllerOptions{
ConcurrentServiceSyncs: componentConfig.ServiceController.ConcurrentServiceSyncs, ConcurrentServiceSyncs: componentConfig.ServiceController.ConcurrentServiceSyncs,
}, },
TTLAfterFinishedController: &TTLAfterFinishedControllerOptions{
ConcurrentTTLSyncs: componentConfig.TTLAfterFinishedController.ConcurrentTTLSyncs,
},
SecureServing: apiserveroptions.NewSecureServingOptions().WithLoopback(), SecureServing: apiserveroptions.NewSecureServingOptions().WithLoopback(),
InsecureServing: (&apiserveroptions.DeprecatedInsecureServingOptions{ InsecureServing: (&apiserveroptions.DeprecatedInsecureServingOptions{
BindAddress: net.ParseIP(componentConfig.Generic.Address), BindAddress: net.ParseIP(componentConfig.Generic.Address),
@ -251,6 +255,7 @@ func (s *KubeControllerManagerOptions) Flags(allControllers []string, disabledBy
s.ReplicationController.AddFlags(fss.FlagSet("replicationcontroller")) s.ReplicationController.AddFlags(fss.FlagSet("replicationcontroller"))
s.ResourceQuotaController.AddFlags(fss.FlagSet("resourcequota controller")) s.ResourceQuotaController.AddFlags(fss.FlagSet("resourcequota controller"))
s.SAController.AddFlags(fss.FlagSet("serviceaccount controller")) s.SAController.AddFlags(fss.FlagSet("serviceaccount controller"))
s.TTLAfterFinishedController.AddFlags(fss.FlagSet("ttl-after-finished controller"))
fs := fss.FlagSet("misc") fs := fss.FlagSet("misc")
fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig).") fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig).")
@ -328,6 +333,9 @@ func (s *KubeControllerManagerOptions) ApplyTo(c *kubecontrollerconfig.Config) e
if err := s.ServiceController.ApplyTo(&c.ComponentConfig.ServiceController); err != nil { if err := s.ServiceController.ApplyTo(&c.ComponentConfig.ServiceController); err != nil {
return err return err
} }
if err := s.TTLAfterFinishedController.ApplyTo(&c.ComponentConfig.TTLAfterFinishedController); err != nil {
return err
}
if err := s.InsecureServing.ApplyTo(&c.InsecureServing, &c.LoopbackClientConfig); err != nil { if err := s.InsecureServing.ApplyTo(&c.InsecureServing, &c.LoopbackClientConfig); err != nil {
return err return err
} }
@ -376,6 +384,7 @@ func (s *KubeControllerManagerOptions) Validate(allControllers []string, disable
errs = append(errs, s.ResourceQuotaController.Validate()...) errs = append(errs, s.ResourceQuotaController.Validate()...)
errs = append(errs, s.SAController.Validate()...) errs = append(errs, s.SAController.Validate()...)
errs = append(errs, s.ServiceController.Validate()...) errs = append(errs, s.ServiceController.Validate()...)
errs = append(errs, s.TTLAfterFinishedController.Validate()...)
errs = append(errs, s.SecureServing.Validate()...) errs = append(errs, s.SecureServing.Validate()...)
errs = append(errs, s.InsecureServing.Validate()...) errs = append(errs, s.InsecureServing.Validate()...)
errs = append(errs, s.Authentication.Validate()...) errs = append(errs, s.Authentication.Validate()...)

View File

@ -116,6 +116,7 @@ func TestAddFlags(t *testing.T) {
"--cert-dir=/a/b/c", "--cert-dir=/a/b/c",
"--bind-address=192.168.4.21", "--bind-address=192.168.4.21",
"--secure-port=10001", "--secure-port=10001",
"--concurrent-ttl-after-finished-syncs=8",
} }
fs.Parse(args) fs.Parse(args)
// Sort GCIgnoredResources because it's built from a map, which means the // Sort GCIgnoredResources because it's built from a map, which means the
@ -255,6 +256,9 @@ func TestAddFlags(t *testing.T) {
ServiceController: &cmoptions.ServiceControllerOptions{ ServiceController: &cmoptions.ServiceControllerOptions{
ConcurrentServiceSyncs: 2, ConcurrentServiceSyncs: 2,
}, },
TTLAfterFinishedController: &TTLAfterFinishedControllerOptions{
ConcurrentTTLSyncs: 8,
},
SecureServing: (&apiserveroptions.SecureServingOptions{ SecureServing: (&apiserveroptions.SecureServingOptions{
BindPort: 10001, BindPort: 10001,
BindAddress: net.ParseIP("192.168.4.21"), BindAddress: net.ParseIP("192.168.4.21"),

View File

@ -0,0 +1,58 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package options
import (
"github.com/spf13/pflag"
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
)
// TTLAfterFinishedControllerOptions holds the TTLAfterFinishedController options.
type TTLAfterFinishedControllerOptions struct {
ConcurrentTTLSyncs int32
}
// AddFlags adds flags related to TTLAfterFinishedController for controller manager to the specified FlagSet.
func (o *TTLAfterFinishedControllerOptions) AddFlags(fs *pflag.FlagSet) {
if o == nil {
return
}
fs.Int32Var(&o.ConcurrentTTLSyncs, "concurrent-ttl-after-finished-syncs", o.ConcurrentTTLSyncs, "The number of TTL-after-finished controller workers that are allowed to sync concurrently.")
}
// ApplyTo fills up TTLAfterFinishedController config with options.
func (o *TTLAfterFinishedControllerOptions) ApplyTo(cfg *kubectrlmgrconfig.TTLAfterFinishedControllerConfiguration) error {
if o == nil {
return nil
}
cfg.ConcurrentTTLSyncs = o.ConcurrentTTLSyncs
return nil
}
// Validate checks validation of TTLAfterFinishedControllerOptions.
func (o *TTLAfterFinishedControllerOptions) Validate() []error {
if o == nil {
return nil
}
errs := []error{}
return errs
}

View File

@ -2505,6 +2505,13 @@ When an object is created, the system will populate this list with the current s
<td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#_v1_podtemplatespec">v1.PodTemplateSpec</a></p></td> <td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#_v1_podtemplatespec">v1.PodTemplateSpec</a></p></td>
<td class="tableblock halign-left valign-top"></td> <td class="tableblock halign-left valign-top"></td>
</tr> </tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">ttlSecondsAfterFinished</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won&#8217;t be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">integer (int32)</p></td>
<td class="tableblock halign-left valign-top"></td>
</tr>
</tbody> </tbody>
</table> </table>

View File

@ -2539,6 +2539,13 @@ When an object is created, the system will populate this list with the current s
<td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#_v1_podtemplatespec">v1.PodTemplateSpec</a></p></td> <td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#_v1_podtemplatespec">v1.PodTemplateSpec</a></p></td>
<td class="tableblock halign-left valign-top"></td> <td class="tableblock halign-left valign-top"></td>
</tr> </tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">ttlSecondsAfterFinished</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won&#8217;t be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">integer (int32)</p></td>
<td class="tableblock halign-left valign-top"></td>
</tr>
</tbody> </tbody>
</table> </table>

View File

@ -2512,6 +2512,13 @@ When an object is created, the system will populate this list with the current s
<td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#_v1_podtemplatespec">v1.PodTemplateSpec</a></p></td> <td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#_v1_podtemplatespec">v1.PodTemplateSpec</a></p></td>
<td class="tableblock halign-left valign-top"></td> <td class="tableblock halign-left valign-top"></td>
</tr> </tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">ttlSecondsAfterFinished</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won&#8217;t be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">false</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">integer (int32)</p></td>
<td class="tableblock halign-left valign-top"></td>
</tr>
</tbody> </tbody>
</table> </table>

View File

@ -138,6 +138,18 @@ type JobSpec struct {
// Describes the pod that will be created when executing a job. // Describes the pod that will be created when executing a job.
Template api.PodTemplateSpec Template api.PodTemplateSpec
// ttlSecondsAfterFinished limits the lifetime of a Job that has finished
// execution (either Complete or Failed). If this field is set,
// ttlSecondsAfterFinished after the Job finishes, it is eligible to be
// automatically deleted. When the Job is being deleted, its lifecycle
// guarantees (e.g. finalizers) will be honored. If this field is unset,
// the Job won't be automatically deleted. If this field is set to zero,
// the Job becomes eligible to be deleted immediately after it finishes.
// This field is alpha-level and is only honored by servers that enable the
// TTLAfterFinished feature.
// +optional
TTLSecondsAfterFinished *int32
} }
// JobStatus represents the current state of a Job. // JobStatus represents the current state of a Job.

View File

@ -54,6 +54,7 @@ func Convert_batch_JobSpec_To_v1_JobSpec(in *batch.JobSpec, out *batchv1.JobSpec
out.Completions = in.Completions out.Completions = in.Completions
out.ActiveDeadlineSeconds = in.ActiveDeadlineSeconds out.ActiveDeadlineSeconds = in.ActiveDeadlineSeconds
out.BackoffLimit = in.BackoffLimit out.BackoffLimit = in.BackoffLimit
out.TTLSecondsAfterFinished = in.TTLSecondsAfterFinished
out.Selector = in.Selector out.Selector = in.Selector
if in.ManualSelector != nil { if in.ManualSelector != nil {
out.ManualSelector = new(bool) out.ManualSelector = new(bool)
@ -73,6 +74,7 @@ func Convert_v1_JobSpec_To_batch_JobSpec(in *batchv1.JobSpec, out *batch.JobSpec
out.Completions = in.Completions out.Completions = in.Completions
out.ActiveDeadlineSeconds = in.ActiveDeadlineSeconds out.ActiveDeadlineSeconds = in.ActiveDeadlineSeconds
out.BackoffLimit = in.BackoffLimit out.BackoffLimit = in.BackoffLimit
out.TTLSecondsAfterFinished = in.TTLSecondsAfterFinished
out.Selector = in.Selector out.Selector = in.Selector
if in.ManualSelector != nil { if in.ManualSelector != nil {
out.ManualSelector = new(bool) out.ManualSelector = new(bool)

View File

@ -217,6 +217,7 @@ func autoConvert_v1_JobSpec_To_batch_JobSpec(in *v1.JobSpec, out *batch.JobSpec,
if err := apiscorev1.Convert_v1_PodTemplateSpec_To_core_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { if err := apiscorev1.Convert_v1_PodTemplateSpec_To_core_PodTemplateSpec(&in.Template, &out.Template, s); err != nil {
return err return err
} }
out.TTLSecondsAfterFinished = (*int32)(unsafe.Pointer(in.TTLSecondsAfterFinished))
return nil return nil
} }
@ -230,6 +231,7 @@ func autoConvert_batch_JobSpec_To_v1_JobSpec(in *batch.JobSpec, out *v1.JobSpec,
if err := apiscorev1.Convert_core_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { if err := apiscorev1.Convert_core_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil {
return err return err
} }
out.TTLSecondsAfterFinished = (*int32)(unsafe.Pointer(in.TTLSecondsAfterFinished))
return nil return nil
} }

View File

@ -14,11 +14,13 @@ go_library(
"//pkg/apis/batch:go_default_library", "//pkg/apis/batch:go_default_library",
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/apis/core/validation:go_default_library", "//pkg/apis/core/validation:go_default_library",
"//pkg/features:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/github.com/robfig/cron:go_default_library", "//vendor/github.com/robfig/cron:go_default_library",
], ],
) )
@ -30,8 +32,10 @@ go_test(
deps = [ deps = [
"//pkg/apis/batch:go_default_library", "//pkg/apis/batch:go_default_library",
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/features:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
], ],
) )

View File

@ -24,9 +24,11 @@ import (
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
apimachineryvalidation "k8s.io/apimachinery/pkg/util/validation" apimachineryvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/batch"
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/kubernetes/pkg/features"
) )
// TODO: generalize for other controller objects that will follow the same pattern, such as ReplicaSet and DaemonSet, and // TODO: generalize for other controller objects that will follow the same pattern, such as ReplicaSet and DaemonSet, and
@ -117,6 +119,14 @@ func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList {
if spec.BackoffLimit != nil { if spec.BackoffLimit != nil {
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.BackoffLimit), fldPath.Child("backoffLimit"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.BackoffLimit), fldPath.Child("backoffLimit"))...)
} }
if utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
// normal validation for TTLSecondsAfterFinished
if spec.TTLSecondsAfterFinished != nil {
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.TTLSecondsAfterFinished), fldPath.Child("ttlSecondsAfterFinished"))...)
}
} else if spec.TTLSecondsAfterFinished != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("ttlSecondsAfterFinished"), "disabled by feature-gate"))
}
allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"))...) allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"))...)
if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure &&

View File

@ -17,13 +17,16 @@ limitations under the License.
package validation package validation
import ( import (
"fmt"
"strings" "strings"
"testing" "testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/batch"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
) )
func getValidManualSelector() *metav1.LabelSelector { func getValidManualSelector() *metav1.LabelSelector {
@ -64,7 +67,21 @@ func getValidPodTemplateSpecForGenerated(selector *metav1.LabelSelector) api.Pod
} }
} }
func featureToggle(feature utilfeature.Feature) []string {
enabled := fmt.Sprintf("%s=%t", feature, true)
disabled := fmt.Sprintf("%s=%t", feature, false)
return []string{enabled, disabled}
}
func TestValidateJob(t *testing.T) { func TestValidateJob(t *testing.T) {
ttlEnabled := utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished)
defer func() {
err := utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("%s=%t", features.TTLAfterFinished, ttlEnabled))
if err != nil {
t.Fatalf("Failed to set feature gate for %s: %v", features.TTLAfterFinished, err)
}
}()
validManualSelector := getValidManualSelector() validManualSelector := getValidManualSelector()
validPodTemplateSpecForManual := getValidPodTemplateSpecForManual(validManualSelector) validPodTemplateSpecForManual := getValidPodTemplateSpecForManual(validManualSelector)
validGeneratedSelector := getValidGeneratedSelector() validGeneratedSelector := getValidGeneratedSelector()
@ -214,15 +231,39 @@ func TestValidateJob(t *testing.T) {
}, },
} }
for k, v := range errorCases { for _, setFeature := range featureToggle(features.TTLAfterFinished) {
errs := ValidateJob(&v) // Set error cases based on if TTLAfterFinished feature is enabled or not
if len(errs) == 0 { if err := utilfeature.DefaultFeatureGate.Set(setFeature); err != nil {
t.Errorf("expected failure for %s", k) t.Fatalf("Failed to set feature gate for %s: %v", features.TTLAfterFinished, err)
}
ttlCase := "spec.ttlSecondsAfterFinished:must be greater than or equal to 0"
if utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
errorCases[ttlCase] = batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.JobSpec{
TTLSecondsAfterFinished: &negative,
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
}
} else { } else {
s := strings.Split(k, ":") delete(errorCases, ttlCase)
err := errs[0] }
if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) {
t.Errorf("unexpected error: %v, expected: %s", err, k) for k, v := range errorCases {
errs := ValidateJob(&v)
if len(errs) == 0 {
t.Errorf("expected failure for %s", k)
} else {
s := strings.Split(k, ":")
err := errs[0]
if err.Field != s[0] || !strings.Contains(err.Error(), s[1]) {
t.Errorf("unexpected error: %v, expected: %s", err, k)
}
} }
} }
} }
@ -584,6 +625,25 @@ func TestValidateCronJob(t *testing.T) {
}, },
}, },
} }
if utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
errorCases["spec.jobTemplate.spec.ttlSecondsAfterFinished:must be greater than or equal to 0"] = batch.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "mycronjob",
Namespace: metav1.NamespaceDefault,
UID: types.UID("1a2b3c"),
},
Spec: batch.CronJobSpec{
Schedule: "* * * * ?",
ConcurrencyPolicy: batch.AllowConcurrent,
JobTemplate: batch.JobTemplateSpec{
Spec: batch.JobSpec{
TTLSecondsAfterFinished: &negative,
Template: validPodTemplateSpec,
},
},
},
}
}
for k, v := range errorCases { for k, v := range errorCases {
errs := ValidateCronJob(&v) errs := ValidateCronJob(&v)

View File

@ -262,6 +262,11 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) {
**out = **in **out = **in
} }
in.Template.DeepCopyInto(&out.Template) in.Template.DeepCopyInto(&out.Template)
if in.TTLSecondsAfterFinished != nil {
in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished
*out = new(int32)
**out = **in
}
return return
} }

View File

@ -131,6 +131,7 @@ filegroup(
"//pkg/controller/statefulset:all-srcs", "//pkg/controller/statefulset:all-srcs",
"//pkg/controller/testutil:all-srcs", "//pkg/controller/testutil:all-srcs",
"//pkg/controller/ttl:all-srcs", "//pkg/controller/ttl:all-srcs",
"//pkg/controller/ttlafterfinished:all-srcs",
"//pkg/controller/util/node:all-srcs", "//pkg/controller/util/node:all-srcs",
"//pkg/controller/volume/attachdetach:all-srcs", "//pkg/controller/volume/attachdetach:all-srcs",
"//pkg/controller/volume/events:all-srcs", "//pkg/controller/volume/events:all-srcs",

View File

@ -96,6 +96,9 @@ type KubeControllerManagerConfiguration struct {
// ServiceControllerConfiguration holds configuration for ServiceController // ServiceControllerConfiguration holds configuration for ServiceController
// related features. // related features.
ServiceController ServiceControllerConfiguration ServiceController ServiceControllerConfiguration
// TTLAfterFinishedControllerConfiguration holds configuration for
// TTLAfterFinishedController related features.
TTLAfterFinishedController TTLAfterFinishedControllerConfiguration
} }
// GenericControllerManagerConfiguration holds configuration for a generic controller-manager // GenericControllerManagerConfiguration holds configuration for a generic controller-manager
@ -438,3 +441,10 @@ type PersistentVolumeRecyclerConfiguration struct {
// in a multi-node cluster. // in a multi-node cluster.
IncrementTimeoutHostPath int32 IncrementTimeoutHostPath int32
} }
// TTLAfterFinishedControllerConfiguration contains elements describing TTLAfterFinishedController.
type TTLAfterFinishedControllerConfiguration struct {
// concurrentTTLSyncs is the number of TTL-after-finished collector workers that are
// allowed to sync concurrently.
ConcurrentTTLSyncs int32
}

View File

@ -48,6 +48,9 @@ func SetDefaults_KubeControllerManagerConfiguration(obj *kubectrlmgrconfigv1alph
if obj.SAController.ConcurrentSATokenSyncs == 0 { if obj.SAController.ConcurrentSATokenSyncs == 0 {
obj.SAController.ConcurrentSATokenSyncs = 5 obj.SAController.ConcurrentSATokenSyncs = 5
} }
if obj.TTLAfterFinishedController.ConcurrentTTLSyncs <= 0 {
obj.TTLAfterFinishedController.ConcurrentTTLSyncs = 5
}
// These defaults override the recommended defaults from the apimachineryconfigv1alpha1 package that are applied automatically // These defaults override the recommended defaults from the apimachineryconfigv1alpha1 package that are applied automatically
// These client-connection defaults are specific to the kube-controller-manager // These client-connection defaults are specific to the kube-controller-manager

View File

@ -289,6 +289,16 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil { }); err != nil {
return err return err
} }
if err := s.AddGeneratedConversionFunc((*v1alpha1.TTLAfterFinishedControllerConfiguration)(nil), (*config.TTLAfterFinishedControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_TTLAfterFinishedControllerConfiguration_To_config_TTLAfterFinishedControllerConfiguration(a.(*v1alpha1.TTLAfterFinishedControllerConfiguration), b.(*config.TTLAfterFinishedControllerConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*config.TTLAfterFinishedControllerConfiguration)(nil), (*v1alpha1.TTLAfterFinishedControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_config_TTLAfterFinishedControllerConfiguration_To_v1alpha1_TTLAfterFinishedControllerConfiguration(a.(*config.TTLAfterFinishedControllerConfiguration), b.(*v1alpha1.TTLAfterFinishedControllerConfiguration), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v1alpha1.VolumeConfiguration)(nil), (*config.VolumeConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { if err := s.AddGeneratedConversionFunc((*v1alpha1.VolumeConfiguration)(nil), (*config.VolumeConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_VolumeConfiguration_To_config_VolumeConfiguration(a.(*v1alpha1.VolumeConfiguration), b.(*config.VolumeConfiguration), scope) return Convert_v1alpha1_VolumeConfiguration_To_config_VolumeConfiguration(a.(*v1alpha1.VolumeConfiguration), b.(*config.VolumeConfiguration), scope)
}); err != nil { }); err != nil {
@ -734,6 +744,9 @@ func autoConvert_v1alpha1_KubeControllerManagerConfiguration_To_config_KubeContr
if err := Convert_v1alpha1_ServiceControllerConfiguration_To_config_ServiceControllerConfiguration(&in.ServiceController, &out.ServiceController, s); err != nil { if err := Convert_v1alpha1_ServiceControllerConfiguration_To_config_ServiceControllerConfiguration(&in.ServiceController, &out.ServiceController, s); err != nil {
return err return err
} }
if err := Convert_v1alpha1_TTLAfterFinishedControllerConfiguration_To_config_TTLAfterFinishedControllerConfiguration(&in.TTLAfterFinishedController, &out.TTLAfterFinishedController, s); err != nil {
return err
}
return nil return nil
} }
@ -806,6 +819,9 @@ func autoConvert_config_KubeControllerManagerConfiguration_To_v1alpha1_KubeContr
if err := Convert_config_ServiceControllerConfiguration_To_v1alpha1_ServiceControllerConfiguration(&in.ServiceController, &out.ServiceController, s); err != nil { if err := Convert_config_ServiceControllerConfiguration_To_v1alpha1_ServiceControllerConfiguration(&in.ServiceController, &out.ServiceController, s); err != nil {
return err return err
} }
if err := Convert_config_TTLAfterFinishedControllerConfiguration_To_v1alpha1_TTLAfterFinishedControllerConfiguration(&in.TTLAfterFinishedController, &out.TTLAfterFinishedController, s); err != nil {
return err
}
return nil return nil
} }
@ -1070,6 +1086,26 @@ func autoConvert_config_ServiceControllerConfiguration_To_v1alpha1_ServiceContro
return nil return nil
} }
func autoConvert_v1alpha1_TTLAfterFinishedControllerConfiguration_To_config_TTLAfterFinishedControllerConfiguration(in *v1alpha1.TTLAfterFinishedControllerConfiguration, out *config.TTLAfterFinishedControllerConfiguration, s conversion.Scope) error {
out.ConcurrentTTLSyncs = in.ConcurrentTTLSyncs
return nil
}
// Convert_v1alpha1_TTLAfterFinishedControllerConfiguration_To_config_TTLAfterFinishedControllerConfiguration is an autogenerated conversion function.
func Convert_v1alpha1_TTLAfterFinishedControllerConfiguration_To_config_TTLAfterFinishedControllerConfiguration(in *v1alpha1.TTLAfterFinishedControllerConfiguration, out *config.TTLAfterFinishedControllerConfiguration, s conversion.Scope) error {
return autoConvert_v1alpha1_TTLAfterFinishedControllerConfiguration_To_config_TTLAfterFinishedControllerConfiguration(in, out, s)
}
func autoConvert_config_TTLAfterFinishedControllerConfiguration_To_v1alpha1_TTLAfterFinishedControllerConfiguration(in *config.TTLAfterFinishedControllerConfiguration, out *v1alpha1.TTLAfterFinishedControllerConfiguration, s conversion.Scope) error {
out.ConcurrentTTLSyncs = in.ConcurrentTTLSyncs
return nil
}
// Convert_config_TTLAfterFinishedControllerConfiguration_To_v1alpha1_TTLAfterFinishedControllerConfiguration is an autogenerated conversion function.
func Convert_config_TTLAfterFinishedControllerConfiguration_To_v1alpha1_TTLAfterFinishedControllerConfiguration(in *config.TTLAfterFinishedControllerConfiguration, out *v1alpha1.TTLAfterFinishedControllerConfiguration, s conversion.Scope) error {
return autoConvert_config_TTLAfterFinishedControllerConfiguration_To_v1alpha1_TTLAfterFinishedControllerConfiguration(in, out, s)
}
func autoConvert_v1alpha1_VolumeConfiguration_To_config_VolumeConfiguration(in *v1alpha1.VolumeConfiguration, out *config.VolumeConfiguration, s conversion.Scope) error { func autoConvert_v1alpha1_VolumeConfiguration_To_config_VolumeConfiguration(in *v1alpha1.VolumeConfiguration, out *config.VolumeConfiguration, s conversion.Scope) error {
if err := v1.Convert_Pointer_bool_To_bool(&in.EnableHostPathProvisioning, &out.EnableHostPathProvisioning, s); err != nil { if err := v1.Convert_Pointer_bool_To_bool(&in.EnableHostPathProvisioning, &out.EnableHostPathProvisioning, s); err != nil {
return err return err

View File

@ -285,6 +285,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa
out.ResourceQuotaController = in.ResourceQuotaController out.ResourceQuotaController = in.ResourceQuotaController
out.SAController = in.SAController out.SAController = in.SAController
out.ServiceController = in.ServiceController out.ServiceController = in.ServiceController
out.TTLAfterFinishedController = in.TTLAfterFinishedController
return return
} }
@ -489,6 +490,22 @@ func (in *ServiceControllerConfiguration) DeepCopy() *ServiceControllerConfigura
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TTLAfterFinishedControllerConfiguration) DeepCopyInto(out *TTLAfterFinishedControllerConfiguration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TTLAfterFinishedControllerConfiguration.
func (in *TTLAfterFinishedControllerConfiguration) DeepCopy() *TTLAfterFinishedControllerConfiguration {
if in == nil {
return nil
}
out := new(TTLAfterFinishedControllerConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VolumeConfiguration) DeepCopyInto(out *VolumeConfiguration) { func (in *VolumeConfiguration) DeepCopyInto(out *VolumeConfiguration) {
*out = *in *out = *in

View File

@ -0,0 +1,54 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["ttlafterfinished_controller.go"],
importpath = "k8s.io/kubernetes/pkg/controller/ttlafterfinished",
visibility = ["//visibility:public"],
deps = [
"//pkg/controller:go_default_library",
"//pkg/controller/job:go_default_library",
"//pkg/kubectl/scheme:go_default_library",
"//pkg/util/metrics:go_default_library",
"//staging/src/k8s.io/api/batch/v1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/clock:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//staging/src/k8s.io/client-go/informers/batch/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
"//staging/src/k8s.io/client-go/listers/batch/v1:go_default_library",
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
"//staging/src/k8s.io/client-go/tools/record:go_default_library",
"//staging/src/k8s.io/client-go/util/workqueue:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["ttlafterfinished_controller_test.go"],
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/api/batch/v1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,308 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ttlafterfinished
import (
"fmt"
"time"
"github.com/golang/glog"
batch "k8s.io/api/batch/v1"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/clock"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
batchinformers "k8s.io/client-go/informers/batch/v1"
clientset "k8s.io/client-go/kubernetes"
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
batchlisters "k8s.io/client-go/listers/batch/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/pkg/controller"
jobutil "k8s.io/kubernetes/pkg/controller/job"
"k8s.io/kubernetes/pkg/kubectl/scheme"
"k8s.io/kubernetes/pkg/util/metrics"
)
// Controller watches for changes of Jobs API objects. Triggered by Job creation
// and updates, it enqueues Jobs that have non-nil `.spec.ttlSecondsAfterFinished`
// to the `queue`. The Controller has workers who consume `queue`, check whether
// the Job TTL has expired or not; if the Job TTL hasn't expired, it will add the
// Job to the queue after the TTL is expected to expire; if the TTL has expired, the
// worker will send requests to the API server to delete the Jobs accordingly.
// This is implemented outside of Job controller for separation of concerns, and
// because it will be extended to handle other finishable resource types.
type Controller struct {
client clientset.Interface
recorder record.EventRecorder
// jLister can list/get Jobs from the shared informer's store
jLister batchlisters.JobLister
// jStoreSynced returns true if the Job store has been synced at least once.
// Added as a member to the struct to allow injection for testing.
jListerSynced cache.InformerSynced
// Jobs that the controller will check its TTL and attempt to delete when the TTL expires.
queue workqueue.RateLimitingInterface
// The clock for tracking time
clock clock.Clock
}
// New creates an instance of Controller
func New(jobInformer batchinformers.JobInformer, client clientset.Interface) *Controller {
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(glog.Infof)
eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: client.CoreV1().Events("")})
if client != nil && client.CoreV1().RESTClient().GetRateLimiter() != nil {
metrics.RegisterMetricAndTrackRateLimiterUsage("ttl_after_finished_controller", client.CoreV1().RESTClient().GetRateLimiter())
}
tc := &Controller{
client: client,
recorder: eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "ttl-after-finished-controller"}),
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "ttl_jobs_to_delete"),
}
jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: tc.addJob,
UpdateFunc: tc.updateJob,
})
tc.jLister = jobInformer.Lister()
tc.jListerSynced = jobInformer.Informer().HasSynced
tc.clock = clock.RealClock{}
return tc
}
// Run starts the workers to clean up Jobs.
func (tc *Controller) Run(workers int, stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer tc.queue.ShutDown()
glog.Infof("Starting TTL after finished controller")
defer glog.Infof("Shutting down TTL after finished controller")
if !controller.WaitForCacheSync("TTL after finished", stopCh, tc.jListerSynced) {
return
}
for i := 0; i < workers; i++ {
go wait.Until(tc.worker, time.Second, stopCh)
}
<-stopCh
}
func (tc *Controller) addJob(obj interface{}) {
job := obj.(*batch.Job)
glog.V(4).Infof("Adding job %s/%s", job.Namespace, job.Name)
if job.DeletionTimestamp == nil && needsCleanup(job) {
tc.enqueue(job)
}
}
func (tc *Controller) updateJob(old, cur interface{}) {
job := cur.(*batch.Job)
glog.V(4).Infof("Updating job %s/%s", job.Namespace, job.Name)
if job.DeletionTimestamp == nil && needsCleanup(job) {
tc.enqueue(job)
}
}
func (tc *Controller) enqueue(job *batch.Job) {
glog.V(4).Infof("Add job %s/%s to cleanup", job.Namespace, job.Name)
key, err := controller.KeyFunc(job)
if err != nil {
utilruntime.HandleError(fmt.Errorf("couldn't get key for object %#v: %v", job, err))
return
}
tc.queue.Add(key)
}
func (tc *Controller) enqueueAfter(job *batch.Job, after time.Duration) {
key, err := controller.KeyFunc(job)
if err != nil {
utilruntime.HandleError(fmt.Errorf("couldn't get key for object %#v: %v", job, err))
return
}
tc.queue.AddAfter(key, after)
}
func (tc *Controller) worker() {
for tc.processNextWorkItem() {
}
}
func (tc *Controller) processNextWorkItem() bool {
key, quit := tc.queue.Get()
if quit {
return false
}
defer tc.queue.Done(key)
err := tc.processJob(key.(string))
tc.handleErr(err, key)
return true
}
func (tc *Controller) handleErr(err error, key interface{}) {
if err == nil {
tc.queue.Forget(key)
return
}
utilruntime.HandleError(fmt.Errorf("error cleaning up Job %v, will retry: %v", key, err))
tc.queue.AddRateLimited(key)
}
// processJob will check the Job's state and TTL and delete the Job when it
// finishes and its TTL after finished has expired. If the Job hasn't finished or
// its TTL hasn't expired, it will be added to the queue after the TTL is expected
// to expire.
// This function is not meant to be invoked concurrently with the same key.
func (tc *Controller) processJob(key string) error {
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return err
}
glog.V(4).Infof("Checking if Job %s/%s is ready for cleanup", namespace, name)
// Ignore the Jobs that are already deleted or being deleted, or the ones that don't need clean up.
job, err := tc.jLister.Jobs(namespace).Get(name)
if errors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
if expired, err := tc.processTTL(job); err != nil {
return err
} else if !expired {
return nil
}
// The Job's TTL is assumed to have expired, but the Job TTL might be stale.
// Before deleting the Job, do a final sanity check.
// If TTL is modified before we do this check, we cannot be sure if the TTL truly expires.
// The latest Job may have a different UID, but it's fine because the checks will be run again.
fresh, err := tc.client.BatchV1().Jobs(namespace).Get(name, metav1.GetOptions{})
if errors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
// Use the latest Job TTL to see if the TTL truly expires.
if expired, err := tc.processTTL(fresh); err != nil {
return err
} else if !expired {
return nil
}
// Cascade deletes the Jobs if TTL truly expires.
policy := metav1.DeletePropagationForeground
options := &metav1.DeleteOptions{
PropagationPolicy: &policy,
Preconditions: &metav1.Preconditions{UID: &fresh.UID},
}
glog.V(4).Infof("Cleaning up Job %s/%s", namespace, name)
return tc.client.BatchV1().Jobs(fresh.Namespace).Delete(fresh.Name, options)
}
// processTTL checks whether a given Job's TTL has expired, and add it to the queue after the TTL is expected to expire
// if the TTL will expire later.
func (tc *Controller) processTTL(job *batch.Job) (expired bool, err error) {
// We don't care about the Jobs that are going to be deleted, or the ones that don't need clean up.
if job.DeletionTimestamp != nil || !needsCleanup(job) {
return false, nil
}
now := tc.clock.Now()
t, err := timeLeft(job, &now)
if err != nil {
return false, err
}
// TTL has expired
if *t <= 0 {
return true, nil
}
tc.enqueueAfter(job, *t)
return false, nil
}
// needsCleanup checks whether a Job has finished and has a TTL set.
func needsCleanup(j *batch.Job) bool {
return j.Spec.TTLSecondsAfterFinished != nil && jobutil.IsJobFinished(j)
}
func getFinishAndExpireTime(j *batch.Job) (*time.Time, *time.Time, error) {
if !needsCleanup(j) {
return nil, nil, fmt.Errorf("Job %s/%s should not be cleaned up", j.Namespace, j.Name)
}
finishAt, err := jobFinishTime(j)
if err != nil {
return nil, nil, err
}
finishAtUTC := finishAt.UTC()
expireAtUTC := finishAtUTC.Add(time.Duration(*j.Spec.TTLSecondsAfterFinished) * time.Second)
return &finishAtUTC, &expireAtUTC, nil
}
func timeLeft(j *batch.Job, since *time.Time) (*time.Duration, error) {
finishAt, expireAt, err := getFinishAndExpireTime(j)
if err != nil {
return nil, err
}
if finishAt.UTC().After(since.UTC()) {
glog.Warningf("Warning: Found Job %s/%s finished in the future. This is likely due to time skew in the cluster. Job cleanup will be deferred.", j.Namespace, j.Name)
}
remaining := expireAt.UTC().Sub(since.UTC())
glog.V(4).Infof("Found Job %s/%s finished at %v, remaining TTL %v since %v, TTL will expire at %v", j.Namespace, j.Name, finishAt.UTC(), remaining, since.UTC(), expireAt.UTC())
return &remaining, nil
}
// jobFinishTime takes an already finished Job and returns the time it finishes.
func jobFinishTime(finishedJob *batch.Job) (metav1.Time, error) {
for _, c := range finishedJob.Status.Conditions {
if (c.Type == batch.JobComplete || c.Type == batch.JobFailed) && c.Status == v1.ConditionTrue {
finishAt := c.LastTransitionTime
if finishAt.IsZero() {
return metav1.Time{}, fmt.Errorf("unable to find the time when the Job %s/%s finished", finishedJob.Namespace, finishedJob.Name)
}
return c.LastTransitionTime, nil
}
}
// This should never happen if the Jobs has finished
return metav1.Time{}, fmt.Errorf("unable to find the status of the finished Job %s/%s", finishedJob.Namespace, finishedJob.Name)
}

View File

@ -0,0 +1,177 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ttlafterfinished
import (
"strings"
"testing"
"time"
batch "k8s.io/api/batch/v1"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func newJob(completionTime, failedTime metav1.Time, ttl *int32) *batch.Job {
j := &batch.Job{
TypeMeta: metav1.TypeMeta{Kind: "Job"},
ObjectMeta: metav1.ObjectMeta{
Name: "foobar",
Namespace: metav1.NamespaceDefault,
},
Spec: batch.JobSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"foo": "bar",
},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Image: "foo/bar"},
},
},
},
},
}
if !completionTime.IsZero() {
c := batch.JobCondition{Type: batch.JobComplete, Status: v1.ConditionTrue, LastTransitionTime: completionTime}
j.Status.Conditions = append(j.Status.Conditions, c)
}
if !failedTime.IsZero() {
c := batch.JobCondition{Type: batch.JobFailed, Status: v1.ConditionTrue, LastTransitionTime: failedTime}
j.Status.Conditions = append(j.Status.Conditions, c)
}
if ttl != nil {
j.Spec.TTLSecondsAfterFinished = ttl
}
return j
}
func durationPointer(n int) *time.Duration {
s := time.Duration(n) * time.Second
return &s
}
func int32Ptr(n int32) *int32 {
return &n
}
func TestTimeLeft(t *testing.T) {
now := metav1.Now()
testCases := []struct {
name string
completionTime metav1.Time
failedTime metav1.Time
ttl *int32
since *time.Time
expectErr bool
expectErrStr string
expectedTimeLeft *time.Duration
}{
{
name: "Error case: Job unfinished",
ttl: int32Ptr(100),
since: &now.Time,
expectErr: true,
expectErrStr: "should not be cleaned up",
},
{
name: "Error case: Job completed now, no TTL",
completionTime: now,
since: &now.Time,
expectErr: true,
expectErrStr: "should not be cleaned up",
},
{
name: "Job completed now, 0s TTL",
completionTime: now,
ttl: int32Ptr(0),
since: &now.Time,
expectedTimeLeft: durationPointer(0),
},
{
name: "Job completed now, 10s TTL",
completionTime: now,
ttl: int32Ptr(10),
since: &now.Time,
expectedTimeLeft: durationPointer(10),
},
{
name: "Job completed 10s ago, 15s TTL",
completionTime: metav1.NewTime(now.Add(-10 * time.Second)),
ttl: int32Ptr(15),
since: &now.Time,
expectedTimeLeft: durationPointer(5),
},
{
name: "Error case: Job failed now, no TTL",
failedTime: now,
since: &now.Time,
expectErr: true,
expectErrStr: "should not be cleaned up",
},
{
name: "Job failed now, 0s TTL",
failedTime: now,
ttl: int32Ptr(0),
since: &now.Time,
expectedTimeLeft: durationPointer(0),
},
{
name: "Job failed now, 10s TTL",
failedTime: now,
ttl: int32Ptr(10),
since: &now.Time,
expectedTimeLeft: durationPointer(10),
},
{
name: "Job failed 10s ago, 15s TTL",
failedTime: metav1.NewTime(now.Add(-10 * time.Second)),
ttl: int32Ptr(15),
since: &now.Time,
expectedTimeLeft: durationPointer(5),
},
}
for _, tc := range testCases {
job := newJob(tc.completionTime, tc.failedTime, tc.ttl)
gotTimeLeft, gotErr := timeLeft(job, tc.since)
if tc.expectErr != (gotErr != nil) {
t.Errorf("%s: expected error is %t, got %t, error: %v", tc.name, tc.expectErr, gotErr != nil, gotErr)
}
if tc.expectErr && len(tc.expectErrStr) == 0 {
t.Errorf("%s: invalid test setup; error message must not be empty for error cases", tc.name)
}
if tc.expectErr && !strings.Contains(gotErr.Error(), tc.expectErrStr) {
t.Errorf("%s: expected error message contains %q, got %v", tc.name, tc.expectErrStr, gotErr)
}
if !tc.expectErr {
if *gotTimeLeft != *tc.expectedTimeLeft {
t.Errorf("%s: expected time left %v, got %v", tc.name, tc.expectedTimeLeft, gotTimeLeft)
}
}
}
}

View File

@ -381,6 +381,12 @@ const (
// //
// Enables control over ProcMountType for containers. // Enables control over ProcMountType for containers.
ProcMountType utilfeature.Feature = "ProcMountType" ProcMountType utilfeature.Feature = "ProcMountType"
// owner: @janetkuo
// alpha: v1.12
//
// Allow TTL controller to clean up Pods and Jobs after they finish.
TTLAfterFinished utilfeature.Feature = "TTLAfterFinished"
) )
func init() { func init() {
@ -445,6 +451,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
SCTPSupport: {Default: false, PreRelease: utilfeature.Alpha}, SCTPSupport: {Default: false, PreRelease: utilfeature.Alpha},
VolumeSnapshotDataSource: {Default: false, PreRelease: utilfeature.Alpha}, VolumeSnapshotDataSource: {Default: false, PreRelease: utilfeature.Alpha},
ProcMountType: {Default: false, PreRelease: utilfeature.Alpha}, ProcMountType: {Default: false, PreRelease: utilfeature.Alpha},
TTLAfterFinished: {Default: false, PreRelease: utilfeature.Alpha},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed // inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side: // unintentionally on either side:

View File

@ -18,6 +18,7 @@ go_library(
"//pkg/api/pod:go_default_library", "//pkg/api/pod:go_default_library",
"//pkg/apis/batch:go_default_library", "//pkg/apis/batch:go_default_library",
"//pkg/apis/batch/validation:go_default_library", "//pkg/apis/batch/validation:go_default_library",
"//pkg/features:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
@ -27,6 +28,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
], ],
) )
@ -39,10 +41,12 @@ go_test(
"//pkg/api/testing:go_default_library", "//pkg/api/testing:go_default_library",
"//pkg/apis/batch:go_default_library", "//pkg/apis/batch:go_default_library",
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/features:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
], ],
) )

View File

@ -30,10 +30,12 @@ import (
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/api/pod" "k8s.io/kubernetes/pkg/api/pod"
"k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/apis/batch/validation" "k8s.io/kubernetes/pkg/apis/batch/validation"
"k8s.io/kubernetes/pkg/features"
) )
// jobStrategy implements verification logic for Replication Controllers. // jobStrategy implements verification logic for Replication Controllers.
@ -61,6 +63,10 @@ func (jobStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
job := obj.(*batch.Job) job := obj.(*batch.Job)
job.Status = batch.JobStatus{} job.Status = batch.JobStatus{}
if !utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
job.Spec.TTLSecondsAfterFinished = nil
}
pod.DropDisabledAlphaFields(&job.Spec.Template.Spec) pod.DropDisabledAlphaFields(&job.Spec.Template.Spec)
} }
@ -70,6 +76,11 @@ func (jobStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object
oldJob := old.(*batch.Job) oldJob := old.(*batch.Job)
newJob.Status = oldJob.Status newJob.Status = oldJob.Status
if !utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
newJob.Spec.TTLSecondsAfterFinished = nil
oldJob.Spec.TTLSecondsAfterFinished = nil
}
pod.DropDisabledAlphaFields(&newJob.Spec.Template.Spec) pod.DropDisabledAlphaFields(&newJob.Spec.Template.Spec)
pod.DropDisabledAlphaFields(&oldJob.Spec.Template.Spec) pod.DropDisabledAlphaFields(&oldJob.Spec.Template.Spec)
} }

View File

@ -24,19 +24,24 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/testapi"
apitesting "k8s.io/kubernetes/pkg/api/testing" apitesting "k8s.io/kubernetes/pkg/api/testing"
"k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/batch"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
) )
func newBool(a bool) *bool { func newBool(a bool) *bool {
r := new(bool) return &a
*r = a }
return r
func newInt32(i int32) *int32 {
return &i
} }
func TestJobStrategy(t *testing.T) { func TestJobStrategy(t *testing.T) {
ttlEnabled := utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished)
ctx := genericapirequest.NewDefaultContext() ctx := genericapirequest.NewDefaultContext()
if !Strategy.NamespaceScoped() { if !Strategy.NamespaceScoped() {
t.Errorf("Job must be namespace scoped") t.Errorf("Job must be namespace scoped")
@ -64,9 +69,10 @@ func TestJobStrategy(t *testing.T) {
Namespace: metav1.NamespaceDefault, Namespace: metav1.NamespaceDefault,
}, },
Spec: batch.JobSpec{ Spec: batch.JobSpec{
Selector: validSelector, Selector: validSelector,
Template: validPodTemplateSpec, Template: validPodTemplateSpec,
ManualSelector: newBool(true), TTLSecondsAfterFinished: newInt32(0), // Set TTL
ManualSelector: newBool(true),
}, },
Status: batch.JobStatus{ Status: batch.JobStatus{
Active: 11, Active: 11,
@ -81,11 +87,21 @@ func TestJobStrategy(t *testing.T) {
if len(errs) != 0 { if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs) t.Errorf("Unexpected error validating %v", errs)
} }
if ttlEnabled && job.Spec.TTLSecondsAfterFinished == nil {
// When the TTL feature is enabled, the TTL field can be set
t.Errorf("Job should allow setting .spec.ttlSecondsAfterFinished when %v feature is enabled", features.TTLAfterFinished)
}
if !ttlEnabled && job.Spec.TTLSecondsAfterFinished != nil {
// When the TTL feature is disabled, the TTL field cannot be set
t.Errorf("Job should not allow setting .spec.ttlSecondsAfterFinished when %v feature is disabled", features.TTLAfterFinished)
}
parallelism := int32(10) parallelism := int32(10)
updatedJob := &batch.Job{ updatedJob := &batch.Job{
ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "4"}, ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "4"},
Spec: batch.JobSpec{ Spec: batch.JobSpec{
Parallelism: &parallelism, Parallelism: &parallelism,
TTLSecondsAfterFinished: newInt32(1), // Update TTL
}, },
Status: batch.JobStatus{ Status: batch.JobStatus{
Active: 11, Active: 11,
@ -101,6 +117,9 @@ func TestJobStrategy(t *testing.T) {
if len(errs) == 0 { if len(errs) == 0 {
t.Errorf("Expected a validation error") t.Errorf("Expected a validation error")
} }
if ttlEnabled != (job.Spec.TTLSecondsAfterFinished != nil || updatedJob.Spec.TTLSecondsAfterFinished != nil) {
t.Errorf("Job should only allow updating .spec.ttlSecondsAfterFinished when %v feature is enabled", features.TTLAfterFinished)
}
// Make sure we correctly implement the interface. // Make sure we correctly implement the interface.
// Otherwise a typo could silently change the default. // Otherwise a typo could silently change the default.

View File

@ -340,6 +340,15 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding)
eventsRule(), eventsRule(),
}, },
}) })
if utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
addControllerRole(&controllerRoles, &controllerRoleBindings, rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{Name: saRolePrefix + "ttl-after-finished-controller"},
Rules: []rbacv1.PolicyRule{
rbacv1helpers.NewRule("get", "list", "watch", "delete").Groups(batchGroup).Resources("jobs").RuleOrDie(),
eventsRule(),
},
})
}
return controllerRoles, controllerRoleBindings return controllerRoles, controllerRoleBindings
} }

View File

@ -276,6 +276,11 @@ func (m *JobSpec) MarshalTo(dAtA []byte) (int, error) {
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(*m.BackoffLimit)) i = encodeVarintGenerated(dAtA, i, uint64(*m.BackoffLimit))
} }
if m.TTLSecondsAfterFinished != nil {
dAtA[i] = 0x40
i++
i = encodeVarintGenerated(dAtA, i, uint64(*m.TTLSecondsAfterFinished))
}
return i, nil return i, nil
} }
@ -433,6 +438,9 @@ func (m *JobSpec) Size() (n int) {
if m.BackoffLimit != nil { if m.BackoffLimit != nil {
n += 1 + sovGenerated(uint64(*m.BackoffLimit)) n += 1 + sovGenerated(uint64(*m.BackoffLimit))
} }
if m.TTLSecondsAfterFinished != nil {
n += 1 + sovGenerated(uint64(*m.TTLSecondsAfterFinished))
}
return n return n
} }
@ -522,6 +530,7 @@ func (this *JobSpec) String() string {
`ManualSelector:` + valueToStringGenerated(this.ManualSelector) + `,`, `ManualSelector:` + valueToStringGenerated(this.ManualSelector) + `,`,
`Template:` + strings.Replace(strings.Replace(this.Template.String(), "PodTemplateSpec", "k8s_io_api_core_v1.PodTemplateSpec", 1), `&`, ``, 1) + `,`, `Template:` + strings.Replace(strings.Replace(this.Template.String(), "PodTemplateSpec", "k8s_io_api_core_v1.PodTemplateSpec", 1), `&`, ``, 1) + `,`,
`BackoffLimit:` + valueToStringGenerated(this.BackoffLimit) + `,`, `BackoffLimit:` + valueToStringGenerated(this.BackoffLimit) + `,`,
`TTLSecondsAfterFinished:` + valueToStringGenerated(this.TTLSecondsAfterFinished) + `,`,
`}`, `}`,
}, "") }, "")
return s return s
@ -1219,6 +1228,26 @@ func (m *JobSpec) Unmarshal(dAtA []byte) error {
} }
} }
m.BackoffLimit = &v m.BackoffLimit = &v
case 8:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field TTLSecondsAfterFinished", wireType)
}
var v int32
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
v |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
m.TTLSecondsAfterFinished = &v
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:]) skippy, err := skipGenerated(dAtA[iNdEx:])
@ -1554,61 +1583,64 @@ func init() {
} }
var fileDescriptorGenerated = []byte{ var fileDescriptorGenerated = []byte{
// 893 bytes of a gzipped FileDescriptorProto // 929 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x41, 0x6f, 0xe3, 0x44, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x5d, 0x6f, 0xe3, 0x44,
0x18, 0x8d, 0x9b, 0xa6, 0x4d, 0x26, 0x69, 0xb7, 0x0c, 0xaa, 0x14, 0x2a, 0xe4, 0x2c, 0x41, 0x42, 0x14, 0xad, 0x9b, 0xa6, 0x4d, 0xa6, 0x1f, 0x5b, 0x06, 0x55, 0x1b, 0x0a, 0xb2, 0x97, 0x20, 0xa1,
0x05, 0x09, 0x9b, 0x94, 0x0a, 0x21, 0x04, 0x48, 0xb8, 0x68, 0x25, 0xaa, 0x54, 0x5b, 0x26, 0x45, 0x82, 0x84, 0x4d, 0x4b, 0x85, 0x10, 0x02, 0xa4, 0x75, 0x51, 0x25, 0xaa, 0x54, 0x5b, 0x26, 0x59,
0x48, 0x08, 0x24, 0xc6, 0xf6, 0x97, 0xd4, 0xc4, 0xf6, 0x58, 0x9e, 0x49, 0xa4, 0xde, 0xf8, 0x09, 0x21, 0x21, 0x90, 0x18, 0xdb, 0x37, 0x89, 0x89, 0xed, 0xb1, 0x3c, 0x93, 0x48, 0x7d, 0xe3, 0x27,
0xfc, 0x08, 0xc4, 0x4f, 0x41, 0x3d, 0xee, 0x71, 0x4f, 0x11, 0x35, 0xdc, 0xb9, 0xef, 0x09, 0xcd, 0xf0, 0x23, 0x10, 0x7f, 0x82, 0x77, 0xd4, 0xc7, 0x7d, 0xdc, 0x27, 0x8b, 0x9a, 0x1f, 0xc0, 0xfb,
0x78, 0x62, 0x3b, 0x6d, 0x2a, 0xda, 0xbd, 0x79, 0xde, 0xbc, 0xf7, 0xbe, 0xf1, 0x37, 0x6f, 0x3e, 0x3e, 0xa1, 0x19, 0x3b, 0xb6, 0xd3, 0x26, 0xa2, 0xcb, 0x5b, 0xe6, 0xcc, 0x39, 0xe7, 0x5e, 0xdf,
0xf4, 0xf9, 0xf4, 0x53, 0x6e, 0x05, 0xcc, 0x9e, 0xce, 0x5c, 0x48, 0x63, 0x10, 0xc0, 0xed, 0x39, 0x39, 0xb9, 0xe8, 0x8b, 0xc9, 0x67, 0xdc, 0xf4, 0x99, 0x35, 0x99, 0x3a, 0x90, 0x44, 0x20, 0x80,
0xc4, 0x3e, 0x4b, 0x6d, 0xbd, 0x41, 0x93, 0xc0, 0x76, 0xa9, 0xf0, 0x2e, 0xed, 0xf9, 0xc0, 0x9e, 0x5b, 0x33, 0x88, 0x3c, 0x96, 0x58, 0xc5, 0x05, 0x8d, 0x7d, 0xcb, 0xa1, 0xc2, 0x1d, 0x5b, 0xb3,
0x40, 0x0c, 0x29, 0x15, 0xe0, 0x5b, 0x49, 0xca, 0x04, 0xc3, 0x6f, 0xe6, 0x24, 0x8b, 0x26, 0x81, 0x63, 0x6b, 0x04, 0x11, 0x24, 0x54, 0x80, 0x67, 0xc6, 0x09, 0x13, 0x0c, 0xbf, 0x99, 0x93, 0x4c,
0xa5, 0x48, 0xd6, 0x7c, 0x70, 0xf0, 0xe1, 0x24, 0x10, 0x97, 0x33, 0xd7, 0xf2, 0x58, 0x64, 0x4f, 0x1a, 0xfb, 0xa6, 0x22, 0x99, 0xb3, 0xe3, 0xc3, 0x8f, 0x46, 0xbe, 0x18, 0x4f, 0x1d, 0xd3, 0x65,
0xd8, 0x84, 0xd9, 0x8a, 0xeb, 0xce, 0xc6, 0x6a, 0xa5, 0x16, 0xea, 0x2b, 0xf7, 0x38, 0xe8, 0x57, 0xa1, 0x35, 0x62, 0x23, 0x66, 0x29, 0xae, 0x33, 0x1d, 0xaa, 0x93, 0x3a, 0xa8, 0x5f, 0xb9, 0xc7,
0x0a, 0x79, 0x2c, 0x85, 0x35, 0x75, 0x0e, 0x8e, 0x4b, 0x4e, 0x44, 0xbd, 0xcb, 0x20, 0x86, 0xf4, 0x61, 0xb7, 0x56, 0xc8, 0x65, 0x09, 0x2c, 0xa9, 0x73, 0x78, 0x5a, 0x71, 0x42, 0xea, 0x8e, 0xfd,
0xca, 0x4e, 0xa6, 0x13, 0x09, 0x70, 0x3b, 0x02, 0x41, 0xd7, 0xa9, 0xec, 0xfb, 0x54, 0xe9, 0x2c, 0x08, 0x92, 0x6b, 0x2b, 0x9e, 0x8c, 0x24, 0xc0, 0xad, 0x10, 0x04, 0x5d, 0xa6, 0xb2, 0x56, 0xa9,
0x16, 0x41, 0x04, 0x77, 0x04, 0x9f, 0xfc, 0x9f, 0x80, 0x7b, 0x97, 0x10, 0xd1, 0xdb, 0xba, 0xfe, 0x92, 0x69, 0x24, 0xfc, 0x10, 0xee, 0x09, 0x3e, 0xfd, 0x2f, 0x01, 0x77, 0xc7, 0x10, 0xd2, 0xbb,
0xbf, 0x06, 0xaa, 0x9f, 0x32, 0x17, 0xff, 0x8c, 0x9a, 0xf2, 0x2c, 0x3e, 0x15, 0xb4, 0x6b, 0x3c, 0xba, 0xee, 0x3f, 0x1a, 0x6a, 0x5c, 0x30, 0x07, 0xff, 0x84, 0x5a, 0xb2, 0x17, 0x8f, 0x0a, 0xda,
0x35, 0x0e, 0xdb, 0x47, 0x1f, 0x59, 0x65, 0x87, 0x0a, 0x4b, 0x2b, 0x99, 0x4e, 0x24, 0xc0, 0x2d, 0xd1, 0x9e, 0x68, 0x47, 0xdb, 0x27, 0x1f, 0x9b, 0xd5, 0x84, 0x4a, 0x4b, 0x33, 0x9e, 0x8c, 0x24,
0xc9, 0xb6, 0xe6, 0x03, 0xeb, 0xb9, 0xfb, 0x0b, 0x78, 0xe2, 0x0c, 0x04, 0x75, 0xf0, 0xf5, 0xa2, 0xc0, 0x4d, 0xc9, 0x36, 0x67, 0xc7, 0xe6, 0x33, 0xe7, 0x67, 0x70, 0xc5, 0x25, 0x08, 0x6a, 0xe3,
0x57, 0xcb, 0x16, 0x3d, 0x54, 0x62, 0xa4, 0x70, 0xc5, 0x5f, 0xa2, 0x4d, 0x9e, 0x80, 0xd7, 0xdd, 0x9b, 0xd4, 0x58, 0xcb, 0x52, 0x03, 0x55, 0x18, 0x29, 0x5d, 0xf1, 0x57, 0x68, 0x83, 0xc7, 0xe0,
0x50, 0xee, 0x6f, 0x5b, 0x6b, 0xfa, 0x6f, 0x9d, 0x32, 0x77, 0x94, 0x80, 0xe7, 0x74, 0xb4, 0xd3, 0x76, 0xd6, 0x95, 0xfb, 0x3b, 0xe6, 0x92, 0xf9, 0x9b, 0x17, 0xcc, 0xe9, 0xc7, 0xe0, 0xda, 0x3b,
0xa6, 0x5c, 0x11, 0xa5, 0xc3, 0xcf, 0xd0, 0x16, 0x17, 0x54, 0xcc, 0x78, 0xb7, 0xae, 0x1c, 0xcc, 0x85, 0xd3, 0x86, 0x3c, 0x11, 0xa5, 0xc3, 0xe7, 0x68, 0x93, 0x0b, 0x2a, 0xa6, 0xbc, 0xd3, 0x50,
0x7b, 0x1d, 0x14, 0xcb, 0xd9, 0xd5, 0x1e, 0x5b, 0xf9, 0x9a, 0x68, 0x75, 0xff, 0xcf, 0x3a, 0xea, 0x0e, 0xfa, 0x4a, 0x07, 0xc5, 0xb2, 0xf7, 0x0a, 0x8f, 0xcd, 0xfc, 0x4c, 0x0a, 0x75, 0xf7, 0xcf,
0x9c, 0x32, 0xf7, 0x84, 0xc5, 0x7e, 0x20, 0x02, 0x16, 0xe3, 0x63, 0xb4, 0x29, 0xae, 0x12, 0x50, 0x06, 0xda, 0xb9, 0x60, 0xce, 0x19, 0x8b, 0x3c, 0x5f, 0xf8, 0x2c, 0xc2, 0xa7, 0x68, 0x43, 0x5c,
0xbf, 0xdd, 0x72, 0x9e, 0x2e, 0x4b, 0x5f, 0x5c, 0x25, 0xf0, 0x6a, 0xd1, 0xdb, 0xab, 0x72, 0x25, 0xc7, 0xa0, 0x3e, 0xbb, 0x6d, 0x3f, 0x99, 0x97, 0x1e, 0x5c, 0xc7, 0xf0, 0x2a, 0x35, 0xf6, 0xeb,
0x46, 0x14, 0x1b, 0x0f, 0x8b, 0xe3, 0x6c, 0x28, 0xdd, 0xf1, 0x6a, 0xb9, 0x57, 0x8b, 0xde, 0x9a, 0x5c, 0x89, 0x11, 0xc5, 0xc6, 0xbd, 0xb2, 0x9d, 0x75, 0xa5, 0x3b, 0x5d, 0x2c, 0xf7, 0x2a, 0x35,
0x74, 0x58, 0x85, 0xd3, 0xea, 0xa1, 0xf0, 0x04, 0xed, 0x84, 0x94, 0x8b, 0xf3, 0x94, 0xb9, 0x70, 0x96, 0xa4, 0xc3, 0x2c, 0x9d, 0x16, 0x9b, 0xc2, 0x23, 0xb4, 0x1b, 0x50, 0x2e, 0xae, 0x12, 0xe6,
0x11, 0x44, 0xa0, 0xff, 0xf1, 0x83, 0x87, 0xdd, 0x81, 0x54, 0x38, 0xfb, 0xfa, 0x00, 0x3b, 0xc3, 0xc0, 0xc0, 0x0f, 0xa1, 0xf8, 0xc6, 0x0f, 0x1f, 0xf6, 0x06, 0x52, 0x61, 0x1f, 0x14, 0x0d, 0xec,
0xaa, 0x11, 0x59, 0xf5, 0xc5, 0x73, 0x84, 0x25, 0x70, 0x91, 0xd2, 0x98, 0xe7, 0xbf, 0x24, 0xab, 0xf6, 0xea, 0x46, 0x64, 0xd1, 0x17, 0xcf, 0x10, 0x96, 0xc0, 0x20, 0xa1, 0x11, 0xcf, 0x3f, 0x49,
0x6d, 0x3e, 0xba, 0xda, 0x81, 0xae, 0x86, 0x87, 0x77, 0xdc, 0xc8, 0x9a, 0x0a, 0xf8, 0x3d, 0xb4, 0x56, 0xdb, 0x78, 0xed, 0x6a, 0x87, 0x45, 0x35, 0xdc, 0xbb, 0xe7, 0x46, 0x96, 0x54, 0xc0, 0xef,
0x95, 0x02, 0xe5, 0x2c, 0xee, 0x36, 0x54, 0xbb, 0x8a, 0xdb, 0x21, 0x0a, 0x25, 0x7a, 0x17, 0xbf, 0xa3, 0xcd, 0x04, 0x28, 0x67, 0x51, 0xa7, 0xa9, 0xc6, 0x55, 0xbe, 0x0e, 0x51, 0x28, 0x29, 0x6e,
0x8f, 0xb6, 0x23, 0xe0, 0x9c, 0x4e, 0xa0, 0xbb, 0xa5, 0x88, 0x4f, 0x34, 0x71, 0xfb, 0x2c, 0x87, 0xf1, 0x07, 0x68, 0x2b, 0x04, 0xce, 0xe9, 0x08, 0x3a, 0x9b, 0x8a, 0xf8, 0xa8, 0x20, 0x6e, 0x5d,
0xc9, 0x72, 0xbf, 0xff, 0x87, 0x81, 0xb6, 0x4f, 0x99, 0x3b, 0x0c, 0xb8, 0xc0, 0x3f, 0xde, 0x89, 0xe6, 0x30, 0x99, 0xdf, 0x77, 0x7f, 0xd7, 0xd0, 0xd6, 0x05, 0x73, 0x7a, 0x3e, 0x17, 0xf8, 0x87,
0xaf, 0xf5, 0xb0, 0x9f, 0x91, 0x6a, 0x15, 0xde, 0x3d, 0x5d, 0xa7, 0xb9, 0x44, 0x2a, 0xd1, 0xfd, 0x7b, 0xf1, 0x35, 0x1f, 0xf6, 0x31, 0x52, 0xad, 0xc2, 0xbb, 0x5f, 0xd4, 0x69, 0xcd, 0x91, 0x5a,
0x02, 0x35, 0x02, 0x01, 0x91, 0xbc, 0xea, 0xfa, 0x61, 0xfb, 0xa8, 0x7b, 0x5f, 0xf2, 0x9c, 0x1d, 0x74, 0xbf, 0x44, 0x4d, 0x5f, 0x40, 0x28, 0x9f, 0xba, 0x71, 0xb4, 0x7d, 0xd2, 0x59, 0x95, 0x3c,
0x6d, 0xd2, 0xf8, 0x46, 0xd2, 0x49, 0xae, 0xea, 0xff, 0x53, 0x57, 0x07, 0x95, 0x59, 0xc6, 0x03, 0x7b, 0xb7, 0x30, 0x69, 0x7e, 0x23, 0xe9, 0x24, 0x57, 0x75, 0xff, 0xd8, 0x50, 0x8d, 0xca, 0x2c,
0xd4, 0x4e, 0x68, 0x4a, 0xc3, 0x10, 0xc2, 0x80, 0x47, 0xea, 0xac, 0x0d, 0xe7, 0x49, 0xb6, 0xe8, 0xe3, 0x63, 0xb4, 0x1d, 0xd3, 0x84, 0x06, 0x01, 0x04, 0x3e, 0x0f, 0x55, 0xaf, 0x4d, 0xfb, 0x51,
0xb5, 0xcf, 0x4b, 0x98, 0x54, 0x39, 0x52, 0xe2, 0xb1, 0x28, 0x09, 0x41, 0x36, 0x33, 0x8f, 0x9b, 0x96, 0x1a, 0xdb, 0x57, 0x15, 0x4c, 0xea, 0x1c, 0x29, 0x71, 0x59, 0x18, 0x07, 0x20, 0x87, 0x99,
0x96, 0x9c, 0x94, 0x30, 0xa9, 0x72, 0xf0, 0x73, 0xb4, 0x4f, 0x3d, 0x11, 0xcc, 0xe1, 0x6b, 0xa0, 0xc7, 0xad, 0x90, 0x9c, 0x55, 0x30, 0xa9, 0x73, 0xf0, 0x33, 0x74, 0x40, 0x5d, 0xe1, 0xcf, 0xe0,
0x7e, 0x18, 0xc4, 0x30, 0x02, 0x8f, 0xc5, 0x7e, 0xfe, 0x74, 0xea, 0xce, 0x5b, 0xd9, 0xa2, 0xb7, 0x6b, 0xa0, 0x5e, 0xe0, 0x47, 0xd0, 0x07, 0x97, 0x45, 0x5e, 0xfe, 0xd7, 0x69, 0xd8, 0x6f, 0x65,
0xff, 0xd5, 0x3a, 0x02, 0x59, 0xaf, 0xc3, 0x3f, 0xa1, 0x26, 0x87, 0x10, 0x3c, 0xc1, 0x52, 0x1d, 0xa9, 0x71, 0xf0, 0x74, 0x19, 0x81, 0x2c, 0xd7, 0xe1, 0x1f, 0x51, 0x8b, 0x43, 0x00, 0xae, 0x60,
0x96, 0x8f, 0x1f, 0xd8, 0x5f, 0xea, 0x42, 0x38, 0xd2, 0x52, 0xa7, 0x23, 0x1b, 0xbc, 0x5c, 0x91, 0x49, 0x11, 0x96, 0x4f, 0x1e, 0x38, 0x5f, 0xea, 0x40, 0xd0, 0x2f, 0xa4, 0xf6, 0x8e, 0x1c, 0xf0,
0xc2, 0x12, 0x7f, 0x86, 0x76, 0x23, 0x1a, 0xcf, 0x68, 0xc1, 0x54, 0x29, 0x69, 0x3a, 0x38, 0x5b, 0xfc, 0x44, 0x4a, 0x4b, 0xfc, 0x39, 0xda, 0x0b, 0x69, 0x34, 0xa5, 0x25, 0x53, 0xa5, 0xa4, 0x65,
0xf4, 0x76, 0xcf, 0x56, 0x76, 0xc8, 0x2d, 0x26, 0xfe, 0x16, 0x35, 0x05, 0x44, 0x49, 0x48, 0x45, 0xe3, 0x2c, 0x35, 0xf6, 0x2e, 0x17, 0x6e, 0xc8, 0x1d, 0x26, 0xfe, 0x16, 0xb5, 0x04, 0x84, 0x71,
0x1e, 0x99, 0xf6, 0xd1, 0xbb, 0xd5, 0xfb, 0x91, 0x2f, 0x4f, 0x1e, 0xe4, 0x9c, 0xf9, 0x17, 0x9a, 0x40, 0x45, 0x1e, 0x99, 0xed, 0x93, 0xf7, 0xea, 0xef, 0x23, 0xff, 0x79, 0xb2, 0x91, 0x2b, 0xe6,
0xa6, 0x46, 0x4c, 0x71, 0xdf, 0x4b, 0x94, 0x14, 0x36, 0xf8, 0x18, 0x75, 0x5c, 0xea, 0x4d, 0xd9, 0x0d, 0x0a, 0x9a, 0x5a, 0x31, 0xe5, 0x7b, 0xcf, 0x51, 0x52, 0xda, 0xe0, 0x53, 0xb4, 0xe3, 0x50,
0x78, 0x3c, 0x0c, 0xa2, 0x40, 0x74, 0xb7, 0x55, 0xcb, 0xf7, 0xb2, 0x45, 0xaf, 0xe3, 0x54, 0x70, 0x77, 0xc2, 0x86, 0xc3, 0x9e, 0x1f, 0xfa, 0xa2, 0xb3, 0xa5, 0x46, 0xbe, 0x9f, 0xa5, 0xc6, 0x8e,
0xb2, 0xc2, 0xea, 0xff, 0x5e, 0x47, 0xad, 0x62, 0xfc, 0xe0, 0xef, 0x10, 0xf2, 0x96, 0x8f, 0x9d, 0x5d, 0xc3, 0xc9, 0x02, 0x0b, 0x3f, 0x47, 0x8f, 0x85, 0x08, 0x8a, 0x89, 0x3d, 0x1d, 0x0a, 0x48,
0x77, 0x0d, 0x15, 0x9c, 0x77, 0xee, 0x0b, 0x4e, 0x31, 0x16, 0xca, 0x19, 0x5a, 0x40, 0x9c, 0x54, 0xce, 0xfd, 0xc8, 0xe7, 0x63, 0xf0, 0x3a, 0x2d, 0x65, 0xf0, 0x76, 0x96, 0x1a, 0x8f, 0x07, 0x83,
0x8c, 0xf0, 0xf7, 0xa8, 0xc5, 0x05, 0x4d, 0x85, 0x7a, 0xb6, 0x1b, 0x8f, 0x7e, 0xb6, 0x3b, 0xd9, 0xde, 0x32, 0x0a, 0x59, 0xa5, 0xed, 0xfe, 0xd6, 0x40, 0xed, 0x72, 0xab, 0xe1, 0xe7, 0x08, 0xb9,
0xa2, 0xd7, 0x1a, 0x2d, 0x0d, 0x48, 0xe9, 0x85, 0xc7, 0x68, 0xb7, 0x4c, 0xd0, 0x6b, 0x8e, 0x20, 0xf3, 0x1d, 0xc2, 0x3b, 0x9a, 0xca, 0xe3, 0xbb, 0xab, 0xf2, 0x58, 0x6e, 0x9b, 0x6a, 0x35, 0x97,
0x75, 0x5d, 0x27, 0x2b, 0x2e, 0xe4, 0x96, 0xab, 0x1c, 0x04, 0x79, 0xc4, 0x54, 0x8e, 0x1a, 0xe5, 0x10, 0x27, 0x35, 0x23, 0xfc, 0x1d, 0x6a, 0x73, 0x41, 0x13, 0xa1, 0xb6, 0xc1, 0xfa, 0x6b, 0x6f,
0x20, 0xc8, 0xf3, 0x48, 0xf4, 0x2e, 0xb6, 0x51, 0x8b, 0xcf, 0x3c, 0x0f, 0xc0, 0x07, 0x5f, 0xa5, 0x83, 0xdd, 0x2c, 0x35, 0xda, 0xfd, 0xb9, 0x01, 0xa9, 0xbc, 0xf0, 0x10, 0xed, 0x55, 0xc1, 0xfc,
0xa1, 0xe1, 0xbc, 0xa1, 0xa9, 0xad, 0xd1, 0x72, 0x83, 0x94, 0x1c, 0x69, 0x3c, 0xa6, 0x41, 0x08, 0x9f, 0x9b, 0x4d, 0xa5, 0xe0, 0x6c, 0xc1, 0x85, 0xdc, 0x71, 0x95, 0xfb, 0x25, 0x4f, 0xae, 0x8a,
0xbe, 0x4a, 0x41, 0xc5, 0xf8, 0x99, 0x42, 0x89, 0xde, 0x75, 0x0e, 0xaf, 0x6f, 0xcc, 0xda, 0x8b, 0x67, 0xb3, 0xda, 0x2f, 0x79, 0xcc, 0x49, 0x71, 0x8b, 0x2d, 0xd4, 0xe6, 0x53, 0xd7, 0x05, 0xf0,
0x1b, 0xb3, 0xf6, 0xf2, 0xc6, 0xac, 0xfd, 0x9a, 0x99, 0xc6, 0x75, 0x66, 0x1a, 0x2f, 0x32, 0xd3, 0xc0, 0x53, 0x21, 0x6b, 0xda, 0x6f, 0x14, 0xd4, 0x76, 0x7f, 0x7e, 0x41, 0x2a, 0x8e, 0x34, 0x1e,
0x78, 0x99, 0x99, 0xc6, 0x5f, 0x99, 0x69, 0xfc, 0xf6, 0xb7, 0x59, 0xfb, 0x61, 0x63, 0x3e, 0xf8, 0x52, 0x3f, 0x00, 0x4f, 0x85, 0xab, 0x66, 0x7c, 0xae, 0x50, 0x52, 0xdc, 0xda, 0x47, 0x37, 0xb7,
0x2f, 0x00, 0x00, 0xff, 0xff, 0xdd, 0xcc, 0x84, 0xd1, 0x61, 0x08, 0x00, 0x00, 0xfa, 0xda, 0x8b, 0x5b, 0x7d, 0xed, 0xe5, 0xad, 0xbe, 0xf6, 0x4b, 0xa6, 0x6b, 0x37, 0x99, 0xae,
0xbd, 0xc8, 0x74, 0xed, 0x65, 0xa6, 0x6b, 0x7f, 0x65, 0xba, 0xf6, 0xeb, 0xdf, 0xfa, 0xda, 0xf7,
0xeb, 0xb3, 0xe3, 0x7f, 0x03, 0x00, 0x00, 0xff, 0xff, 0x13, 0xdb, 0x98, 0xf9, 0xb8, 0x08, 0x00,
0x00,
} }

View File

@ -134,6 +134,18 @@ message JobSpec {
// Describes the pod that will be created when executing a job. // Describes the pod that will be created when executing a job.
// 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 k8s.io.api.core.v1.PodTemplateSpec template = 6; optional k8s.io.api.core.v1.PodTemplateSpec template = 6;
// ttlSecondsAfterFinished limits the lifetime of a Job that has finished
// execution (either Complete or Failed). If this field is set,
// ttlSecondsAfterFinished after the Job finishes, it is eligible to be
// automatically deleted. When the Job is being deleted, its lifecycle
// guarantees (e.g. finalizers) will be honored. If this field is unset,
// the Job won't be automatically deleted. If this field is set to zero,
// the Job becomes eligible to be deleted immediately after it finishes.
// This field is alpha-level and is only honored by servers that enable the
// TTLAfterFinished feature.
// +optional
optional int32 ttlSecondsAfterFinished = 8;
} }
// JobStatus represents the current state of a Job. // JobStatus represents the current state of a Job.

View File

@ -114,6 +114,18 @@ type JobSpec struct {
// Describes the pod that will be created when executing a job. // Describes the pod that will be created when executing a job.
// 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/
Template v1.PodTemplateSpec `json:"template" protobuf:"bytes,6,opt,name=template"` Template v1.PodTemplateSpec `json:"template" protobuf:"bytes,6,opt,name=template"`
// ttlSecondsAfterFinished limits the lifetime of a Job that has finished
// execution (either Complete or Failed). If this field is set,
// ttlSecondsAfterFinished after the Job finishes, it is eligible to be
// automatically deleted. When the Job is being deleted, its lifecycle
// guarantees (e.g. finalizers) will be honored. If this field is unset,
// the Job won't be automatically deleted. If this field is set to zero,
// the Job becomes eligible to be deleted immediately after it finishes.
// This field is alpha-level and is only honored by servers that enable the
// TTLAfterFinished feature.
// +optional
TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty" protobuf:"varint,8,opt,name=ttlSecondsAfterFinished"`
} }
// JobStatus represents the current state of a Job. // JobStatus represents the current state of a Job.

View File

@ -63,14 +63,15 @@ func (JobList) SwaggerDoc() map[string]string {
} }
var map_JobSpec = map[string]string{ var map_JobSpec = map[string]string{
"": "JobSpec describes how the job execution will look like.", "": "JobSpec describes how the job execution will look like.",
"parallelism": "Specifies the maximum desired number of pods the job should run at any given time. The actual number of pods running in steady state will be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), i.e. when the work left to do is less than max parallelism. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", "parallelism": "Specifies the maximum desired number of pods the job should run at any given time. The actual number of pods running in steady state will be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), i.e. when the work left to do is less than max parallelism. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"completions": "Specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", "completions": "Specifies the desired number of successfully finished pods the job should be run with. Setting to nil means that the success of any pod signals the success of all pods, and allows parallelism to have any positive value. Setting to 1 means that parallelism is limited to 1 and the success of that pod signals the success of the job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"activeDeadlineSeconds": "Specifies the duration in seconds relative to the startTime that the job may be active before the system tries to terminate it; value must be positive integer", "activeDeadlineSeconds": "Specifies the duration in seconds relative to the startTime that the job may be active before the system tries to terminate it; value must be positive integer",
"backoffLimit": "Specifies the number of retries before marking this job failed. Defaults to 6", "backoffLimit": "Specifies the number of retries before marking this job failed. Defaults to 6",
"selector": "A label query over pods that should match the pod count. Normally, the system sets this field for you. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors", "selector": "A label query over pods that should match the pod count. Normally, the system sets this field for you. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors",
"manualSelector": "manualSelector controls generation of pod labels and pod selectors. Leave `manualSelector` unset unless you are certain what you are doing. When false or unset, the system pick labels unique to this job and appends those labels to the pod template. When true, the user is responsible for picking unique labels and specifying the selector. Failure to pick a unique label may cause this and other jobs to not function correctly. However, You may see `manualSelector=true` in jobs that were created with the old `extensions/v1beta1` API. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#specifying-your-own-pod-selector", "manualSelector": "manualSelector controls generation of pod labels and pod selectors. Leave `manualSelector` unset unless you are certain what you are doing. When false or unset, the system pick labels unique to this job and appends those labels to the pod template. When true, the user is responsible for picking unique labels and specifying the selector. Failure to pick a unique label may cause this and other jobs to not function correctly. However, You may see `manualSelector=true` in jobs that were created with the old `extensions/v1beta1` API. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/#specifying-your-own-pod-selector",
"template": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/", "template": "Describes the pod that will be created when executing a job. More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/",
"ttlSecondsAfterFinished": "ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed). If this field is set, ttlSecondsAfterFinished after the Job finishes, it is eligible to be automatically deleted. When the Job is being deleted, its lifecycle guarantees (e.g. finalizers) will be honored. If this field is unset, the Job won't be automatically deleted. If this field is set to zero, the Job becomes eligible to be deleted immediately after it finishes. This field is alpha-level and is only honored by servers that enable the TTLAfterFinished feature.",
} }
func (JobSpec) SwaggerDoc() map[string]string { func (JobSpec) SwaggerDoc() map[string]string {

View File

@ -138,6 +138,11 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) {
**out = **in **out = **in
} }
in.Template.DeepCopyInto(&out.Template) in.Template.DeepCopyInto(&out.Template)
if in.TTLSecondsAfterFinished != nil {
in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished
*out = new(int32)
**out = **in
}
return return
} }

View File

@ -145,6 +145,9 @@ type KubeControllerManagerConfiguration struct {
// ServiceControllerConfiguration holds configuration for ServiceController // ServiceControllerConfiguration holds configuration for ServiceController
// related features. // related features.
ServiceController ServiceControllerConfiguration ServiceController ServiceControllerConfiguration
// TTLAfterFinishedControllerConfiguration holds configuration for
// TTLAfterFinishedController related features.
TTLAfterFinishedController TTLAfterFinishedControllerConfiguration
} }
// GenericControllerManagerConfiguration holds configuration for a generic controller-manager. // GenericControllerManagerConfiguration holds configuration for a generic controller-manager.
@ -438,3 +441,10 @@ type ServiceControllerConfiguration struct {
// management, but more CPU (and network) load. // management, but more CPU (and network) load.
ConcurrentServiceSyncs int32 ConcurrentServiceSyncs int32
} }
// TTLAfterFinishedControllerConfiguration contains elements describing TTLAfterFinishedController.
type TTLAfterFinishedControllerConfiguration struct {
// concurrentTTLSyncs is the number of TTL-after-finished collector workers that are
// allowed to sync concurrently.
ConcurrentTTLSyncs int32
}

View File

@ -300,6 +300,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa
out.ResourceQuotaController = in.ResourceQuotaController out.ResourceQuotaController = in.ResourceQuotaController
out.SAController = in.SAController out.SAController = in.SAController
out.ServiceController = in.ServiceController out.ServiceController = in.ServiceController
out.TTLAfterFinishedController = in.TTLAfterFinishedController
return return
} }
@ -509,6 +510,22 @@ func (in *ServiceControllerConfiguration) DeepCopy() *ServiceControllerConfigura
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TTLAfterFinishedControllerConfiguration) DeepCopyInto(out *TTLAfterFinishedControllerConfiguration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TTLAfterFinishedControllerConfiguration.
func (in *TTLAfterFinishedControllerConfiguration) DeepCopy() *TTLAfterFinishedControllerConfiguration {
if in == nil {
return nil
}
out := new(TTLAfterFinishedControllerConfiguration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VolumeConfiguration) DeepCopyInto(out *VolumeConfiguration) { func (in *VolumeConfiguration) DeepCopyInto(out *VolumeConfiguration) {
*out = *in *out = *in

View File

@ -44,7 +44,7 @@ var _ = SIGDescribe("Job", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("Ensuring job reaches completions") By("Ensuring job reaches completions")
err = framework.WaitForJobFinish(f.ClientSet, f.Namespace.Name, job.Name, completions) err = framework.WaitForJobComplete(f.ClientSet, f.Namespace.Name, job.Name, completions)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
}) })
@ -63,7 +63,7 @@ var _ = SIGDescribe("Job", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("Ensuring job reaches completions") By("Ensuring job reaches completions")
err = framework.WaitForJobFinish(f.ClientSet, f.Namespace.Name, job.Name, completions) err = framework.WaitForJobComplete(f.ClientSet, f.Namespace.Name, job.Name, completions)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
}) })
@ -84,7 +84,7 @@ var _ = SIGDescribe("Job", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("Ensuring job reaches completions") By("Ensuring job reaches completions")
err = framework.WaitForJobFinish(f.ClientSet, f.Namespace.Name, job.Name, *job.Spec.Completions) err = framework.WaitForJobComplete(f.ClientSet, f.Namespace.Name, job.Name, *job.Spec.Completions)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
}) })

View File

@ -58,7 +58,7 @@ var _ = SIGDescribe("Metadata Concealment", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
By("Ensuring job reaches completions") By("Ensuring job reaches completions")
err = framework.WaitForJobFinish(f.ClientSet, f.Namespace.Name, job.Name, int32(1)) err = framework.WaitForJobComplete(f.ClientSet, f.Namespace.Name, job.Name, int32(1))
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
}) })
}) })

View File

@ -34,6 +34,7 @@ go_library(
"secrets_volume.go", "secrets_volume.go",
"security_context.go", "security_context.go",
"sysctl.go", "sysctl.go",
"ttlafterfinished.go",
"util.go", "util.go",
"volumes.go", "volumes.go",
], ],
@ -43,13 +44,16 @@ go_library(
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library", "//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/conditions:go_default_library", "//pkg/client/conditions:go_default_library",
"//pkg/features:go_default_library",
"//pkg/kubelet:go_default_library", "//pkg/kubelet:go_default_library",
"//pkg/kubelet/apis:go_default_library", "//pkg/kubelet/apis:go_default_library",
"//pkg/kubelet/images:go_default_library", "//pkg/kubelet/images:go_default_library",
"//pkg/kubelet/sysctl:go_default_library", "//pkg/kubelet/sysctl:go_default_library",
"//pkg/security/apparmor:go_default_library", "//pkg/security/apparmor:go_default_library",
"//pkg/util/slice:go_default_library",
"//pkg/util/version:go_default_library", "//pkg/util/version:go_default_library",
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library", "//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
"//staging/src/k8s.io/api/batch/v1:go_default_library",
"//staging/src/k8s.io/api/coordination/v1beta1:go_default_library", "//staging/src/k8s.io/api/coordination/v1beta1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
@ -65,6 +69,7 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/scale:go_default_library", "//staging/src/k8s.io/client-go/scale:go_default_library",
"//staging/src/k8s.io/client-go/tools/cache:go_default_library", "//staging/src/k8s.io/client-go/tools/cache:go_default_library",

View File

@ -0,0 +1,101 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package common
import (
"time"
batch "k8s.io/api/batch/v1"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/wait"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/util/slice"
"k8s.io/kubernetes/test/e2e/framework"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
const dummyFinalizer = "k8s.io/dummy-finalizer"
var _ = framework.KubeDescribe("TTLAfterFinished", func() {
f := framework.NewDefaultFramework("ttlafterfinished")
It("Job should be deleted once it finishes after TTL seconds [Feature:TTLAfterFinished]", func() {
if !utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) {
framework.Skipf("Skip because %s feature is not enabled; run test with --feature-gates=%s=true", features.TTLAfterFinished, features.TTLAfterFinished)
}
testFinishedJob(f)
})
})
func cleanupJob(f *framework.Framework, job *batch.Job) {
ns := f.Namespace.Name
c := f.ClientSet
framework.Logf("Remove the Job's dummy finalizer; the Job should be deleted cascadingly")
removeFinalizerFunc := func(j *batch.Job) {
j.ObjectMeta.Finalizers = slice.RemoveString(j.ObjectMeta.Finalizers, dummyFinalizer, nil)
}
_, err := framework.UpdateJobWithRetries(c, ns, job.Name, removeFinalizerFunc)
Expect(err).NotTo(HaveOccurred())
framework.WaitForJobGone(c, ns, job.Name, wait.ForeverTestTimeout)
err = framework.WaitForAllJobPodsGone(c, ns, job.Name)
Expect(err).NotTo(HaveOccurred())
}
func testFinishedJob(f *framework.Framework) {
ns := f.Namespace.Name
c := f.ClientSet
parallelism := int32(1)
completions := int32(1)
backoffLimit := int32(2)
ttl := int32(10)
job := framework.NewTestJob("randomlySucceedOrFail", "rand-non-local", v1.RestartPolicyNever, parallelism, completions, nil, backoffLimit)
job.Spec.TTLSecondsAfterFinished = &ttl
job.ObjectMeta.Finalizers = []string{dummyFinalizer}
defer cleanupJob(f, job)
framework.Logf("Create a Job %s/%s with TTL", job.Namespace, job.Name)
job, err := framework.CreateJob(c, ns, job)
Expect(err).NotTo(HaveOccurred())
framework.Logf("Wait for the Job to finish")
err = framework.WaitForJobFinish(c, ns, job.Name)
Expect(err).NotTo(HaveOccurred())
framework.Logf("Wait for TTL after finished controller to delete the Job")
err = framework.WaitForJobDeleting(c, ns, job.Name)
Expect(err).NotTo(HaveOccurred())
framework.Logf("Check Job's deletionTimestamp and compare with the time when the Job finished")
job, err = framework.GetJob(c, ns, job.Name)
Expect(err).NotTo(HaveOccurred())
finishTime := framework.JobFinishTime(job)
finishTimeUTC := finishTime.UTC()
Expect(finishTime.IsZero()).NotTo(BeTrue())
deleteAtUTC := job.ObjectMeta.DeletionTimestamp.UTC()
Expect(deleteAtUTC).NotTo(BeNil())
expireAtUTC := finishTimeUTC.Add(time.Duration(ttl) * time.Second)
Expect(deleteAtUTC.Before(expireAtUTC)).To(BeFalse())
}

View File

@ -59,6 +59,7 @@ go_library(
"//pkg/cloudprovider/providers/gce:go_default_library", "//pkg/cloudprovider/providers/gce:go_default_library",
"//pkg/controller:go_default_library", "//pkg/controller:go_default_library",
"//pkg/controller/deployment/util:go_default_library", "//pkg/controller/deployment/util:go_default_library",
"//pkg/controller/job:go_default_library",
"//pkg/controller/nodelifecycle:go_default_library", "//pkg/controller/nodelifecycle:go_default_library",
"//pkg/controller/service:go_default_library", "//pkg/controller/service:go_default_library",
"//pkg/features:go_default_library", "//pkg/features:go_default_library",

View File

@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
jobutil "k8s.io/kubernetes/pkg/controller/job"
) )
const ( const (
@ -181,8 +182,8 @@ func WaitForAllJobPodsRunning(c clientset.Interface, ns, jobName string, paralle
}) })
} }
// WaitForJobFinish uses c to wait for compeletions to complete for the Job jobName in namespace ns. // WaitForJobComplete uses c to wait for compeletions to complete for the Job jobName in namespace ns.
func WaitForJobFinish(c clientset.Interface, ns, jobName string, completions int32) error { func WaitForJobComplete(c clientset.Interface, ns, jobName string, completions int32) error {
return wait.Poll(Poll, JobTimeout, func() (bool, error) { return wait.Poll(Poll, JobTimeout, func() (bool, error) {
curr, err := c.BatchV1().Jobs(ns).Get(jobName, metav1.GetOptions{}) curr, err := c.BatchV1().Jobs(ns).Get(jobName, metav1.GetOptions{})
if err != nil { if err != nil {
@ -192,6 +193,17 @@ func WaitForJobFinish(c clientset.Interface, ns, jobName string, completions int
}) })
} }
// WaitForJobFinish uses c to wait for the Job jobName in namespace ns to finish (either Failed or Complete).
func WaitForJobFinish(c clientset.Interface, ns, jobName string) error {
return wait.PollImmediate(Poll, JobTimeout, func() (bool, error) {
curr, err := c.BatchV1().Jobs(ns).Get(jobName, metav1.GetOptions{})
if err != nil {
return false, err
}
return jobutil.IsJobFinished(curr), nil
})
}
// WaitForJobFailure uses c to wait for up to timeout for the Job named jobName in namespace ns to fail. // WaitForJobFailure uses c to wait for up to timeout for the Job named jobName in namespace ns to fail.
func WaitForJobFailure(c clientset.Interface, ns, jobName string, timeout time.Duration, reason string) error { func WaitForJobFailure(c clientset.Interface, ns, jobName string, timeout time.Duration, reason string) error {
return wait.Poll(Poll, timeout, func() (bool, error) { return wait.Poll(Poll, timeout, func() (bool, error) {
@ -239,6 +251,18 @@ func CheckForAllJobPodsRunning(c clientset.Interface, ns, jobName string, parall
return count == parallelism, nil return count == parallelism, nil
} }
// WaitForAllJobPodsRunning wait for all pods for the Job named jobName in namespace ns
// to be deleted.
func WaitForAllJobPodsGone(c clientset.Interface, ns, jobName string) error {
return wait.PollImmediate(Poll, JobTimeout, func() (bool, error) {
pods, err := GetJobPods(c, ns, jobName)
if err != nil {
return false, err
}
return len(pods.Items) == 0, nil
})
}
func newBool(val bool) *bool { func newBool(val bool) *bool {
p := new(bool) p := new(bool)
*p = val *p = val
@ -250,7 +274,7 @@ type updateJobFunc func(*batch.Job)
func UpdateJobWithRetries(c clientset.Interface, namespace, name string, applyUpdate updateJobFunc) (job *batch.Job, err error) { func UpdateJobWithRetries(c clientset.Interface, namespace, name string, applyUpdate updateJobFunc) (job *batch.Job, err error) {
jobs := c.BatchV1().Jobs(namespace) jobs := c.BatchV1().Jobs(namespace)
var updateErr error var updateErr error
pollErr := wait.PollImmediate(10*time.Millisecond, 1*time.Minute, func() (bool, error) { pollErr := wait.PollImmediate(Poll, JobTimeout, func() (bool, error) {
if job, err = jobs.Get(name, metav1.GetOptions{}); err != nil { if job, err = jobs.Get(name, metav1.GetOptions{}); err != nil {
return false, err return false, err
} }
@ -268,3 +292,25 @@ func UpdateJobWithRetries(c clientset.Interface, namespace, name string, applyUp
} }
return job, pollErr return job, pollErr
} }
// WaitForJobDeleting uses c to wait for the Job jobName in namespace ns to have
// a non-nil deletionTimestamp (i.e. being deleted).
func WaitForJobDeleting(c clientset.Interface, ns, jobName string) error {
return wait.PollImmediate(Poll, JobTimeout, func() (bool, error) {
curr, err := c.BatchV1().Jobs(ns).Get(jobName, metav1.GetOptions{})
if err != nil {
return false, err
}
return curr.ObjectMeta.DeletionTimestamp != nil, nil
})
}
func JobFinishTime(finishedJob *batch.Job) metav1.Time {
var finishTime metav1.Time
for _, c := range finishedJob.Status.Conditions {
if (c.Type == batch.JobComplete || c.Type == batch.JobFailed) && c.Status == v1.ConditionTrue {
return c.LastTransitionTime
}
}
return finishTime
}