2020-08-26 16:50:59 +02:00
package main
import (
"context"
"fmt"
2022-09-23 18:38:02 +02:00
"io"
"os"
"os/signal"
"syscall"
"time"
2023-05-23 19:32:04 +02:00
"github.com/alecthomas/kingpin/v2"
2020-08-26 16:50:59 +02:00
"github.com/multiplay/go-slack/chat"
"github.com/multiplay/go-slack/webhook"
"github.com/robfig/cron"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
2022-09-23 18:38:02 +02:00
batchv1 "k8s.io/client-go/kubernetes/typed/batch/v1"
2020-08-26 16:50:59 +02:00
"k8s.io/client-go/rest"
)
2020-09-06 13:20:55 +02:00
var checkFunc = doCheck
var exitFunc = os . Exit
2020-08-26 16:50:59 +02:00
func main ( ) {
slackUrl := kingpin . Flag ( "slack-url" , "The Slack Webhook URL" ) . Envar ( "SLACK_URL" ) . Required ( ) . String ( )
kingpin . Parse ( )
2020-09-06 13:20:55 +02:00
exitFunc ( doMain ( * slackUrl , & DefaultProvider { & InClusterProvider { } } ) )
}
func doMain ( slackUrl string , provider ClientProvider ) int {
client , err := provider . Provide ( )
2020-08-26 16:50:59 +02:00
if err != nil {
2020-09-06 13:20:55 +02:00
fmt . Printf ( "Unable to connect to K8S: %s\n" , err )
return 1
2020-08-26 16:50:59 +02:00
}
2020-09-06 13:20:55 +02:00
ic := make ( chan os . Signal , 1 )
signal . Notify ( ic , os . Interrupt , syscall . SIGTERM )
if err := checkFunc ( client , slackUrl , ic , 60 * time . Second , os . Stdout ) ; err != nil {
fmt . Printf ( "Error checking jobs: %s\n" , err )
return 1
2020-08-26 16:50:59 +02:00
}
2020-09-06 13:20:55 +02:00
return 0
}
2020-08-26 16:50:59 +02:00
2020-09-06 13:20:55 +02:00
func doCheck ( client Client , slackUrl string , ic chan os . Signal , sleepTime time . Duration , out io . Writer ) error {
slack := webhook . New ( slackUrl )
2020-08-26 16:50:59 +02:00
parser := cron . NewParser ( cron . Minute | cron . Hour | cron . Dom | cron . Month | cron . Dow )
for {
select {
case <- ic :
2020-09-06 13:20:55 +02:00
_ , _ = fmt . Fprintf ( out , "Got SIGTERM signal, exiting\n" )
return nil
2020-08-26 16:50:59 +02:00
default :
2022-09-23 18:38:02 +02:00
cronJobs , err := client . BatchV1 ( ) . CronJobs ( "" ) . List ( context . Background ( ) , v1 . ListOptions { } )
2020-08-26 16:50:59 +02:00
if err != nil {
2020-09-06 13:20:55 +02:00
return fmt . Errorf ( "error getting cronjobs: %w" , err )
2020-08-26 16:50:59 +02:00
}
limit := time . Now ( ) . Add ( - 120 * time . Second )
2020-09-06 13:20:55 +02:00
for _ , c := range cronJobs . Items {
2020-08-26 16:50:59 +02:00
if c . Spec . Suspend == nil || ! * c . Spec . Suspend {
since := c . CreationTimestamp
if c . Status . LastScheduleTime != nil {
since = * c . Status . LastScheduleTime
}
schedule , err := parser . Parse ( c . Spec . Schedule )
if err != nil {
2020-09-06 13:20:55 +02:00
return fmt . Errorf ( "error parsing schedule of %s/%s (%s): %w" , c . Namespace , c . Name , c . Spec . Schedule , err )
2020-08-26 16:50:59 +02:00
}
next := schedule . Next ( since . Time )
2020-09-06 13:20:55 +02:00
_ , _ = fmt . Fprintf ( out , "Checking %s/%s since %s, next schedule %s, limit %s.\n" , c . Namespace , c . Name , since . Format ( time . RFC3339 ) , next . Format ( time . RFC3339 ) , limit . Format ( time . RFC3339 ) )
2020-08-26 16:50:59 +02:00
if next . Before ( limit ) {
2020-09-06 13:20:55 +02:00
_ , _ = fmt . Fprintf ( out , "%s/%s was not scheduled. Sending Slack notification.\n" , c . Namespace , c . Name )
2020-08-26 16:50:59 +02:00
m := & chat . Message {
Text : fmt . Sprintf ( "Cronjob %s/%s is not running according to schedule (%s). Last scheduled: %s" , c . Namespace , c . Name , c . Spec . Schedule , since . Format ( time . RFC3339 ) ) ,
Username : "cron-checker" ,
}
2020-09-06 13:20:55 +02:00
_ , err := m . Send ( slack )
2020-08-26 16:50:59 +02:00
if err != nil {
2020-09-06 13:20:55 +02:00
_ , _ = fmt . Fprintf ( out , "Unable to send Slack notification: %s\n" , err )
2020-08-26 16:50:59 +02:00
}
}
}
}
2020-09-06 13:20:55 +02:00
time . Sleep ( sleepTime )
2020-08-26 16:50:59 +02:00
}
}
}
2020-09-06 13:20:55 +02:00
type Client interface {
2022-09-23 18:38:02 +02:00
BatchV1 ( ) batchv1 . BatchV1Interface
2020-09-06 13:20:55 +02:00
}
type ClientProvider interface {
Provide ( ) ( Client , error )
}
type DefaultProvider struct {
provider ConfigProvider
}
func ( d DefaultProvider ) Provide ( ) ( Client , error ) {
config , err := d . provider . Provide ( )
if err != nil {
return nil , err
}
return kubernetes . NewForConfig ( config )
}
var _ ClientProvider = & DefaultProvider { }
type ConfigProvider interface {
Provide ( ) ( * rest . Config , error )
}
type InClusterProvider struct { }
func ( i InClusterProvider ) Provide ( ) ( * rest . Config , error ) {
return rest . InClusterConfig ( )
}
var _ ConfigProvider = & InClusterProvider { }