glrdomon.go 4.12 KB
Newer Older
Erick Hitter's avatar
Erick Hitter committed
1 2
package main

Erick Hitter's avatar
Erick Hitter committed
3
import (
Erick Hitter's avatar
Erick Hitter committed
4
	"context"
Erick Hitter's avatar
Erick Hitter committed
5 6 7 8 9 10 11 12
	"encoding/json"
	"flag"
	"io/ioutil"
	"log"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
13
	"time"
Erick Hitter's avatar
Erick Hitter committed
14

Erick Hitter's avatar
Erick Hitter committed
15
	"github.com/digitalocean/godo"
16
	"github.com/dustin/go-humanize"
Erick Hitter's avatar
Erick Hitter committed
17
	"github.com/robfig/cron"
Erick Hitter's avatar
Erick Hitter committed
18
	"golang.org/x/oauth2"
Erick Hitter's avatar
Erick Hitter committed
19 20 21
)

type config struct {
Erick Hitter's avatar
Erick Hitter committed
22
	LogDest     string `json:"log-dest"`
23 24 25 26
	APIKey      string `json:"api-key"`
	Threshold   int    `json:"threshold"`
	Schedule    string `json:"schedule"`
	DeleteStale bool   `json:"delete-stale"`
Erick Hitter's avatar
Erick Hitter committed
27 28
}

Erick Hitter's avatar
Erick Hitter committed
29
type tokenSource struct {
Erick Hitter's avatar
Erick Hitter committed
30 31 32
	AccessToken string
}

Erick Hitter's avatar
Erick Hitter committed
33 34 35
var (
	configPath string

Erick Hitter's avatar
Erick Hitter committed
36 37
	logger  *log.Logger
	logDest string
Erick Hitter's avatar
Erick Hitter committed
38

Erick Hitter's avatar
Erick Hitter committed
39 40
	apiKey string

41 42 43
	threshold   int
	schedule    string
	deleteStale bool
Erick Hitter's avatar
Erick Hitter committed
44 45

	client *godo.Client
Erick Hitter's avatar
Erick Hitter committed
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
)

func init() {
	flag.StringVar(&configPath, "config", "./config.json", "Path to configuration file")
	flag.Parse()

	cfgPathValid := validatePath(&configPath)
	if !cfgPathValid {
		usage()
	}

	configFile, err := ioutil.ReadFile(configPath)
	if err != nil {
		usage()
	}

	cfg := config{}
	if err = json.Unmarshal(configFile, &cfg); err != nil {
		usage()
	}

Erick Hitter's avatar
Erick Hitter committed
67
	logDest = cfg.LogDest
Erick Hitter's avatar
Erick Hitter committed
68

Erick Hitter's avatar
Erick Hitter committed
69
	apiKey = cfg.APIKey
Erick Hitter's avatar
Erick Hitter committed
70

Erick Hitter's avatar
Erick Hitter committed
71 72
	threshold = cfg.Threshold
	schedule = cfg.Schedule
73
	deleteStale = cfg.DeleteStale
Erick Hitter's avatar
Erick Hitter committed
74

Erick Hitter's avatar
Erick Hitter committed
75 76 77
	setUpLogger()
}

Erick Hitter's avatar
Erick Hitter committed
78
func main() {
Erick Hitter's avatar
Erick Hitter committed
79 80 81 82 83
	logger.Printf("Starting GitLab Runner monitoring with config %s", configPath)

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

84 85 86 87
	if deleteStale {
		logger.Println("Stale droplets WILL BE DELETED automatically")
	} else {
		logger.Println("Stale droplets will be logged, but not deleted")
Erick Hitter's avatar
Erick Hitter committed
88 89
	}

Erick Hitter's avatar
Erick Hitter committed
90 91
	authenticate()
	startCron()
Erick Hitter's avatar
Erick Hitter committed
92

Erick Hitter's avatar
Erick Hitter committed
93 94 95 96 97
	caughtSig := <-sig

	logger.Printf("Stopping, got signal %s", caughtSig)
}

Erick Hitter's avatar
Erick Hitter committed
98
func authenticate() {
Erick Hitter's avatar
Erick Hitter committed
99
	tokenSource := &tokenSource{
Erick Hitter's avatar
Erick Hitter committed
100 101 102 103 104 105 106
		AccessToken: apiKey,
	}

	oauthClient := oauth2.NewClient(context.Background(), tokenSource)
	client = godo.NewClient(oauthClient)
}

Erick Hitter's avatar
Erick Hitter committed
107 108
// oAuth token
func (t *tokenSource) Token() (*oauth2.Token, error) {
Erick Hitter's avatar
Erick Hitter committed
109 110 111
	token := &oauth2.Token{
		AccessToken: t.AccessToken,
	}
Erick Hitter's avatar
Erick Hitter committed
112

Erick Hitter's avatar
Erick Hitter committed
113
	return token, nil
Erick Hitter's avatar
Erick Hitter committed
114 115 116 117
}

func startCron() {
	c := cron.New()
Erick Hitter's avatar
Erick Hitter committed
118
	c.AddFunc(schedule, checkAPI)
Erick Hitter's avatar
Erick Hitter committed
119 120 121
	c.Start()
}

Erick Hitter's avatar
Erick Hitter committed
122 123 124
func checkAPI() {
	ctx := context.TODO()
	droplets, err := listDroplets(ctx, client)
Erick Hitter's avatar
Erick Hitter committed
125
	if err != nil {
126 127 128
		logger.Println("Warning! Failed to retrieve droplet list.")
		logger.Print(err)
		return
Erick Hitter's avatar
Erick Hitter committed
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
	}

	for _, droplet := range droplets {
		go checkDropletAge(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 checkDropletAge(droplet godo.Droplet) {
166 167 168 169 170 171 172 173 174
	thr := time.Now().Add(time.Duration(threshold))
	created, err := time.Parse(time.RFC3339, droplet.Created)
	if err != nil {
		logger.Printf("Could not parse created-timestamp for droplet ID %d", droplet.ID)
		return
	}

	if thr.After(created) {
		logger.Printf("Stale droplet => ID: %d; name: \"%s\"; created: %s, %s (%d)", droplet.ID, droplet.Name, humanize.Time(created), droplet.Created, created.Unix())
175
		deleteDroplet(droplet)
176 177 178 179
	}
}

func deleteDroplet(droplet godo.Droplet) bool {
180 181 182 183
	if !deleteStale {
		return false
	}

184 185
	logger.Printf("Deleting droplet %d", droplet.ID)
	return false
Erick Hitter's avatar
Erick Hitter committed
186 187
}

Erick Hitter's avatar
Erick Hitter committed
188 189 190
func setUpLogger() {
	logOpts := log.Ldate | log.Ltime | log.LUTC | log.Lshortfile

Erick Hitter's avatar
Erick Hitter committed
191
	if logDest == "os.Stdout" {
Erick Hitter's avatar
Erick Hitter committed
192 193
		logger = log.New(os.Stdout, "DEBUG: ", logOpts)
	} else {
Erick Hitter's avatar
Erick Hitter committed
194
		path, err := filepath.Abs(logDest)
Erick Hitter's avatar
Erick Hitter committed
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
		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
}
Erick Hitter's avatar
Erick Hitter committed
227

Erick Hitter's avatar
Erick Hitter committed
228 229 230
func usage() {
	flag.Usage()
	os.Exit(3)
Erick Hitter's avatar
Erick Hitter committed
231
}