diff --git a/api/api-rules/violation_exceptions.list b/api/api-rules/violation_exceptions.list index df8bfded969..8add67dc09a 100644 --- a/api/api-rules/violation_exceptions.list +++ b/api/api-rules/violation_exceptions.list @@ -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,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,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,ConcurrentNamespaceSyncs 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,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,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,EnableDynamicProvisioning API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,VolumeConfiguration,PersistentVolumeRecyclerConfiguration diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 5142b1ff06e..53d1c1cc535 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -79306,6 +79306,11 @@ "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/", "$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" } } }, diff --git a/api/swagger-spec/batch_v1.json b/api/swagger-spec/batch_v1.json index 4669972e178..1c7f36766f6 100644 --- a/api/swagger-spec/batch_v1.json +++ b/api/swagger-spec/batch_v1.json @@ -1536,6 +1536,11 @@ "template": { "$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/" + }, + "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." } } }, diff --git a/api/swagger-spec/batch_v1beta1.json b/api/swagger-spec/batch_v1beta1.json index 3487425e287..b43f6107c15 100644 --- a/api/swagger-spec/batch_v1beta1.json +++ b/api/swagger-spec/batch_v1beta1.json @@ -1591,6 +1591,11 @@ "template": { "$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/" + }, + "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." } } }, diff --git a/api/swagger-spec/batch_v2alpha1.json b/api/swagger-spec/batch_v2alpha1.json index ff9d51385b1..044d864f92a 100644 --- a/api/swagger-spec/batch_v2alpha1.json +++ b/api/swagger-spec/batch_v2alpha1.json @@ -1591,6 +1591,11 @@ "template": { "$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/" + }, + "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." } } }, diff --git a/cmd/kube-controller-manager/app/BUILD b/cmd/kube-controller-manager/app/BUILD index bbdcb19a044..f183dda468d 100644 --- a/cmd/kube-controller-manager/app/BUILD +++ b/cmd/kube-controller-manager/app/BUILD @@ -68,6 +68,7 @@ go_library( "//pkg/controller/serviceaccount:go_default_library", "//pkg/controller/statefulset: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/expand:go_default_library", "//pkg/controller/volume/persistentvolume:go_default_library", diff --git a/cmd/kube-controller-manager/app/controllermanager.go b/cmd/kube-controller-manager/app/controllermanager.go index 12e7c35faea..0f33ec168c0 100644 --- a/cmd/kube-controller-manager/app/controllermanager.go +++ b/cmd/kube-controller-manager/app/controllermanager.go @@ -378,6 +378,7 @@ func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc controllers["clusterrole-aggregation"] = startClusterRoleAggregrationController controllers["pvc-protection"] = startPVCProtectionController controllers["pv-protection"] = startPVProtectionController + controllers["ttl-after-finished"] = startTTLAfterFinishedController return controllers } diff --git a/cmd/kube-controller-manager/app/core.go b/cmd/kube-controller-manager/app/core.go index 92700988766..f6a273385a8 100644 --- a/cmd/kube-controller-manager/app/core.go +++ b/cmd/kube-controller-manager/app/core.go @@ -52,6 +52,7 @@ import ( servicecontroller "k8s.io/kubernetes/pkg/controller/service" serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount" 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/expand" persistentvolumecontroller "k8s.io/kubernetes/pkg/controller/volume/persistentvolume" @@ -417,3 +418,14 @@ func startPVProtectionController(ctx ControllerContext) (http.Handler, bool, err ).Run(1, ctx.Stop) 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 +} diff --git a/cmd/kube-controller-manager/app/options/BUILD b/cmd/kube-controller-manager/app/options/BUILD index 23d29b8bc0e..fcae9358cf7 100644 --- a/cmd/kube-controller-manager/app/options/BUILD +++ b/cmd/kube-controller-manager/app/options/BUILD @@ -28,6 +28,7 @@ go_library( "replicationcontroller.go", "resourcequotacontroller.go", "serviceaccountcontroller.go", + "ttlafterfinishedcontroller.go", ], importpath = "k8s.io/kubernetes/cmd/kube-controller-manager/app/options", deps = [ diff --git a/cmd/kube-controller-manager/app/options/options.go b/cmd/kube-controller-manager/app/options/options.go index 56178e9ed5a..22cf24a12e3 100644 --- a/cmd/kube-controller-manager/app/options/options.go +++ b/cmd/kube-controller-manager/app/options/options.go @@ -77,6 +77,7 @@ type KubeControllerManagerOptions struct { ReplicationController *ReplicationControllerOptions ResourceQuotaController *ResourceQuotaControllerOptions SAController *SAControllerOptions + TTLAfterFinishedController *TTLAfterFinishedControllerOptions SecureServing *apiserveroptions.SecureServingOptionsWithLoopback // TODO: remove insecure serving mode @@ -172,6 +173,9 @@ func NewKubeControllerManagerOptions() (*KubeControllerManagerOptions, error) { ServiceController: &cmoptions.ServiceControllerOptions{ ConcurrentServiceSyncs: componentConfig.ServiceController.ConcurrentServiceSyncs, }, + TTLAfterFinishedController: &TTLAfterFinishedControllerOptions{ + ConcurrentTTLSyncs: componentConfig.TTLAfterFinishedController.ConcurrentTTLSyncs, + }, SecureServing: apiserveroptions.NewSecureServingOptions().WithLoopback(), InsecureServing: (&apiserveroptions.DeprecatedInsecureServingOptions{ BindAddress: net.ParseIP(componentConfig.Generic.Address), @@ -251,6 +255,7 @@ func (s *KubeControllerManagerOptions) Flags(allControllers []string, disabledBy s.ReplicationController.AddFlags(fss.FlagSet("replicationcontroller")) s.ResourceQuotaController.AddFlags(fss.FlagSet("resourcequota controller")) s.SAController.AddFlags(fss.FlagSet("serviceaccount controller")) + s.TTLAfterFinishedController.AddFlags(fss.FlagSet("ttl-after-finished controller")) fs := fss.FlagSet("misc") 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 { 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 { return err } @@ -376,6 +384,7 @@ func (s *KubeControllerManagerOptions) Validate(allControllers []string, disable errs = append(errs, s.ResourceQuotaController.Validate()...) errs = append(errs, s.SAController.Validate()...) errs = append(errs, s.ServiceController.Validate()...) + errs = append(errs, s.TTLAfterFinishedController.Validate()...) errs = append(errs, s.SecureServing.Validate()...) errs = append(errs, s.InsecureServing.Validate()...) errs = append(errs, s.Authentication.Validate()...) diff --git a/cmd/kube-controller-manager/app/options/options_test.go b/cmd/kube-controller-manager/app/options/options_test.go index 5a59471ac44..4129038be7b 100644 --- a/cmd/kube-controller-manager/app/options/options_test.go +++ b/cmd/kube-controller-manager/app/options/options_test.go @@ -116,6 +116,7 @@ func TestAddFlags(t *testing.T) { "--cert-dir=/a/b/c", "--bind-address=192.168.4.21", "--secure-port=10001", + "--concurrent-ttl-after-finished-syncs=8", } fs.Parse(args) // Sort GCIgnoredResources because it's built from a map, which means the @@ -255,6 +256,9 @@ func TestAddFlags(t *testing.T) { ServiceController: &cmoptions.ServiceControllerOptions{ ConcurrentServiceSyncs: 2, }, + TTLAfterFinishedController: &TTLAfterFinishedControllerOptions{ + ConcurrentTTLSyncs: 8, + }, SecureServing: (&apiserveroptions.SecureServingOptions{ BindPort: 10001, BindAddress: net.ParseIP("192.168.4.21"), diff --git a/cmd/kube-controller-manager/app/options/ttlafterfinishedcontroller.go b/cmd/kube-controller-manager/app/options/ttlafterfinishedcontroller.go new file mode 100644 index 00000000000..2f7fe765eac --- /dev/null +++ b/cmd/kube-controller-manager/app/options/ttlafterfinishedcontroller.go @@ -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 +} diff --git a/docs/api-reference/batch/v1/definitions.html b/docs/api-reference/batch/v1/definitions.html index 14b6e918979..dcd059bbc4d 100755 --- a/docs/api-reference/batch/v1/definitions.html +++ b/docs/api-reference/batch/v1/definitions.html @@ -2505,6 +2505,13 @@ When an object is created, the system will populate this list with the current s

v1.PodTemplateSpec

+ +

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.

+

false

+

integer (int32)

+ + diff --git a/docs/api-reference/batch/v1beta1/definitions.html b/docs/api-reference/batch/v1beta1/definitions.html index 148aa8bcf8e..c3bca7454ef 100755 --- a/docs/api-reference/batch/v1beta1/definitions.html +++ b/docs/api-reference/batch/v1beta1/definitions.html @@ -2539,6 +2539,13 @@ When an object is created, the system will populate this list with the current s

v1.PodTemplateSpec

+ +

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.

+

false

+

integer (int32)

+ + diff --git a/docs/api-reference/batch/v2alpha1/definitions.html b/docs/api-reference/batch/v2alpha1/definitions.html index 950d2d5cf86..51a06d431be 100755 --- a/docs/api-reference/batch/v2alpha1/definitions.html +++ b/docs/api-reference/batch/v2alpha1/definitions.html @@ -2512,6 +2512,13 @@ When an object is created, the system will populate this list with the current s

v1.PodTemplateSpec

+ +

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.

+

false

+

integer (int32)

+ + diff --git a/pkg/apis/batch/types.go b/pkg/apis/batch/types.go index 32d921fb6af..d62cdaf3bbb 100644 --- a/pkg/apis/batch/types.go +++ b/pkg/apis/batch/types.go @@ -138,6 +138,18 @@ type JobSpec struct { // Describes the pod that will be created when executing a job. 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. diff --git a/pkg/apis/batch/v1/conversion.go b/pkg/apis/batch/v1/conversion.go index 9c20fa93702..7c715542367 100644 --- a/pkg/apis/batch/v1/conversion.go +++ b/pkg/apis/batch/v1/conversion.go @@ -54,6 +54,7 @@ func Convert_batch_JobSpec_To_v1_JobSpec(in *batch.JobSpec, out *batchv1.JobSpec out.Completions = in.Completions out.ActiveDeadlineSeconds = in.ActiveDeadlineSeconds out.BackoffLimit = in.BackoffLimit + out.TTLSecondsAfterFinished = in.TTLSecondsAfterFinished out.Selector = in.Selector if in.ManualSelector != nil { 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.ActiveDeadlineSeconds = in.ActiveDeadlineSeconds out.BackoffLimit = in.BackoffLimit + out.TTLSecondsAfterFinished = in.TTLSecondsAfterFinished out.Selector = in.Selector if in.ManualSelector != nil { out.ManualSelector = new(bool) diff --git a/pkg/apis/batch/v1/zz_generated.conversion.go b/pkg/apis/batch/v1/zz_generated.conversion.go index 55a56426e1d..066f30ee30b 100644 --- a/pkg/apis/batch/v1/zz_generated.conversion.go +++ b/pkg/apis/batch/v1/zz_generated.conversion.go @@ -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 { return err } + out.TTLSecondsAfterFinished = (*int32)(unsafe.Pointer(in.TTLSecondsAfterFinished)) 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 { return err } + out.TTLSecondsAfterFinished = (*int32)(unsafe.Pointer(in.TTLSecondsAfterFinished)) return nil } diff --git a/pkg/apis/batch/validation/BUILD b/pkg/apis/batch/validation/BUILD index b71dec02963..d371966a354 100644 --- a/pkg/apis/batch/validation/BUILD +++ b/pkg/apis/batch/validation/BUILD @@ -14,11 +14,13 @@ go_library( "//pkg/apis/batch:go_default_library", "//pkg/apis/core: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/validation: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/field:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/github.com/robfig/cron:go_default_library", ], ) @@ -30,8 +32,10 @@ go_test( deps = [ "//pkg/apis/batch: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/types:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", ], ) diff --git a/pkg/apis/batch/validation/validation.go b/pkg/apis/batch/validation/validation.go index a31bf6416e4..1e04e9779e5 100644 --- a/pkg/apis/batch/validation/validation.go +++ b/pkg/apis/batch/validation/validation.go @@ -24,9 +24,11 @@ import ( "k8s.io/apimachinery/pkg/labels" apimachineryvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/apis/batch" api "k8s.io/kubernetes/pkg/apis/core" 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 @@ -117,6 +119,14 @@ func validateJobSpec(spec *batch.JobSpec, fldPath *field.Path) field.ErrorList { if spec.BackoffLimit != nil { 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"))...) if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && diff --git a/pkg/apis/batch/validation/validation_test.go b/pkg/apis/batch/validation/validation_test.go index 13135e2ea53..7ff9e3f73ea 100644 --- a/pkg/apis/batch/validation/validation_test.go +++ b/pkg/apis/batch/validation/validation_test.go @@ -17,13 +17,16 @@ limitations under the License. package validation import ( + "fmt" "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/apis/batch" api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" ) 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) { + 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() validPodTemplateSpecForManual := getValidPodTemplateSpecForManual(validManualSelector) validGeneratedSelector := getValidGeneratedSelector() @@ -214,15 +231,39 @@ func TestValidateJob(t *testing.T) { }, } - for k, v := range errorCases { - errs := ValidateJob(&v) - if len(errs) == 0 { - t.Errorf("expected failure for %s", k) + for _, setFeature := range featureToggle(features.TTLAfterFinished) { + // Set error cases based on if TTLAfterFinished feature is enabled or not + if err := utilfeature.DefaultFeatureGate.Set(setFeature); err != nil { + 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 { - 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) + delete(errorCases, ttlCase) + } + + 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 { errs := ValidateCronJob(&v) diff --git a/pkg/apis/batch/zz_generated.deepcopy.go b/pkg/apis/batch/zz_generated.deepcopy.go index ea986d3d7cb..492b1699e9e 100644 --- a/pkg/apis/batch/zz_generated.deepcopy.go +++ b/pkg/apis/batch/zz_generated.deepcopy.go @@ -262,6 +262,11 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) { **out = **in } in.Template.DeepCopyInto(&out.Template) + if in.TTLSecondsAfterFinished != nil { + in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished + *out = new(int32) + **out = **in + } return } diff --git a/pkg/controller/BUILD b/pkg/controller/BUILD index 17f4fba639b..e5c8dc7695f 100644 --- a/pkg/controller/BUILD +++ b/pkg/controller/BUILD @@ -131,6 +131,7 @@ filegroup( "//pkg/controller/statefulset:all-srcs", "//pkg/controller/testutil:all-srcs", "//pkg/controller/ttl:all-srcs", + "//pkg/controller/ttlafterfinished:all-srcs", "//pkg/controller/util/node:all-srcs", "//pkg/controller/volume/attachdetach:all-srcs", "//pkg/controller/volume/events:all-srcs", diff --git a/pkg/controller/apis/config/types.go b/pkg/controller/apis/config/types.go index b7abe6b9234..c04a1b3552d 100644 --- a/pkg/controller/apis/config/types.go +++ b/pkg/controller/apis/config/types.go @@ -96,6 +96,9 @@ type KubeControllerManagerConfiguration struct { // ServiceControllerConfiguration holds configuration for ServiceController // related features. ServiceController ServiceControllerConfiguration + // TTLAfterFinishedControllerConfiguration holds configuration for + // TTLAfterFinishedController related features. + TTLAfterFinishedController TTLAfterFinishedControllerConfiguration } // GenericControllerManagerConfiguration holds configuration for a generic controller-manager @@ -438,3 +441,10 @@ type PersistentVolumeRecyclerConfiguration struct { // in a multi-node cluster. 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 +} diff --git a/pkg/controller/apis/config/v1alpha1/defaults.go b/pkg/controller/apis/config/v1alpha1/defaults.go index 6e51aa964d2..af965725660 100644 --- a/pkg/controller/apis/config/v1alpha1/defaults.go +++ b/pkg/controller/apis/config/v1alpha1/defaults.go @@ -48,6 +48,9 @@ func SetDefaults_KubeControllerManagerConfiguration(obj *kubectrlmgrconfigv1alph if obj.SAController.ConcurrentSATokenSyncs == 0 { 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 client-connection defaults are specific to the kube-controller-manager diff --git a/pkg/controller/apis/config/v1alpha1/zz_generated.conversion.go b/pkg/controller/apis/config/v1alpha1/zz_generated.conversion.go index 70b6319bd09..1e7718d084c 100644 --- a/pkg/controller/apis/config/v1alpha1/zz_generated.conversion.go +++ b/pkg/controller/apis/config/v1alpha1/zz_generated.conversion.go @@ -289,6 +289,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { 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 { return Convert_v1alpha1_VolumeConfiguration_To_config_VolumeConfiguration(a.(*v1alpha1.VolumeConfiguration), b.(*config.VolumeConfiguration), scope) }); 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 { return err } + if err := Convert_v1alpha1_TTLAfterFinishedControllerConfiguration_To_config_TTLAfterFinishedControllerConfiguration(&in.TTLAfterFinishedController, &out.TTLAfterFinishedController, s); err != nil { + return err + } 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 { return err } + if err := Convert_config_TTLAfterFinishedControllerConfiguration_To_v1alpha1_TTLAfterFinishedControllerConfiguration(&in.TTLAfterFinishedController, &out.TTLAfterFinishedController, s); err != nil { + return err + } return nil } @@ -1070,6 +1086,26 @@ func autoConvert_config_ServiceControllerConfiguration_To_v1alpha1_ServiceContro 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 { if err := v1.Convert_Pointer_bool_To_bool(&in.EnableHostPathProvisioning, &out.EnableHostPathProvisioning, s); err != nil { return err diff --git a/pkg/controller/apis/config/zz_generated.deepcopy.go b/pkg/controller/apis/config/zz_generated.deepcopy.go index a859a3ef830..caebbe8789a 100644 --- a/pkg/controller/apis/config/zz_generated.deepcopy.go +++ b/pkg/controller/apis/config/zz_generated.deepcopy.go @@ -285,6 +285,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa out.ResourceQuotaController = in.ResourceQuotaController out.SAController = in.SAController out.ServiceController = in.ServiceController + out.TTLAfterFinishedController = in.TTLAfterFinishedController return } @@ -489,6 +490,22 @@ func (in *ServiceControllerConfiguration) DeepCopy() *ServiceControllerConfigura 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. func (in *VolumeConfiguration) DeepCopyInto(out *VolumeConfiguration) { *out = *in diff --git a/pkg/controller/ttlafterfinished/BUILD b/pkg/controller/ttlafterfinished/BUILD new file mode 100644 index 00000000000..3eda34912f6 --- /dev/null +++ b/pkg/controller/ttlafterfinished/BUILD @@ -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"], +) diff --git a/pkg/controller/ttlafterfinished/ttlafterfinished_controller.go b/pkg/controller/ttlafterfinished/ttlafterfinished_controller.go new file mode 100644 index 00000000000..99c5e625fdc --- /dev/null +++ b/pkg/controller/ttlafterfinished/ttlafterfinished_controller.go @@ -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) +} diff --git a/pkg/controller/ttlafterfinished/ttlafterfinished_controller_test.go b/pkg/controller/ttlafterfinished/ttlafterfinished_controller_test.go new file mode 100644 index 00000000000..b5908955aa1 --- /dev/null +++ b/pkg/controller/ttlafterfinished/ttlafterfinished_controller_test.go @@ -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) + } + } + } +} diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 9fdefcf71d5..56a7ed33e7d 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -381,6 +381,12 @@ const ( // // Enables control over ProcMountType for containers. 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() { @@ -445,6 +451,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS SCTPSupport: {Default: false, PreRelease: utilfeature.Alpha}, VolumeSnapshotDataSource: {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 // unintentionally on either side: diff --git a/pkg/registry/batch/job/BUILD b/pkg/registry/batch/job/BUILD index 5f2ccc0ab99..30474db1f3c 100644 --- a/pkg/registry/batch/job/BUILD +++ b/pkg/registry/batch/job/BUILD @@ -18,6 +18,7 @@ go_library( "//pkg/api/pod:go_default_library", "//pkg/apis/batch: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/fields: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/storage: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/apis/batch: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/types: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/util/feature:go_default_library", ], ) diff --git a/pkg/registry/batch/job/strategy.go b/pkg/registry/batch/job/strategy.go index c0eb2be5f53..4e46ddc4b37 100644 --- a/pkg/registry/batch/job/strategy.go +++ b/pkg/registry/batch/job/strategy.go @@ -30,10 +30,12 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/names" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/pod" "k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/batch/validation" + "k8s.io/kubernetes/pkg/features" ) // 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.Status = batch.JobStatus{} + if !utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) { + job.Spec.TTLSecondsAfterFinished = nil + } + pod.DropDisabledAlphaFields(&job.Spec.Template.Spec) } @@ -70,6 +76,11 @@ func (jobStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object oldJob := old.(*batch.Job) 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(&oldJob.Spec.Template.Spec) } diff --git a/pkg/registry/batch/job/strategy_test.go b/pkg/registry/batch/job/strategy_test.go index f8cfc609196..e3b51678c15 100644 --- a/pkg/registry/batch/job/strategy_test.go +++ b/pkg/registry/batch/job/strategy_test.go @@ -24,19 +24,24 @@ import ( "k8s.io/apimachinery/pkg/types" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/testapi" apitesting "k8s.io/kubernetes/pkg/api/testing" "k8s.io/kubernetes/pkg/apis/batch" api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" ) func newBool(a bool) *bool { - r := new(bool) - *r = a - return r + return &a +} + +func newInt32(i int32) *int32 { + return &i } func TestJobStrategy(t *testing.T) { + ttlEnabled := utilfeature.DefaultFeatureGate.Enabled(features.TTLAfterFinished) ctx := genericapirequest.NewDefaultContext() if !Strategy.NamespaceScoped() { t.Errorf("Job must be namespace scoped") @@ -64,9 +69,10 @@ func TestJobStrategy(t *testing.T) { Namespace: metav1.NamespaceDefault, }, Spec: batch.JobSpec{ - Selector: validSelector, - Template: validPodTemplateSpec, - ManualSelector: newBool(true), + Selector: validSelector, + Template: validPodTemplateSpec, + TTLSecondsAfterFinished: newInt32(0), // Set TTL + ManualSelector: newBool(true), }, Status: batch.JobStatus{ Active: 11, @@ -81,11 +87,21 @@ func TestJobStrategy(t *testing.T) { if len(errs) != 0 { 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) updatedJob := &batch.Job{ ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "4"}, Spec: batch.JobSpec{ - Parallelism: ¶llelism, + Parallelism: ¶llelism, + TTLSecondsAfterFinished: newInt32(1), // Update TTL }, Status: batch.JobStatus{ Active: 11, @@ -101,6 +117,9 @@ func TestJobStrategy(t *testing.T) { if len(errs) == 0 { 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. // Otherwise a typo could silently change the default. diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go index 5dfbaa6f23b..ca81758fea4 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go @@ -340,6 +340,15 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding) 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 } diff --git a/staging/src/k8s.io/api/batch/v1/generated.pb.go b/staging/src/k8s.io/api/batch/v1/generated.pb.go index aa5cdf89b68..097a6ff28c5 100644 --- a/staging/src/k8s.io/api/batch/v1/generated.pb.go +++ b/staging/src/k8s.io/api/batch/v1/generated.pb.go @@ -276,6 +276,11 @@ func (m *JobSpec) MarshalTo(dAtA []byte) (int, error) { i++ 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 } @@ -433,6 +438,9 @@ func (m *JobSpec) Size() (n int) { if m.BackoffLimit != nil { n += 1 + sovGenerated(uint64(*m.BackoffLimit)) } + if m.TTLSecondsAfterFinished != nil { + n += 1 + sovGenerated(uint64(*m.TTLSecondsAfterFinished)) + } return n } @@ -522,6 +530,7 @@ func (this *JobSpec) String() string { `ManualSelector:` + valueToStringGenerated(this.ManualSelector) + `,`, `Template:` + strings.Replace(strings.Replace(this.Template.String(), "PodTemplateSpec", "k8s_io_api_core_v1.PodTemplateSpec", 1), `&`, ``, 1) + `,`, `BackoffLimit:` + valueToStringGenerated(this.BackoffLimit) + `,`, + `TTLSecondsAfterFinished:` + valueToStringGenerated(this.TTLSecondsAfterFinished) + `,`, `}`, }, "") return s @@ -1219,6 +1228,26 @@ func (m *JobSpec) Unmarshal(dAtA []byte) error { } } 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: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -1554,61 +1583,64 @@ func init() { } var fileDescriptorGenerated = []byte{ - // 893 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x41, 0x6f, 0xe3, 0x44, - 0x18, 0x8d, 0x9b, 0xa6, 0x4d, 0x26, 0x69, 0xb7, 0x0c, 0xaa, 0x14, 0x2a, 0xe4, 0x2c, 0x41, 0x42, - 0x05, 0x09, 0x9b, 0x94, 0x0a, 0x21, 0x04, 0x48, 0xb8, 0x68, 0x25, 0xaa, 0x54, 0x5b, 0x26, 0x45, - 0x48, 0x08, 0x24, 0xc6, 0xf6, 0x97, 0xd4, 0xc4, 0xf6, 0x58, 0x9e, 0x49, 0xa4, 0xde, 0xf8, 0x09, - 0xfc, 0x08, 0xc4, 0x4f, 0x41, 0x3d, 0xee, 0x71, 0x4f, 0x11, 0x35, 0xdc, 0xb9, 0xef, 0x09, 0xcd, - 0x78, 0x62, 0x3b, 0x6d, 0x2a, 0xda, 0xbd, 0x79, 0xde, 0xbc, 0xf7, 0xbe, 0xf1, 0x37, 0x6f, 0x3e, - 0xf4, 0xf9, 0xf4, 0x53, 0x6e, 0x05, 0xcc, 0x9e, 0xce, 0x5c, 0x48, 0x63, 0x10, 0xc0, 0xed, 0x39, - 0xc4, 0x3e, 0x4b, 0x6d, 0xbd, 0x41, 0x93, 0xc0, 0x76, 0xa9, 0xf0, 0x2e, 0xed, 0xf9, 0xc0, 0x9e, - 0x40, 0x0c, 0x29, 0x15, 0xe0, 0x5b, 0x49, 0xca, 0x04, 0xc3, 0x6f, 0xe6, 0x24, 0x8b, 0x26, 0x81, - 0xa5, 0x48, 0xd6, 0x7c, 0x70, 0xf0, 0xe1, 0x24, 0x10, 0x97, 0x33, 0xd7, 0xf2, 0x58, 0x64, 0x4f, - 0xd8, 0x84, 0xd9, 0x8a, 0xeb, 0xce, 0xc6, 0x6a, 0xa5, 0x16, 0xea, 0x2b, 0xf7, 0x38, 0xe8, 0x57, - 0x0a, 0x79, 0x2c, 0x85, 0x35, 0x75, 0x0e, 0x8e, 0x4b, 0x4e, 0x44, 0xbd, 0xcb, 0x20, 0x86, 0xf4, - 0xca, 0x4e, 0xa6, 0x13, 0x09, 0x70, 0x3b, 0x02, 0x41, 0xd7, 0xa9, 0xec, 0xfb, 0x54, 0xe9, 0x2c, - 0x16, 0x41, 0x04, 0x77, 0x04, 0x9f, 0xfc, 0x9f, 0x80, 0x7b, 0x97, 0x10, 0xd1, 0xdb, 0xba, 0xfe, - 0xbf, 0x06, 0xaa, 0x9f, 0x32, 0x17, 0xff, 0x8c, 0x9a, 0xf2, 0x2c, 0x3e, 0x15, 0xb4, 0x6b, 0x3c, - 0x35, 0x0e, 0xdb, 0x47, 0x1f, 0x59, 0x65, 0x87, 0x0a, 0x4b, 0x2b, 0x99, 0x4e, 0x24, 0xc0, 0x2d, - 0xc9, 0xb6, 0xe6, 0x03, 0xeb, 0xb9, 0xfb, 0x0b, 0x78, 0xe2, 0x0c, 0x04, 0x75, 0xf0, 0xf5, 0xa2, - 0x57, 0xcb, 0x16, 0x3d, 0x54, 0x62, 0xa4, 0x70, 0xc5, 0x5f, 0xa2, 0x4d, 0x9e, 0x80, 0xd7, 0xdd, - 0x50, 0xee, 0x6f, 0x5b, 0x6b, 0xfa, 0x6f, 0x9d, 0x32, 0x77, 0x94, 0x80, 0xe7, 0x74, 0xb4, 0xd3, - 0xa6, 0x5c, 0x11, 0xa5, 0xc3, 0xcf, 0xd0, 0x16, 0x17, 0x54, 0xcc, 0x78, 0xb7, 0xae, 0x1c, 0xcc, - 0x7b, 0x1d, 0x14, 0xcb, 0xd9, 0xd5, 0x1e, 0x5b, 0xf9, 0x9a, 0x68, 0x75, 0xff, 0xcf, 0x3a, 0xea, - 0x9c, 0x32, 0xf7, 0x84, 0xc5, 0x7e, 0x20, 0x02, 0x16, 0xe3, 0x63, 0xb4, 0x29, 0xae, 0x12, 0x50, - 0xbf, 0xdd, 0x72, 0x9e, 0x2e, 0x4b, 0x5f, 0x5c, 0x25, 0xf0, 0x6a, 0xd1, 0xdb, 0xab, 0x72, 0x25, - 0x46, 0x14, 0x1b, 0x0f, 0x8b, 0xe3, 0x6c, 0x28, 0xdd, 0xf1, 0x6a, 0xb9, 0x57, 0x8b, 0xde, 0x9a, - 0x74, 0x58, 0x85, 0xd3, 0xea, 0xa1, 0xf0, 0x04, 0xed, 0x84, 0x94, 0x8b, 0xf3, 0x94, 0xb9, 0x70, - 0x11, 0x44, 0xa0, 0xff, 0xf1, 0x83, 0x87, 0xdd, 0x81, 0x54, 0x38, 0xfb, 0xfa, 0x00, 0x3b, 0xc3, - 0xaa, 0x11, 0x59, 0xf5, 0xc5, 0x73, 0x84, 0x25, 0x70, 0x91, 0xd2, 0x98, 0xe7, 0xbf, 0x24, 0xab, - 0x6d, 0x3e, 0xba, 0xda, 0x81, 0xae, 0x86, 0x87, 0x77, 0xdc, 0xc8, 0x9a, 0x0a, 0xf8, 0x3d, 0xb4, - 0x95, 0x02, 0xe5, 0x2c, 0xee, 0x36, 0x54, 0xbb, 0x8a, 0xdb, 0x21, 0x0a, 0x25, 0x7a, 0x17, 0xbf, - 0x8f, 0xb6, 0x23, 0xe0, 0x9c, 0x4e, 0xa0, 0xbb, 0xa5, 0x88, 0x4f, 0x34, 0x71, 0xfb, 0x2c, 0x87, - 0xc9, 0x72, 0xbf, 0xff, 0x87, 0x81, 0xb6, 0x4f, 0x99, 0x3b, 0x0c, 0xb8, 0xc0, 0x3f, 0xde, 0x89, - 0xaf, 0xf5, 0xb0, 0x9f, 0x91, 0x6a, 0x15, 0xde, 0x3d, 0x5d, 0xa7, 0xb9, 0x44, 0x2a, 0xd1, 0xfd, - 0x02, 0x35, 0x02, 0x01, 0x91, 0xbc, 0xea, 0xfa, 0x61, 0xfb, 0xa8, 0x7b, 0x5f, 0xf2, 0x9c, 0x1d, - 0x6d, 0xd2, 0xf8, 0x46, 0xd2, 0x49, 0xae, 0xea, 0xff, 0x53, 0x57, 0x07, 0x95, 0x59, 0xc6, 0x03, - 0xd4, 0x4e, 0x68, 0x4a, 0xc3, 0x10, 0xc2, 0x80, 0x47, 0xea, 0xac, 0x0d, 0xe7, 0x49, 0xb6, 0xe8, - 0xb5, 0xcf, 0x4b, 0x98, 0x54, 0x39, 0x52, 0xe2, 0xb1, 0x28, 0x09, 0x41, 0x36, 0x33, 0x8f, 0x9b, - 0x96, 0x9c, 0x94, 0x30, 0xa9, 0x72, 0xf0, 0x73, 0xb4, 0x4f, 0x3d, 0x11, 0xcc, 0xe1, 0x6b, 0xa0, - 0x7e, 0x18, 0xc4, 0x30, 0x02, 0x8f, 0xc5, 0x7e, 0xfe, 0x74, 0xea, 0xce, 0x5b, 0xd9, 0xa2, 0xb7, - 0xff, 0xd5, 0x3a, 0x02, 0x59, 0xaf, 0xc3, 0x3f, 0xa1, 0x26, 0x87, 0x10, 0x3c, 0xc1, 0x52, 0x1d, - 0x96, 0x8f, 0x1f, 0xd8, 0x5f, 0xea, 0x42, 0x38, 0xd2, 0x52, 0xa7, 0x23, 0x1b, 0xbc, 0x5c, 0x91, - 0xc2, 0x12, 0x7f, 0x86, 0x76, 0x23, 0x1a, 0xcf, 0x68, 0xc1, 0x54, 0x29, 0x69, 0x3a, 0x38, 0x5b, - 0xf4, 0x76, 0xcf, 0x56, 0x76, 0xc8, 0x2d, 0x26, 0xfe, 0x16, 0x35, 0x05, 0x44, 0x49, 0x48, 0x45, - 0x1e, 0x99, 0xf6, 0xd1, 0xbb, 0xd5, 0xfb, 0x91, 0x2f, 0x4f, 0x1e, 0xe4, 0x9c, 0xf9, 0x17, 0x9a, - 0xa6, 0x46, 0x4c, 0x71, 0xdf, 0x4b, 0x94, 0x14, 0x36, 0xf8, 0x18, 0x75, 0x5c, 0xea, 0x4d, 0xd9, - 0x78, 0x3c, 0x0c, 0xa2, 0x40, 0x74, 0xb7, 0x55, 0xcb, 0xf7, 0xb2, 0x45, 0xaf, 0xe3, 0x54, 0x70, - 0xb2, 0xc2, 0xea, 0xff, 0x5e, 0x47, 0xad, 0x62, 0xfc, 0xe0, 0xef, 0x10, 0xf2, 0x96, 0x8f, 0x9d, - 0x77, 0x0d, 0x15, 0x9c, 0x77, 0xee, 0x0b, 0x4e, 0x31, 0x16, 0xca, 0x19, 0x5a, 0x40, 0x9c, 0x54, - 0x8c, 0xf0, 0xf7, 0xa8, 0xc5, 0x05, 0x4d, 0x85, 0x7a, 0xb6, 0x1b, 0x8f, 0x7e, 0xb6, 0x3b, 0xd9, - 0xa2, 0xd7, 0x1a, 0x2d, 0x0d, 0x48, 0xe9, 0x85, 0xc7, 0x68, 0xb7, 0x4c, 0xd0, 0x6b, 0x8e, 0x20, - 0x75, 0x5d, 0x27, 0x2b, 0x2e, 0xe4, 0x96, 0xab, 0x1c, 0x04, 0x79, 0xc4, 0x54, 0x8e, 0x1a, 0xe5, - 0x20, 0xc8, 0xf3, 0x48, 0xf4, 0x2e, 0xb6, 0x51, 0x8b, 0xcf, 0x3c, 0x0f, 0xc0, 0x07, 0x5f, 0xa5, - 0xa1, 0xe1, 0xbc, 0xa1, 0xa9, 0xad, 0xd1, 0x72, 0x83, 0x94, 0x1c, 0x69, 0x3c, 0xa6, 0x41, 0x08, - 0xbe, 0x4a, 0x41, 0xc5, 0xf8, 0x99, 0x42, 0x89, 0xde, 0x75, 0x0e, 0xaf, 0x6f, 0xcc, 0xda, 0x8b, - 0x1b, 0xb3, 0xf6, 0xf2, 0xc6, 0xac, 0xfd, 0x9a, 0x99, 0xc6, 0x75, 0x66, 0x1a, 0x2f, 0x32, 0xd3, - 0x78, 0x99, 0x99, 0xc6, 0x5f, 0x99, 0x69, 0xfc, 0xf6, 0xb7, 0x59, 0xfb, 0x61, 0x63, 0x3e, 0xf8, - 0x2f, 0x00, 0x00, 0xff, 0xff, 0xdd, 0xcc, 0x84, 0xd1, 0x61, 0x08, 0x00, 0x00, + // 929 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x54, 0x5d, 0x6f, 0xe3, 0x44, + 0x14, 0xad, 0x9b, 0xa6, 0x4d, 0xa6, 0x1f, 0x5b, 0x06, 0x55, 0x1b, 0x0a, 0xb2, 0x97, 0x20, 0xa1, + 0x82, 0x84, 0x4d, 0x4b, 0x85, 0x10, 0x02, 0xa4, 0x75, 0x51, 0x25, 0xaa, 0x54, 0x5b, 0x26, 0x59, + 0x21, 0x21, 0x90, 0x18, 0xdb, 0x37, 0x89, 0x89, 0xed, 0xb1, 0x3c, 0x93, 0x48, 0x7d, 0xe3, 0x27, + 0xf0, 0x23, 0x10, 0x7f, 0x82, 0x77, 0xd4, 0xc7, 0x7d, 0xdc, 0x27, 0x8b, 0x9a, 0x1f, 0xc0, 0xfb, + 0x3e, 0xa1, 0x19, 0x3b, 0xb6, 0xd3, 0x26, 0xa2, 0xcb, 0x5b, 0xe6, 0xcc, 0x39, 0xe7, 0x5e, 0xdf, + 0x39, 0xb9, 0xe8, 0x8b, 0xc9, 0x67, 0xdc, 0xf4, 0x99, 0x35, 0x99, 0x3a, 0x90, 0x44, 0x20, 0x80, + 0x5b, 0x33, 0x88, 0x3c, 0x96, 0x58, 0xc5, 0x05, 0x8d, 0x7d, 0xcb, 0xa1, 0xc2, 0x1d, 0x5b, 0xb3, + 0x63, 0x6b, 0x04, 0x11, 0x24, 0x54, 0x80, 0x67, 0xc6, 0x09, 0x13, 0x0c, 0xbf, 0x99, 0x93, 0x4c, + 0x1a, 0xfb, 0xa6, 0x22, 0x99, 0xb3, 0xe3, 0xc3, 0x8f, 0x46, 0xbe, 0x18, 0x4f, 0x1d, 0xd3, 0x65, + 0xa1, 0x35, 0x62, 0x23, 0x66, 0x29, 0xae, 0x33, 0x1d, 0xaa, 0x93, 0x3a, 0xa8, 0x5f, 0xb9, 0xc7, + 0x61, 0xb7, 0x56, 0xc8, 0x65, 0x09, 0x2c, 0xa9, 0x73, 0x78, 0x5a, 0x71, 0x42, 0xea, 0x8e, 0xfd, + 0x08, 0x92, 0x6b, 0x2b, 0x9e, 0x8c, 0x24, 0xc0, 0xad, 0x10, 0x04, 0x5d, 0xa6, 0xb2, 0x56, 0xa9, + 0x92, 0x69, 0x24, 0xfc, 0x10, 0xee, 0x09, 0x3e, 0xfd, 0x2f, 0x01, 0x77, 0xc7, 0x10, 0xd2, 0xbb, + 0xba, 0xee, 0x3f, 0x1a, 0x6a, 0x5c, 0x30, 0x07, 0xff, 0x84, 0x5a, 0xb2, 0x17, 0x8f, 0x0a, 0xda, + 0xd1, 0x9e, 0x68, 0x47, 0xdb, 0x27, 0x1f, 0x9b, 0xd5, 0x84, 0x4a, 0x4b, 0x33, 0x9e, 0x8c, 0x24, + 0xc0, 0x4d, 0xc9, 0x36, 0x67, 0xc7, 0xe6, 0x33, 0xe7, 0x67, 0x70, 0xc5, 0x25, 0x08, 0x6a, 0xe3, + 0x9b, 0xd4, 0x58, 0xcb, 0x52, 0x03, 0x55, 0x18, 0x29, 0x5d, 0xf1, 0x57, 0x68, 0x83, 0xc7, 0xe0, + 0x76, 0xd6, 0x95, 0xfb, 0x3b, 0xe6, 0x92, 0xf9, 0x9b, 0x17, 0xcc, 0xe9, 0xc7, 0xe0, 0xda, 0x3b, + 0x85, 0xd3, 0x86, 0x3c, 0x11, 0xa5, 0xc3, 0xe7, 0x68, 0x93, 0x0b, 0x2a, 0xa6, 0xbc, 0xd3, 0x50, + 0x0e, 0xfa, 0x4a, 0x07, 0xc5, 0xb2, 0xf7, 0x0a, 0x8f, 0xcd, 0xfc, 0x4c, 0x0a, 0x75, 0xf7, 0xcf, + 0x06, 0xda, 0xb9, 0x60, 0xce, 0x19, 0x8b, 0x3c, 0x5f, 0xf8, 0x2c, 0xc2, 0xa7, 0x68, 0x43, 0x5c, + 0xc7, 0xa0, 0x3e, 0xbb, 0x6d, 0x3f, 0x99, 0x97, 0x1e, 0x5c, 0xc7, 0xf0, 0x2a, 0x35, 0xf6, 0xeb, + 0x5c, 0x89, 0x11, 0xc5, 0xc6, 0xbd, 0xb2, 0x9d, 0x75, 0xa5, 0x3b, 0x5d, 0x2c, 0xf7, 0x2a, 0x35, + 0x96, 0xa4, 0xc3, 0x2c, 0x9d, 0x16, 0x9b, 0xc2, 0x23, 0xb4, 0x1b, 0x50, 0x2e, 0xae, 0x12, 0xe6, + 0xc0, 0xc0, 0x0f, 0xa1, 0xf8, 0xc6, 0x0f, 0x1f, 0xf6, 0x06, 0x52, 0x61, 0x1f, 0x14, 0x0d, 0xec, + 0xf6, 0xea, 0x46, 0x64, 0xd1, 0x17, 0xcf, 0x10, 0x96, 0xc0, 0x20, 0xa1, 0x11, 0xcf, 0x3f, 0x49, + 0x56, 0xdb, 0x78, 0xed, 0x6a, 0x87, 0x45, 0x35, 0xdc, 0xbb, 0xe7, 0x46, 0x96, 0x54, 0xc0, 0xef, + 0xa3, 0xcd, 0x04, 0x28, 0x67, 0x51, 0xa7, 0xa9, 0xc6, 0x55, 0xbe, 0x0e, 0x51, 0x28, 0x29, 0x6e, + 0xf1, 0x07, 0x68, 0x2b, 0x04, 0xce, 0xe9, 0x08, 0x3a, 0x9b, 0x8a, 0xf8, 0xa8, 0x20, 0x6e, 0x5d, + 0xe6, 0x30, 0x99, 0xdf, 0x77, 0x7f, 0xd7, 0xd0, 0xd6, 0x05, 0x73, 0x7a, 0x3e, 0x17, 0xf8, 0x87, + 0x7b, 0xf1, 0x35, 0x1f, 0xf6, 0x31, 0x52, 0xad, 0xc2, 0xbb, 0x5f, 0xd4, 0x69, 0xcd, 0x91, 0x5a, + 0x74, 0xbf, 0x44, 0x4d, 0x5f, 0x40, 0x28, 0x9f, 0xba, 0x71, 0xb4, 0x7d, 0xd2, 0x59, 0x95, 0x3c, + 0x7b, 0xb7, 0x30, 0x69, 0x7e, 0x23, 0xe9, 0x24, 0x57, 0x75, 0xff, 0xd8, 0x50, 0x8d, 0xca, 0x2c, + 0xe3, 0x63, 0xb4, 0x1d, 0xd3, 0x84, 0x06, 0x01, 0x04, 0x3e, 0x0f, 0x55, 0xaf, 0x4d, 0xfb, 0x51, + 0x96, 0x1a, 0xdb, 0x57, 0x15, 0x4c, 0xea, 0x1c, 0x29, 0x71, 0x59, 0x18, 0x07, 0x20, 0x87, 0x99, + 0xc7, 0xad, 0x90, 0x9c, 0x55, 0x30, 0xa9, 0x73, 0xf0, 0x33, 0x74, 0x40, 0x5d, 0xe1, 0xcf, 0xe0, + 0x6b, 0xa0, 0x5e, 0xe0, 0x47, 0xd0, 0x07, 0x97, 0x45, 0x5e, 0xfe, 0xd7, 0x69, 0xd8, 0x6f, 0x65, + 0xa9, 0x71, 0xf0, 0x74, 0x19, 0x81, 0x2c, 0xd7, 0xe1, 0x1f, 0x51, 0x8b, 0x43, 0x00, 0xae, 0x60, + 0x49, 0x11, 0x96, 0x4f, 0x1e, 0x38, 0x5f, 0xea, 0x40, 0xd0, 0x2f, 0xa4, 0xf6, 0x8e, 0x1c, 0xf0, + 0xfc, 0x44, 0x4a, 0x4b, 0xfc, 0x39, 0xda, 0x0b, 0x69, 0x34, 0xa5, 0x25, 0x53, 0xa5, 0xa4, 0x65, + 0xe3, 0x2c, 0x35, 0xf6, 0x2e, 0x17, 0x6e, 0xc8, 0x1d, 0x26, 0xfe, 0x16, 0xb5, 0x04, 0x84, 0x71, + 0x40, 0x45, 0x1e, 0x99, 0xed, 0x93, 0xf7, 0xea, 0xef, 0x23, 0xff, 0x79, 0xb2, 0x91, 0x2b, 0xe6, + 0x0d, 0x0a, 0x9a, 0x5a, 0x31, 0xe5, 0x7b, 0xcf, 0x51, 0x52, 0xda, 0xe0, 0x53, 0xb4, 0xe3, 0x50, + 0x77, 0xc2, 0x86, 0xc3, 0x9e, 0x1f, 0xfa, 0xa2, 0xb3, 0xa5, 0x46, 0xbe, 0x9f, 0xa5, 0xc6, 0x8e, + 0x5d, 0xc3, 0xc9, 0x02, 0x0b, 0x3f, 0x47, 0x8f, 0x85, 0x08, 0x8a, 0x89, 0x3d, 0x1d, 0x0a, 0x48, + 0xce, 0xfd, 0xc8, 0xe7, 0x63, 0xf0, 0x3a, 0x2d, 0x65, 0xf0, 0x76, 0x96, 0x1a, 0x8f, 0x07, 0x83, + 0xde, 0x32, 0x0a, 0x59, 0xa5, 0xed, 0xfe, 0xd6, 0x40, 0xed, 0x72, 0xab, 0xe1, 0xe7, 0x08, 0xb9, + 0xf3, 0x1d, 0xc2, 0x3b, 0x9a, 0xca, 0xe3, 0xbb, 0xab, 0xf2, 0x58, 0x6e, 0x9b, 0x6a, 0x35, 0x97, + 0x10, 0x27, 0x35, 0x23, 0xfc, 0x1d, 0x6a, 0x73, 0x41, 0x13, 0xa1, 0xb6, 0xc1, 0xfa, 0x6b, 0x6f, + 0x83, 0xdd, 0x2c, 0x35, 0xda, 0xfd, 0xb9, 0x01, 0xa9, 0xbc, 0xf0, 0x10, 0xed, 0x55, 0xc1, 0xfc, + 0x9f, 0x9b, 0x4d, 0xa5, 0xe0, 0x6c, 0xc1, 0x85, 0xdc, 0x71, 0x95, 0xfb, 0x25, 0x4f, 0xae, 0x8a, + 0x67, 0xb3, 0xda, 0x2f, 0x79, 0xcc, 0x49, 0x71, 0x8b, 0x2d, 0xd4, 0xe6, 0x53, 0xd7, 0x05, 0xf0, + 0xc0, 0x53, 0x21, 0x6b, 0xda, 0x6f, 0x14, 0xd4, 0x76, 0x7f, 0x7e, 0x41, 0x2a, 0x8e, 0x34, 0x1e, + 0x52, 0x3f, 0x00, 0x4f, 0x85, 0xab, 0x66, 0x7c, 0xae, 0x50, 0x52, 0xdc, 0xda, 0x47, 0x37, 0xb7, + 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, } diff --git a/staging/src/k8s.io/api/batch/v1/generated.proto b/staging/src/k8s.io/api/batch/v1/generated.proto index 91858b019b4..039149daba2 100644 --- a/staging/src/k8s.io/api/batch/v1/generated.proto +++ b/staging/src/k8s.io/api/batch/v1/generated.proto @@ -134,6 +134,18 @@ message JobSpec { // Describes the pod that will be created when executing a job. // More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ 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. diff --git a/staging/src/k8s.io/api/batch/v1/types.go b/staging/src/k8s.io/api/batch/v1/types.go index 84abb1a9015..8dad9043d5c 100644 --- a/staging/src/k8s.io/api/batch/v1/types.go +++ b/staging/src/k8s.io/api/batch/v1/types.go @@ -114,6 +114,18 @@ type JobSpec struct { // 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 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. diff --git a/staging/src/k8s.io/api/batch/v1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/batch/v1/types_swagger_doc_generated.go index 2bb794a5fe1..d8e2bdd780f 100644 --- a/staging/src/k8s.io/api/batch/v1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/batch/v1/types_swagger_doc_generated.go @@ -63,14 +63,15 @@ func (JobList) SwaggerDoc() map[string]string { } var map_JobSpec = map[string]string{ - "": "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/", - "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", - "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", - "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/", + "": "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/", + "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", + "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", + "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/", + "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 { diff --git a/staging/src/k8s.io/api/batch/v1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/batch/v1/zz_generated.deepcopy.go index d641cf3080e..88cb01678be 100644 --- a/staging/src/k8s.io/api/batch/v1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/batch/v1/zz_generated.deepcopy.go @@ -138,6 +138,11 @@ func (in *JobSpec) DeepCopyInto(out *JobSpec) { **out = **in } in.Template.DeepCopyInto(&out.Template) + if in.TTLSecondsAfterFinished != nil { + in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished + *out = new(int32) + **out = **in + } return } diff --git a/staging/src/k8s.io/kube-controller-manager/config/v1alpha1/types.go b/staging/src/k8s.io/kube-controller-manager/config/v1alpha1/types.go index 5bcfadea6f5..2e9f45b5b49 100644 --- a/staging/src/k8s.io/kube-controller-manager/config/v1alpha1/types.go +++ b/staging/src/k8s.io/kube-controller-manager/config/v1alpha1/types.go @@ -145,6 +145,9 @@ type KubeControllerManagerConfiguration struct { // ServiceControllerConfiguration holds configuration for ServiceController // related features. ServiceController ServiceControllerConfiguration + // TTLAfterFinishedControllerConfiguration holds configuration for + // TTLAfterFinishedController related features. + TTLAfterFinishedController TTLAfterFinishedControllerConfiguration } // GenericControllerManagerConfiguration holds configuration for a generic controller-manager. @@ -438,3 +441,10 @@ type ServiceControllerConfiguration struct { // management, but more CPU (and network) load. 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 +} diff --git a/staging/src/k8s.io/kube-controller-manager/config/v1alpha1/zz_generated.deepcopy.go b/staging/src/k8s.io/kube-controller-manager/config/v1alpha1/zz_generated.deepcopy.go index b975a124ac5..f8d242772ce 100644 --- a/staging/src/k8s.io/kube-controller-manager/config/v1alpha1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/kube-controller-manager/config/v1alpha1/zz_generated.deepcopy.go @@ -300,6 +300,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa out.ResourceQuotaController = in.ResourceQuotaController out.SAController = in.SAController out.ServiceController = in.ServiceController + out.TTLAfterFinishedController = in.TTLAfterFinishedController return } @@ -509,6 +510,22 @@ func (in *ServiceControllerConfiguration) DeepCopy() *ServiceControllerConfigura 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. func (in *VolumeConfiguration) DeepCopyInto(out *VolumeConfiguration) { *out = *in diff --git a/test/e2e/apps/job.go b/test/e2e/apps/job.go index 29b5afd7f3a..d70713067ee 100644 --- a/test/e2e/apps/job.go +++ b/test/e2e/apps/job.go @@ -44,7 +44,7 @@ var _ = SIGDescribe("Job", func() { Expect(err).NotTo(HaveOccurred()) 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()) }) @@ -63,7 +63,7 @@ var _ = SIGDescribe("Job", func() { Expect(err).NotTo(HaveOccurred()) 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()) }) @@ -84,7 +84,7 @@ var _ = SIGDescribe("Job", func() { Expect(err).NotTo(HaveOccurred()) 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()) }) diff --git a/test/e2e/auth/metadata_concealment.go b/test/e2e/auth/metadata_concealment.go index 01d46bf8dd7..e10d307c841 100644 --- a/test/e2e/auth/metadata_concealment.go +++ b/test/e2e/auth/metadata_concealment.go @@ -58,7 +58,7 @@ var _ = SIGDescribe("Metadata Concealment", func() { Expect(err).NotTo(HaveOccurred()) 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()) }) }) diff --git a/test/e2e/common/BUILD b/test/e2e/common/BUILD index 8df546df247..74f14a9bd3f 100644 --- a/test/e2e/common/BUILD +++ b/test/e2e/common/BUILD @@ -34,6 +34,7 @@ go_library( "secrets_volume.go", "security_context.go", "sysctl.go", + "ttlafterfinished.go", "util.go", "volumes.go", ], @@ -43,13 +44,16 @@ go_library( "//pkg/apis/core:go_default_library", "//pkg/client/clientset_generated/internalclientset:go_default_library", "//pkg/client/conditions:go_default_library", + "//pkg/features:go_default_library", "//pkg/kubelet:go_default_library", "//pkg/kubelet/apis:go_default_library", "//pkg/kubelet/images:go_default_library", "//pkg/kubelet/sysctl:go_default_library", "//pkg/security/apparmor:go_default_library", + "//pkg/util/slice:go_default_library", "//pkg/util/version: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/core/v1: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/wait: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/scale:go_default_library", "//staging/src/k8s.io/client-go/tools/cache:go_default_library", diff --git a/test/e2e/common/ttlafterfinished.go b/test/e2e/common/ttlafterfinished.go new file mode 100644 index 00000000000..1c62997bd23 --- /dev/null +++ b/test/e2e/common/ttlafterfinished.go @@ -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()) +} diff --git a/test/e2e/framework/BUILD b/test/e2e/framework/BUILD index a0ebaaf3c2c..e468c6568e0 100644 --- a/test/e2e/framework/BUILD +++ b/test/e2e/framework/BUILD @@ -59,6 +59,7 @@ go_library( "//pkg/cloudprovider/providers/gce:go_default_library", "//pkg/controller:go_default_library", "//pkg/controller/deployment/util:go_default_library", + "//pkg/controller/job:go_default_library", "//pkg/controller/nodelifecycle:go_default_library", "//pkg/controller/service:go_default_library", "//pkg/features:go_default_library", diff --git a/test/e2e/framework/jobs_util.go b/test/e2e/framework/jobs_util.go index c5c97f1c772..e3acdac3661 100644 --- a/test/e2e/framework/jobs_util.go +++ b/test/e2e/framework/jobs_util.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/wait" clientset "k8s.io/client-go/kubernetes" + jobutil "k8s.io/kubernetes/pkg/controller/job" ) 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. -func WaitForJobFinish(c clientset.Interface, ns, jobName string, completions int32) error { +// WaitForJobComplete uses c to wait for compeletions to complete for the Job jobName in namespace ns. +func WaitForJobComplete(c clientset.Interface, ns, jobName string, completions int32) error { return wait.Poll(Poll, JobTimeout, func() (bool, error) { curr, err := c.BatchV1().Jobs(ns).Get(jobName, metav1.GetOptions{}) 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. func WaitForJobFailure(c clientset.Interface, ns, jobName string, timeout time.Duration, reason string) 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 } +// 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 { p := new(bool) *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) { jobs := c.BatchV1().Jobs(namespace) 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 { return false, err } @@ -268,3 +292,25 @@ func UpdateJobWithRetries(c clientset.Interface, namespace, name string, applyUp } 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 +}