package main import ( "context" "encoding/json" "flag" "io/ioutil" "log" "os" "path/filepath" "sync" "time" "github.com/digitalocean/godo" "github.com/dustin/go-humanize" "golang.org/x/oauth2" ) type config struct { LogDest string `json:"log-dest"` APIKey string `json:"api-key"` Threshold int `json:"threshold"` DeleteStale bool `json:"delete-stale"` } type tokenSource struct { AccessToken string } var ( configPath string logger *log.Logger apiKey string threshold int deleteStale bool wg sync.WaitGroup client *godo.Client ) func initConfig() { flag.StringVar(&configPath, "config", "./config.json", "Path to configuration file") flag.Parse() if !validatePath(&configPath) { usage() } configFile, err := ioutil.ReadFile(configPath) if err != nil { usage() } cfg := config{} if err = json.Unmarshal(configFile, &cfg); err != nil { usage() } apiKey = cfg.APIKey threshold = cfg.Threshold deleteStale = cfg.DeleteStale setUpLogger(cfg.LogDest) logger.Printf("Starting GitLab Runner monitoring with config %s", configPath) if deleteStale { logger.Println("Stale droplets WILL BE DELETED automatically") } else { logger.Println("Stale droplets will be logged, but not deleted") } } func main() { initConfig() authenticate() checkAPI() wg.Wait() logger.Println("Execution complete!") } func authenticate() { tokenSource := &tokenSource{ AccessToken: apiKey, } oauthClient := oauth2.NewClient(context.Background(), tokenSource) client = godo.NewClient(oauthClient) } // oAuth token func (t *tokenSource) Token() (*oauth2.Token, error) { token := &oauth2.Token{ AccessToken: t.AccessToken, } return token, nil } func checkAPI() { ctx := context.TODO() droplets, err := listDroplets(ctx, client) if err != nil { logger.Println("Warning! Failed to retrieve droplet list.") logger.Print(err) return } for _, droplet := range droplets { wg.Add(1) go checkDroplet(droplet) } } func listDroplets(ctx context.Context, client *godo.Client) ([]godo.Droplet, error) { list := []godo.Droplet{} opt := &godo.ListOptions{} for { droplets, resp, err := client.Droplets.List(ctx, opt) if err != nil { return nil, err } for _, d := range droplets { list = append(list, d) } if resp.Links == nil || resp.Links.IsLastPage() { break } page, err := resp.Links.CurrentPage() if err != nil { return nil, err } opt.Page = page + 1 } return list, nil } func checkDroplet(droplet godo.Droplet) { defer wg.Done() if !checkDropletAge(droplet) { return } deleted := deleteDroplet(droplet) if deleteStale { if deleted { logger.Printf("Removed droplet %s (%d)", droplet.Name, droplet.ID) } else { logger.Printf("Failed to delete droplet %s (%d)", droplet.Name, droplet.ID) } } } func checkDropletAge(droplet godo.Droplet) bool { thr := time.Now().Add(time.Duration(-threshold) * time.Second) created, err := time.Parse(time.RFC3339, droplet.Created) if err != nil { logger.Printf("Could not parse created-timestamp for droplet %s (%d)", droplet.Name, droplet.ID) return false } stale := thr.After(created) if stale { logger.Printf("Stale droplet => ID: %d; name: \"%s\"; created: %s, %s (%d)", droplet.ID, droplet.Name, humanize.Time(created), droplet.Created, created.Unix()) } return stale } func deleteDroplet(droplet godo.Droplet) bool { if !deleteStale { return false } logger.Printf("Deleting droplet %s (%d)", droplet.Name, droplet.ID) ctx := context.TODO() _, err := client.Droplets.Delete(ctx, droplet.ID) return err == nil } func setUpLogger(logDest string) { logOpts := log.Ldate | log.Ltime | log.LUTC | log.Lshortfile if logDest == "os.Stdout" { logger = log.New(os.Stdout, "DEBUG: ", logOpts) } else { path, err := filepath.Abs(logDest) if err != nil { logger.Fatal(err) } logFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { log.Fatal(err) } logger = log.New(logFile, "", logOpts) } } func validatePath(path *string) bool { if len(*path) <= 1 { return false } var err error *path, err = filepath.Abs(*path) if err != nil { logger.Printf("Error: %s", err.Error()) return false } if _, err = os.Stat(*path); os.IsNotExist(err) { return false } return true } func usage() { flag.Usage() os.Exit(3) }