2019-08-30 18:33:25 +00:00
/ *
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 (
2020-03-26 21:07:15 +00:00
"context"
2019-08-30 18:33:25 +00:00
"fmt"
"time"
"k8s.io/klog"
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"
2019-12-12 01:27:03 +00:00
"k8s.io/component-base/metrics/prometheus/ratelimiter"
2019-09-27 21:51:53 +00:00
"k8s.io/kubectl/pkg/scheme"
2019-08-30 18:33:25 +00:00
"k8s.io/kubernetes/pkg/controller"
jobutil "k8s.io/kubernetes/pkg/controller/job"
)
// 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 ( klog . Infof )
eventBroadcaster . StartRecordingToSink ( & v1core . EventSinkImpl { Interface : client . CoreV1 ( ) . Events ( "" ) } )
if client != nil && client . CoreV1 ( ) . RESTClient ( ) . GetRateLimiter ( ) != nil {
2019-12-12 01:27:03 +00:00
ratelimiter . RegisterMetricAndTrackRateLimiterUsage ( "ttl_after_finished_controller" , client . CoreV1 ( ) . RESTClient ( ) . GetRateLimiter ( ) )
2019-08-30 18:33:25 +00:00
}
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 ( )
klog . Infof ( "Starting TTL after finished controller" )
defer klog . Infof ( "Shutting down TTL after finished controller" )
2019-09-27 21:51:53 +00:00
if ! cache . WaitForNamedCacheSync ( "TTL after finished" , stopCh , tc . jListerSynced ) {
2019-08-30 18:33:25 +00:00
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 )
klog . 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 )
klog . 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 ) {
klog . 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
}
klog . 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.
2020-03-26 21:07:15 +00:00
fresh , err := tc . client . BatchV1 ( ) . Jobs ( namespace ) . Get ( context . TODO ( ) , name , metav1 . GetOptions { } )
2019-08-30 18:33:25 +00:00
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
2020-03-26 21:07:15 +00:00
options := metav1 . DeleteOptions {
2019-08-30 18:33:25 +00:00
PropagationPolicy : & policy ,
Preconditions : & metav1 . Preconditions { UID : & fresh . UID } ,
}
klog . V ( 4 ) . Infof ( "Cleaning up Job %s/%s" , namespace , name )
2020-03-26 21:07:15 +00:00
return tc . client . BatchV1 ( ) . Jobs ( fresh . Namespace ) . Delete ( context . TODO ( ) , fresh . Name , options )
2019-08-30 18:33:25 +00:00
}
// 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 ) {
2019-09-27 21:51:53 +00:00
return nil , nil , fmt . Errorf ( "job %s/%s should not be cleaned up" , j . Namespace , j . Name )
2019-08-30 18:33:25 +00:00
}
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 ( ) ) {
klog . 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 ( ) )
klog . 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 )
}