├── .travis.yml ├── check ├── exec │ ├── testdata │ │ └── exec.sh │ ├── exec_test.go │ └── exec.go ├── tcp │ ├── testdata │ │ ├── client.key │ │ ├── leaf.key │ │ ├── root.key │ │ ├── Makefile │ │ ├── client.pem │ │ ├── root.pem │ │ ├── leaf.pem │ │ ├── client.debug.crt │ │ ├── root.debug.crt │ │ └── leaf.debug.crt │ └── tcp.go ├── dns │ ├── dns.go │ └── dns_test.go ├── http │ ├── http_test.go │ └── http.go └── tls │ ├── tls_test.go │ └── tls.go ├── statuspage ├── images │ ├── ok.png │ ├── checkup.png │ ├── favicon.png │ ├── degraded.png │ ├── incident.png │ ├── status-gray.png │ ├── status-green.png │ ├── status-red.png │ └── status-yellow.png ├── js │ ├── config.js │ ├── config_s3.js │ ├── fs.js │ └── checkup.js ├── index.html └── css │ └── style.css ├── storage ├── sql │ ├── types.go │ ├── sql_disabled.go │ ├── sql_test.go │ └── sql.go ├── sqlite3 │ ├── sqlite_disabled.go │ ├── types.go │ ├── sqlite_test.go │ └── sqlite.go ├── fs │ ├── types.go │ ├── fs_test.go │ └── fs.go ├── mysql │ ├── types.go │ ├── mysql_test.go │ └── mysql.go ├── postgres │ ├── types.go │ ├── postgres_test.go │ └── postgres.go ├── s3 │ ├── s3_test.go │ └── s3.go ├── appinsights │ ├── appinsights_test.go │ └── appinsights.go └── github │ └── github.go ├── .gitignore ├── cmd ├── checkup │ └── main.go ├── every.go ├── message.go ├── root.go ├── serve.go └── provision.go ├── errors.go ├── types ├── util.go ├── errors_test.go ├── stats.go ├── attempt.go ├── errors.go ├── status.go ├── provisioner.go └── result.go ├── docker-compose.yml ├── Dockerfile ├── Makefile ├── testdata └── config.json ├── check.go ├── notifier.go ├── notifier ├── discord │ ├── types.go │ └── discord.go ├── pushover │ └── pushover.go ├── slack │ └── slack.go ├── mailgun │ └── mailgun.go └── mail │ └── mail.go ├── .drone.yml ├── .github └── workflows │ └── scip.yml ├── LICENSE ├── storage.go ├── go.mod ├── DCO ├── interfaces.go ├── CONTRIBUTING.md ├── checkup_test.go └── checkup.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.14 -------------------------------------------------------------------------------- /check/exec/testdata/exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "$@" 3 | exit $1 -------------------------------------------------------------------------------- /statuspage/images/ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/ok.png -------------------------------------------------------------------------------- /storage/sql/types.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | // Type should match the package name 4 | const Type = "sql" 5 | -------------------------------------------------------------------------------- /statuspage/images/checkup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/checkup.png -------------------------------------------------------------------------------- /statuspage/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/favicon.png -------------------------------------------------------------------------------- /statuspage/images/degraded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/degraded.png -------------------------------------------------------------------------------- /statuspage/images/incident.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/incident.png -------------------------------------------------------------------------------- /statuspage/images/status-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/status-gray.png -------------------------------------------------------------------------------- /statuspage/images/status-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/status-green.png -------------------------------------------------------------------------------- /statuspage/images/status-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/status-red.png -------------------------------------------------------------------------------- /statuspage/images/status-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcegraph/checkup/HEAD/statuspage/images/status-yellow.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _gitignore/ 2 | .DS_Store 3 | checkup.json 4 | src/ 5 | pkg/ 6 | bin/ 7 | builds/ 8 | dist/ 9 | checks/ 10 | .envrc -------------------------------------------------------------------------------- /cmd/checkup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/sourcegraph/checkup/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package checkup 2 | 3 | const ( 4 | errUnknownCheckerType = "unknown checker type: %s" 5 | errUnknownStorageType = "unknown storage type: %s" 6 | errUnknownNotifierType = "unknown notifier type: %s" 7 | ) 8 | -------------------------------------------------------------------------------- /types/util.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Timestamp returns the UTC Unix timestamp in 8 | // nanoseconds. 9 | func Timestamp() int64 { 10 | return time.Now().UTC().UnixNano() 11 | } 12 | -------------------------------------------------------------------------------- /check/tcp/testdata/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIDxE+LU0KvWeyziyXEHG1WDP/Dk/8tVW1YEhtP3/x8BRoAoGCCqGSM49 3 | AwEHoUQDQgAEKRHWBQ/1pP+CuEe4e+GfxT5OmYT2WEqZwNtLGNpS1Ve1nSgmvvP7 4 | zYCtgMlr7I9IGc+X2uZn6FDBDQeikAGeIg== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /check/tcp/testdata/leaf.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIJR5CixdEgUd/xXjwuI6P7dEepdUBsEtBBu340YS9W81oAoGCCqGSM49 3 | AwEHoUQDQgAEG8uV7uOXdhs01Od6BWJ6yszI6IcRsMirV7fAajEm3bVc9EaLTbIR 4 | VMN085INnKBInk07PAjOBuStGrOC4H5Vaw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /check/tcp/testdata/root.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIJgtDAjzxGibF9c2tdx0CwwUuVzxzwPacyH2RwVEkpULoAoGCCqGSM49 3 | AwEHoUQDQgAEDGrZliWf965C1kKH2IZ0c0KpEUPyHx+LMycvPLUJb9JbRVUsw6mz 4 | xc6ugw8NxSYQZHijYJO4pm0PMj6juArXXw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | 3 | services: 4 | checkup: 5 | hostname: checkup 6 | image: checkup:latest 7 | ports: 8 | - 3000 9 | volumes: 10 | - ./checkup.json:/app/checkup.json 11 | - ./checks:/app/checks 12 | restart: always 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine as builder 2 | 3 | ENV CGO_ENABLED=0 4 | 5 | COPY . /app 6 | WORKDIR /app 7 | RUN apk --no-cache add make && make build 8 | 9 | FROM alpine:latest 10 | 11 | WORKDIR /app 12 | 13 | COPY --from=builder /app/builds/checkup /usr/local/bin/checkup 14 | 15 | ADD statuspage/ /app/statuspage 16 | 17 | USER nobody 18 | EXPOSE 3000 19 | 20 | ENTRYPOINT ["checkup"] 21 | CMD ["serve"] -------------------------------------------------------------------------------- /types/errors_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | var ( 9 | err1 = errors.New("err 1") 10 | err2 = errors.New("err 2") 11 | ) 12 | 13 | func TestErrors(t *testing.T) { 14 | errs := []error{ 15 | err1, 16 | err2, 17 | } 18 | errsT := Errors(errs) 19 | 20 | want := "err 1; err 2" 21 | if got := errsT.Error(); want != got { 22 | t.Errorf("Errors, wanted '%s', got '%s'", want, got) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /types/stats.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Stats is a type that holds information about a Result, 8 | // especially its various Attempts. 9 | type Stats struct { 10 | Total time.Duration `json:"total,omitempty"` 11 | Mean time.Duration `json:"mean,omitempty"` 12 | Median time.Duration `json:"median,omitempty"` 13 | Min time.Duration `json:"min,omitempty"` 14 | Max time.Duration `json:"max,omitempty"` 15 | } 16 | -------------------------------------------------------------------------------- /check/tcp/testdata/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all generate_cert 2 | 3 | all: generate_cert 4 | 5 | GOPATH ?= /root/go 6 | GENERATE_TLS_CERT = $(GOPATH)/bin/generate-tls-cert 7 | 8 | $(GENERATE_TLS_CERT): 9 | go get -u github.com/Shyp/generate-tls-cert 10 | 11 | leaf.pem: | $(GENERATE_TLS_CERT) 12 | rm *.crt *.key *.pem -f 13 | $(GENERATE_TLS_CERT) --host=localhost,127.0.0.1 -duration 876000h 14 | 15 | # Generate TLS certificates for local development. 16 | generate_cert: leaf.pem | $(GENERATE_TLS_CERT) 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build fmt test docker 2 | 3 | all: build test 4 | 5 | DOCKER_IMAGE := checkup 6 | 7 | build: fmt 8 | go build -o builds/ ./cmd/... 9 | 10 | build-%: TAG=$* 11 | build-%: fmt 12 | go build -o builds/ -tags $(TAG) ./cmd/... 13 | 14 | fmt: 15 | mkdir -p builds/ 16 | go fmt ./... 17 | go mod tidy 18 | 19 | test: 20 | go test -race -count=1 ./... 21 | 22 | test-%: TAG=$* 23 | test-%: 24 | go test -tags $(TAG) -race -count=1 ./... 25 | 26 | docker: 27 | docker build --no-cache . -t $(DOCKER_IMAGE) 28 | -------------------------------------------------------------------------------- /types/attempt.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Attempt is an attempt to communicate with the endpoint. 8 | type Attempt struct { 9 | RTT time.Duration `json:"rtt"` 10 | Error string `json:"error,omitempty"` 11 | } 12 | 13 | // Attempts is a list of Attempt that can be sorted by RTT. 14 | type Attempts []Attempt 15 | 16 | func (a Attempts) Len() int { return len(a) } 17 | func (a Attempts) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 18 | func (a Attempts) Less(i, j int) bool { return a[i].RTT < a[j].RTT } 19 | -------------------------------------------------------------------------------- /testdata/config.json: -------------------------------------------------------------------------------- 1 | {"storage":{"type":"s3","access_key_id":"AAAAAA6WVZYYANEAFL6Q","secret_access_key":"DbvNDdKHaN4n8n3qqqXwvUVqVQTcHVmNYtvcJfTd","region":"us-east-1","bucket":"test","check_expiry":604800000000000},"checkers":[{"type":"http","endpoint_name":"Example (HTTP)","endpoint_url":"http://www.example.com","attempts":5},{"type":"http","endpoint_name":"Example (HTTPS)","endpoint_url":"https://example.com","threshold_rtt":500000000,"attempts":5},{"type":"http","endpoint_name":"localhost","endpoint_url":"http://localhost:2015","threshold_rtt":1000000,"attempts":5}],"timestamp":"0001-01-01T00:00:00Z"} -------------------------------------------------------------------------------- /check/tcp/testdata/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBfDCCASKgAwIBAgIBBDAKBggqhkjOPQQDAjAkMRAwDgYDVQQKEwdBY21lIENv 3 | MRAwDgYDVQQDEwdSb290IENBMCAXDTIwMDQwMzExNTYzNFoYDzIxMjAwMzEwMTE1 4 | NjM0WjAyMRAwDgYDVQQKEwdBY21lIENvMR4wHAYDVQQDDBVjbGllbnRfYXV0aF90 5 | ZXN0X2NlcnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQpEdYFD/Wk/4K4R7h7 6 | 4Z/FPk6ZhPZYSpnA20sY2lLVV7WdKCa+8/vNgK2AyWvsj0gZz5fa5mfoUMENB6KQ 7 | AZ4iozUwMzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYD 8 | VR0TAQH/BAIwADAKBggqhkjOPQQDAgNIADBFAiEA+YWtXfDjGxoGlbhrLJ0n0g+5 9 | ELn0Wm2VAuuZ+to8C7wCIH7RCVa6bfawv/zKiR2QS+VukL39d3a0JVvUwKEdtrH7 10 | -----END CERTIFICATE----- 11 | -------------------------------------------------------------------------------- /check/tcp/testdata/root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBgDCCASagAwIBAgIQGF/isM52xotf9wwX3HbaGTAKBggqhkjOPQQDAjAkMRAw 3 | DgYDVQQKEwdBY21lIENvMRAwDgYDVQQDEwdSb290IENBMCAXDTIwMDQwMzExNTYz 4 | NFoYDzIxMjAwMzEwMTE1NjM0WjAkMRAwDgYDVQQKEwdBY21lIENvMRAwDgYDVQQD 5 | EwdSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDGrZliWf965C1kKH 6 | 2IZ0c0KpEUPyHx+LMycvPLUJb9JbRVUsw6mzxc6ugw8NxSYQZHijYJO4pm0PMj6j 7 | uArXX6M4MDYwDgYDVR0PAQH/BAQDAgIEMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8G 8 | A1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgFYA7TjBM1Eb62b2iSh0w 9 | 3yT7PgAXqhJcgpMcgwTobgUCIQCSiTKBAz4KfGET8TJ511Oy0d49uMfnagVh2yuh 10 | 5wudDA== 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /storage/sqlite3/sqlite_disabled.go: -------------------------------------------------------------------------------- 1 | // +build !sqlite3 2 | 3 | package sqlite3 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | 9 | "github.com/sourcegraph/checkup/types" 10 | ) 11 | 12 | var errStoreDisabled = errors.New("sqlite data store is disabled") 13 | 14 | // New creates a new Storage instance based on json config 15 | func New(_ json.RawMessage) (Storage, error) { 16 | return Storage{}, errStoreDisabled 17 | } 18 | 19 | // Type returns the storage driver package name 20 | func (Storage) Type() string { 21 | return Type 22 | } 23 | 24 | func (Storage) Store(results []types.Result) error { 25 | return errStoreDisabled 26 | } 27 | -------------------------------------------------------------------------------- /types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Errors is an error type that concatenates multiple errors. 8 | type Errors []error 9 | 10 | // Error returns a string containing all the errors in e. 11 | func (e Errors) Error() string { 12 | var errs []string 13 | for _, err := range e { 14 | if err != nil { 15 | errs = append(errs, err.Error()) 16 | } 17 | } 18 | return strings.Join(errs, "; ") 19 | } 20 | 21 | // Empty returns whether e has any non-nil errors in it. 22 | func (e Errors) Empty() bool { 23 | for _, err := range e { 24 | if err != nil { 25 | return false 26 | } 27 | } 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /storage/sql/sql_disabled.go: -------------------------------------------------------------------------------- 1 | // +build !sql 2 | 3 | package sql 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | 9 | "github.com/sourcegraph/checkup/types" 10 | ) 11 | 12 | type Storage struct{} 13 | 14 | var errStoreDisabled = errors.New("sql data store is disabled") 15 | 16 | // New creates a new Storage instance based on json config 17 | func New(_ json.RawMessage) (Storage, error) { 18 | return Storage{}, errStoreDisabled 19 | } 20 | 21 | // Type returns the storage driver package name 22 | func (Storage) Type() string { 23 | return Type 24 | } 25 | 26 | func (Storage) Store(results []types.Result) error { 27 | return errStoreDisabled 28 | } 29 | -------------------------------------------------------------------------------- /check/tcp/testdata/leaf.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBnTCCAUOgAwIBAgIQQR3+iZwMMthWHVdinJJTsDAKBggqhkjOPQQDAjAkMRAw 3 | DgYDVQQKEwdBY21lIENvMRAwDgYDVQQDEwdSb290IENBMCAXDTIwMDQwMzExNTYz 4 | NFoYDzIxMjAwMzEwMTE1NjM0WjAoMRAwDgYDVQQKEwdBY21lIENvMRQwEgYDVQQD 5 | DAt0ZXN0X2NlcnRfMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBvLle7jl3Yb 6 | NNTnegViesrMyOiHEbDIq1e3wGoxJt21XPRGi02yEVTDdPOSDZygSJ5NOzwIzgbk 7 | rRqzguB+VWujUTBPMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcD 8 | ATAMBgNVHRMBAf8EAjAAMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAKBggq 9 | hkjOPQQDAgNIADBFAiBNqSH4pq7uUxttnBoYp+7V2Sxx4X1mhM6PzApHS9RT3AIh 10 | AIom2Eh6KabH/9svaShaiLh73cu8W/B8yhRK9tCtkZtV 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /storage/fs/types.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sourcegraph/checkup/types" 7 | ) 8 | 9 | const IndexName = "index.json" 10 | 11 | // FilenameFormatString is the format string used 12 | // by GenerateFilename to create a filename. 13 | const FilenameFormatString = "%d-check.json" 14 | 15 | // GenerateFilename returns a filename that is ideal 16 | // for storing the results file on a storage provider 17 | // that relies on the filename for retrieval that is 18 | // sorted by date/timeframe. It returns a string pointer 19 | // to be used by the AWS SDK... 20 | func GenerateFilename() *string { 21 | s := fmt.Sprintf(FilenameFormatString, types.Timestamp()) 22 | return &s 23 | } 24 | -------------------------------------------------------------------------------- /check.go: -------------------------------------------------------------------------------- 1 | package checkup 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/sourcegraph/checkup/check/dns" 8 | "github.com/sourcegraph/checkup/check/exec" 9 | "github.com/sourcegraph/checkup/check/http" 10 | "github.com/sourcegraph/checkup/check/tcp" 11 | "github.com/sourcegraph/checkup/check/tls" 12 | ) 13 | 14 | func checkerDecode(typeName string, config json.RawMessage) (Checker, error) { 15 | switch typeName { 16 | case dns.Type: 17 | return dns.New(config) 18 | case exec.Type: 19 | return exec.New(config) 20 | case http.Type: 21 | return http.New(config) 22 | case tcp.Type: 23 | return tcp.New(config) 24 | case tls.Type: 25 | return tls.New(config) 26 | default: 27 | return nil, fmt.Errorf(errUnknownCheckerType, typeName) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /notifier.go: -------------------------------------------------------------------------------- 1 | package checkup 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/sourcegraph/checkup/notifier/discord" 8 | "github.com/sourcegraph/checkup/notifier/mail" 9 | "github.com/sourcegraph/checkup/notifier/mailgun" 10 | "github.com/sourcegraph/checkup/notifier/pushover" 11 | "github.com/sourcegraph/checkup/notifier/slack" 12 | ) 13 | 14 | func notifierDecode(typeName string, config json.RawMessage) (Notifier, error) { 15 | switch typeName { 16 | case mail.Type: 17 | return mail.New(config) 18 | case slack.Type: 19 | return slack.New(config) 20 | case mailgun.Type: 21 | return mailgun.New(config) 22 | case pushover.Type: 23 | return pushover.New(config) 24 | case discord.Type: 25 | return discord.New(config) 26 | default: 27 | return nil, fmt.Errorf(errUnknownNotifierType, typeName) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /storage/mysql/types.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Type should match the package name 8 | const Type = "mysql" 9 | 10 | // Storage is a way to store checkup results in a MySQL database. 11 | type Storage struct { 12 | DSN string `json:"dsn"` 13 | 14 | // Issue create statements for database schema 15 | Create bool `json:"create"` 16 | 17 | // Check files older than CheckExpiry will be 18 | // deleted on calls to Maintain(). If this is 19 | // the zero value, no old check files will be 20 | // deleted. 21 | CheckExpiry time.Duration `json:"check_expiry,omitempty"` 22 | } 23 | 24 | // schema is the expected table schema (can be re-applied) 25 | const schema = "CREATE TABLE IF NOT EXISTS `checks` (`name` VARCHAR(512) NOT NULL, `timestamp` BIGINT NOT NULL, `results` TEXT NULL, PRIMARY KEY (`name`), UNIQUE (`timestamp`)) ENGINE = InnoDB;" 26 | -------------------------------------------------------------------------------- /notifier/discord/types.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | const Type = "discord" 4 | 5 | type Notifier struct { 6 | Webhook string `json:"webhook"` 7 | } 8 | 9 | type Payload struct { 10 | Title string `json:"username"` 11 | Content string `json:"content"` 12 | Avatar string `json:"avatar_url"` 13 | Embeds []*Embed `json:"embeds"` 14 | } 15 | 16 | func (p *Payload) AddEmbed(embed *Embed) { 17 | p.Embeds = append(p.Embeds, embed) 18 | } 19 | 20 | type Embed struct { 21 | Title string `json:"title"` 22 | Description string `json:"description"` 23 | Color int `json:"color"` 24 | Fields []*Field `json:"fields"` 25 | } 26 | 27 | func (e *Embed) AddField(field *Field) { 28 | e.Fields = append(e.Fields, field) 29 | } 30 | 31 | type Field struct { 32 | Name string `json:"name"` 33 | Value string `json:"value"` 34 | Inline bool `json:"inline"` 35 | } 36 | -------------------------------------------------------------------------------- /storage/postgres/types.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Type should match the package name 8 | const Type = "postgres" 9 | 10 | // Storage is a way to store checkup results in a PostgreSQL database. 11 | type Storage struct { 12 | DSN string `json:"dsn"` 13 | 14 | // Issue create statements for database schema 15 | Create bool `json:"create"` 16 | 17 | // Check files older than CheckExpiry will be 18 | // deleted on calls to Maintain(). If this is 19 | // the zero value, no old check files will be 20 | // deleted. 21 | CheckExpiry time.Duration `json:"check_expiry,omitempty"` 22 | } 23 | 24 | // schema is the expected table schema (can be re-applied) 25 | const schema = `CREATE TABLE IF NOT EXISTS checks ( 26 | name TEXT NOT NULL PRIMARY KEY, 27 | timestamp INT8 NOT NULL, 28 | results TEXT 29 | ); 30 | CREATE UNIQUE INDEX IF NOT EXISTS idx_checks_timestamp ON checks(timestamp);` 31 | -------------------------------------------------------------------------------- /types/status.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // StatusText is the textual representation of the 4 | // result of a status check. 5 | type StatusText string 6 | 7 | // PriorityOver returns whether s has priority over other. 8 | // For example, a Down status has priority over Degraded. 9 | func (s StatusText) PriorityOver(other StatusText) bool { 10 | if s == other { 11 | return false 12 | } 13 | switch s { 14 | case StatusDown: 15 | return true 16 | case StatusDegraded: 17 | if other == StatusDown { 18 | return false 19 | } 20 | return true 21 | case StatusHealthy: 22 | if other == StatusUnknown { 23 | return true 24 | } 25 | return false 26 | } 27 | return false 28 | } 29 | 30 | // Text representations for the status of a check. 31 | const ( 32 | StatusHealthy StatusText = "healthy" 33 | StatusDegraded StatusText = "degraded" 34 | StatusDown StatusText = "down" 35 | StatusUnknown StatusText = "unknown" 36 | ) 37 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | workspace: 2 | base: /checkup 3 | 4 | kind: pipeline 5 | name: checkup 6 | 7 | steps: 8 | - name: test-mysql 9 | image: golang:1.14 10 | pull: always 11 | commands: 12 | - make test-mysql 13 | - make build-mysql 14 | 15 | - name: test-postgres 16 | image: golang:1.14 17 | pull: always 18 | commands: 19 | - make test-postgres 20 | - make build-postgres 21 | 22 | - name: test-sqlite3 23 | image: golang:1.14 24 | pull: always 25 | commands: 26 | - make test-sqlite3 27 | - make build-sqlite3 28 | 29 | services: 30 | - name: postgres-test-db 31 | image: postgres:12-alpine 32 | environment: 33 | POSTGRES_PASSWORD: test 34 | POSTGRES_USER: test 35 | POSTGRES_DB: test 36 | 37 | - name: mysql-test-db 38 | pull: always 39 | image: percona/percona-server:8.0.17 40 | environment: 41 | MYSQL_RANDOM_ROOT_PASSWORD: true 42 | MYSQL_USER: test 43 | MYSQL_PASSWORD: test 44 | MYSQL_DATABASE: test 45 | -------------------------------------------------------------------------------- /storage/sqlite3/types.go: -------------------------------------------------------------------------------- 1 | package sqlite3 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Type should match the package name 8 | const Type = "sqlite3" 9 | 10 | // Storage is a way to store checkup results in a SQLite database. 11 | type Storage struct { 12 | // DSN is the sqlite3 DB where check results will be stored. 13 | DSN string `json:"dsn,omitempty"` 14 | 15 | // Issue create statements for database schema 16 | Create bool `json:"create"` 17 | 18 | // Check files older than CheckExpiry will be 19 | // deleted on calls to Maintain(). If this is 20 | // the zero value, no old check files will be 21 | // deleted. 22 | CheckExpiry time.Duration `json:"check_expiry,omitempty"` 23 | } 24 | 25 | // schema is the expected table schema (can be re-applied) 26 | const schema = `CREATE TABLE IF NOT EXISTS checks ( 27 | name TEXT NOT NULL PRIMARY KEY, 28 | timestamp INT8 NOT NULL, 29 | results TEXT 30 | ); 31 | CREATE UNIQUE INDEX IF NOT EXISTS idx_checks_timestamp ON checks(timestamp);` 32 | -------------------------------------------------------------------------------- /.github/workflows/scip.yml: -------------------------------------------------------------------------------- 1 | name: SCIP 2 | 'on': 3 | - push 4 | jobs: 5 | scip-go: 6 | runs-on: ubuntu-latest 7 | container: sourcegraph/scip-go 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Get src-cli 11 | run: curl -L https://sourcegraph.com/.api/src-cli/src_linux_amd64 -o /usr/local/bin/src; 12 | chmod +x /usr/local/bin/src 13 | - name: Set directory to safe for git 14 | run: git config --global --add safe.directory $GITHUB_WORKSPACE 15 | - name: Generate SCIP data 16 | run: scip-go 17 | - name: Upload SCIP data to Cloud 18 | run: src code-intel upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress 19 | env: 20 | SRC_ENDPOINT: https://sourcegraph.com 21 | SRC_ACCESS_TOKEN: ${{ secrets.SRC_ACCESS_TOKEN_DOTCOM }} 22 | - name: Upload SCIP to S2 23 | run: src code-intel upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress 24 | env: 25 | SRC_ENDPOINT: https://sourcegraph.sourcegraph.com/ 26 | SRC_ACCESS_TOKEN: ${{ secrets.SRC_ACCESS_TOKEN_S2 }} 27 | -------------------------------------------------------------------------------- /statuspage/js/config.js: -------------------------------------------------------------------------------- 1 | checkup.config = { 2 | // How much history to show on the status page. Long durations and 3 | // frequent checks make for slow loading, so be conservative. 4 | // This value is in NANOSECONDS to mirror Go's time package. 5 | "timeframe": 1 * time.Day, 6 | 7 | // How often, in seconds, to pull new checks and update the page. 8 | "refresh_interval": 60, 9 | 10 | // Configure read-only access to stored checks. This configuration 11 | // depends on your storage provider. Any credentials and other values 12 | // here will be visible to everyone, so use keys with ONLY read access! 13 | "storage": { 14 | // Storage type (fs for local, s3 for AWS S3) 15 | "type": "fs", 16 | // Local checkup server by default, set to github page if 17 | // you're hosting your status page on GitHub. 18 | // e.g. "https://sourcegraph.github.io/checkup/checks/" 19 | "url": "/" 20 | }, 21 | 22 | // The text to display along the top bar depending on overall status. 23 | "status_text": { 24 | "healthy": "Situation Normal", 25 | "degraded": "Degraded Service", 26 | "down": "Service Disruption" 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sourcegraph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package checkup 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/sourcegraph/checkup/storage/appinsights" 8 | "github.com/sourcegraph/checkup/storage/fs" 9 | "github.com/sourcegraph/checkup/storage/github" 10 | "github.com/sourcegraph/checkup/storage/mysql" 11 | "github.com/sourcegraph/checkup/storage/postgres" 12 | "github.com/sourcegraph/checkup/storage/s3" 13 | "github.com/sourcegraph/checkup/storage/sql" 14 | "github.com/sourcegraph/checkup/storage/sqlite3" 15 | ) 16 | 17 | func storageDecode(typeName string, config json.RawMessage) (Storage, error) { 18 | switch typeName { 19 | case sqlite3.Type: 20 | return sqlite3.New(config) 21 | case mysql.Type: 22 | return mysql.New(config) 23 | case postgres.Type: 24 | return postgres.New(config) 25 | case s3.Type: 26 | return s3.New(config) 27 | case github.Type: 28 | return github.New(config) 29 | case fs.Type: 30 | return fs.New(config) 31 | case sql.Type: 32 | return sql.New(config) 33 | case appinsights.Type: 34 | return appinsights.New(config) 35 | default: 36 | return nil, fmt.Errorf(errUnknownStorageType, typeName) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sourcegraph/checkup 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 7 | github.com/aws/aws-sdk-go v1.30.7 8 | github.com/elazarl/goproxy v0.0.0-20200315184450-1f3cb6622dad // indirect 9 | github.com/fatih/color v1.9.0 10 | github.com/go-sql-driver/mysql v1.5.0 11 | github.com/google/go-github v17.0.0+incompatible 12 | github.com/google/go-querystring v1.0.0 // indirect 13 | github.com/gregdel/pushover v0.0.0-20200416074932-c8ad547caed4 14 | github.com/jmoiron/sqlx v1.2.0 15 | github.com/lib/pq v1.3.0 16 | github.com/mailgun/mailgun-go/v4 v4.1.0 17 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 18 | github.com/microsoft/ApplicationInsights-Go v0.4.3 19 | github.com/miekg/dns v1.1.29 20 | github.com/parnurzeal/gorequest v0.2.16 // indirect 21 | github.com/smartystreets/goconvey v1.6.4 // indirect 22 | github.com/spf13/cobra v0.0.7 23 | golang.org/x/net v0.33.0 // indirect 24 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d 25 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 26 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 27 | moul.io/http2curl v1.0.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /check/exec/exec_test.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestChecker(t *testing.T) { 8 | assert := func(ok bool, format string, args ...interface{}) { 9 | if !ok { 10 | t.Errorf(format, args...) 11 | } 12 | } 13 | 14 | command := "testdata/exec.sh" 15 | 16 | // check non-zero exit code 17 | { 18 | testName := "Non-zero exit" 19 | hc := Checker{Name: testName, Command: command, Arguments: []string{"1", testName}, Attempts: 2} 20 | 21 | result, err := hc.Check() 22 | assert(err == nil, "expected no error, got %v, %#v", err, result) 23 | assert(result.Title == testName, "expected result.Title == %s, got '%s'", testName, result.Title) 24 | assert(result.Down == true, "expected result.Down = true, got %v", result.Down) 25 | } 26 | 27 | // check zero exit code 28 | { 29 | testName := "Non-zero exit" 30 | hc := Checker{Name: testName, Command: command, Arguments: []string{"0", testName}, Attempts: 2} 31 | 32 | result, err := hc.Check() 33 | assert(err == nil, "expected no error, got %v, %#v", err, result) 34 | assert(result.Title == testName, "expected result.Title == %s, got '%s'", testName, result.Title) 35 | assert(result.Down == false, "expected result.Down = false, got %v", result.Down) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /statuspage/js/config_s3.js: -------------------------------------------------------------------------------- 1 | checkup.config = { 2 | // How much history to show on the status page. Long durations and 3 | // frequent checks make for slow loading, so be conservative. 4 | // This value is in NANOSECONDS to mirror Go's time package. 5 | "timeframe": 1 * time.Day, 6 | 7 | // How often, in seconds, to pull new checks and update the page. 8 | "refresh_interval": 60, 9 | 10 | // Configure read-only access to stored checks. This configuration 11 | // depends on your storage provider. Any credentials and other values 12 | // here will be visible to everyone, so use keys with ONLY read access! 13 | "storage": { 14 | // Storage type (fs for local, s3 for AWS S3) 15 | "type": "s3", 16 | 17 | // Amazon S3 - if using, ensure these are public, READ-ONLY credentials! 18 | "AccessKeyID": "", 19 | "SecretAccessKey": "", 20 | "Region": "", 21 | "BucketName": "", 22 | 23 | // Local file system (Caddy recommended: https://caddyserver.com) 24 | "url": "http://127.0.0.1:2015/" 25 | }, 26 | 27 | // The text to display along the top bar depending on overall status. 28 | "status_text": { 29 | "healthy": "Situation Normal", 30 | "degraded": "Degraded Service", 31 | "down": "Service Disruption" 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /notifier/pushover/pushover.go: -------------------------------------------------------------------------------- 1 | package pushover 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/gregdel/pushover" 9 | "github.com/sourcegraph/checkup/types" 10 | ) 11 | 12 | const Type = "pushover" 13 | 14 | type Notifier struct { 15 | Token string `json:"token"` 16 | Recipient string `json:"recipient"` 17 | Subject string `json:"subject,omitempty"` 18 | } 19 | 20 | func New(config json.RawMessage) (Notifier, error) { 21 | var notifier Notifier 22 | err := json.Unmarshal(config, ¬ifier) 23 | if strings.TrimSpace(notifier.Subject) == "" { 24 | notifier.Subject = "Checkup: Service Unavailable" 25 | } 26 | return notifier, err 27 | } 28 | 29 | func (Notifier) Type() string { 30 | return Type 31 | } 32 | 33 | func (p Notifier) Notify(results []types.Result) error { 34 | issues := []types.Result{} 35 | for _, result := range results { 36 | if !result.Healthy { 37 | issues = append(issues, result) 38 | } 39 | } 40 | 41 | if len(issues) == 0 { 42 | return nil 43 | } 44 | 45 | app := pushover.New(p.Token) 46 | recipient := pushover.NewRecipient(p.Recipient) 47 | msg := pushover.NewMessageWithTitle(renderMessage(issues), p.Subject) 48 | 49 | _, err := app.SendMessage(msg, recipient) 50 | return err 51 | } 52 | 53 | func renderMessage(issues []types.Result) string { 54 | body := []string{"Checkup has detected the following issues:", "\n\n"} 55 | for _, issue := range issues { 56 | format := "%s - Status: %s" 57 | body = append(body, fmt.Sprintf(format, issue.Title, issue.Status())) 58 | } 59 | return strings.Join(body, "\n") 60 | } 61 | -------------------------------------------------------------------------------- /types/provisioner.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ProvisionInfo contains the results of provisioning a new 8 | // storage facility for check files. Its values should be 9 | // used by the status page in order to obtain read-only 10 | // access to the check files. 11 | type ProvisionInfo struct { 12 | // The ID of a user that was created for accessing checks. 13 | UserID string `json:"user_id"` 14 | 15 | // The username of a user that was created for accessing checks. 16 | Username string `json:"username"` 17 | 18 | // The ID or name of the ID/key used to access checks. Expect 19 | // this value to be made public. (It should have read-only 20 | // access to the checks.) 21 | PublicAccessKeyID string `json:"public_access_key_id"` 22 | 23 | // The "secret" associated with the PublicAccessKeyID, but 24 | // expect this value to be made public. (It should provide 25 | // read-only access to the checks.) 26 | PublicAccessKey string `json:"public_access_key"` 27 | } 28 | 29 | // String returns the information in i in a human-readable format 30 | // along with an important notice. 31 | func (i ProvisionInfo) String() string { 32 | s := "Provision successful\n\n" 33 | s += fmt.Sprintf(" User ID: %s\n", i.UserID) 34 | s += fmt.Sprintf(" Username: %s\n", i.Username) 35 | s += fmt.Sprintf("Public Access Key ID: %s\n", i.PublicAccessKeyID) 36 | s += fmt.Sprintf(" Public Access Key: %s\n\n", i.PublicAccessKey) 37 | s += `IMPORTANT: Copy the Public Access Key ID and Public Access 38 | Key into the config.js file for your status page. You will 39 | not be shown these credentials again.` 40 | return s 41 | } 42 | -------------------------------------------------------------------------------- /statuspage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Status Page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | Loading 21 |
22 |
23 |
24 | checks in the last 25 |
26 |
27 | Last check: 28 |
29 |
30 |
31 | 32 |
33 |
34 | 35 |   36 |
37 |
38 |
39 | There is a big gap of time where no checkups were performed, so some graphs may look distorted. 40 |
41 |
42 |
43 |
44 | 45 |
46 | Powered by 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /cmd/every.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var everyCmd = &cobra.Command{ 14 | Use: "every", 15 | Short: "Run checks indefinitely at an interval", 16 | Long: `The every subcommand runs checkups at the interval you 17 | specify. The result of each check is saved to storage. 18 | Additionally, if a Notifier is configured, it will be 19 | called to analyze and potentially notify you of any 20 | problems. 21 | 22 | This command never unblocks, so you must signal the 23 | program to exit. 24 | 25 | Interval formats are the same as those for Go's 26 | time.ParseDuration() syntax: 27 | https://golang.org/pkg/time/#ParseDuration - with a 28 | few shortcuts: second, minute, hour, day, and week. 29 | 30 | Examples: 31 | 32 | $ checkup every 10m 33 | $ checkup every day 34 | $ checkup every 1h30m`, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | if len(args) != 1 { 37 | fmt.Println(cmd.Long) 38 | os.Exit(1) 39 | } 40 | 41 | itvlStr := strings.ToLower(args[0]) 42 | switch itvlStr { 43 | case "second": 44 | itvlStr = "1s" 45 | case "minute": 46 | itvlStr = "1m" 47 | case "hour": 48 | itvlStr = "1h" 49 | case "day": 50 | itvlStr = "24h" 51 | case "week": 52 | itvlStr = "168h" 53 | } 54 | 55 | interval, err := time.ParseDuration(itvlStr) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | c := loadCheckup() 61 | if len(c.Checkers) == 0 { 62 | log.Fatal("no checkers configured") 63 | } 64 | if c.Storage == nil { 65 | log.Fatal("no storage configured") 66 | } 67 | 68 | c.CheckAndStoreEvery(interval) 69 | select {} 70 | }, 71 | } 72 | 73 | func init() { 74 | RootCmd.AddCommand(everyCmd) 75 | } 76 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | package checkup 2 | 3 | import ( 4 | "github.com/sourcegraph/checkup/types" 5 | ) 6 | 7 | // Checker can create a types.Result. 8 | type Checker interface { 9 | Type() string 10 | Check() (types.Result, error) 11 | } 12 | 13 | // Storage can store results. 14 | type Storage interface { 15 | Type() string 16 | Store([]types.Result) error 17 | } 18 | 19 | // StorageReader can read results from the Storage. 20 | type StorageReader interface { 21 | // Fetch returns the contents of a check file. 22 | Fetch(checkFile string) ([]types.Result, error) 23 | // GetIndex returns the storage index, as a map where keys are check 24 | // result filenames and values are the associated check timestamps. 25 | GetIndex() (map[string]int64, error) 26 | } 27 | 28 | // Maintainer can maintain a store of results by 29 | // deleting old check files that are no longer 30 | // needed or performing other required tasks. 31 | type Maintainer interface { 32 | Maintain() error 33 | } 34 | 35 | // Notifier can notify ops or sysadmins of 36 | // potential problems. A Notifier should keep 37 | // state to avoid sending repeated notices 38 | // more often than the admin would like. 39 | type Notifier interface { 40 | Type() string 41 | Notify([]types.Result) error 42 | } 43 | 44 | // Provisioner is a type of storage mechanism that can 45 | // provision itself for use with checkup. Provisioning 46 | // need only happen once and is merely a convenience 47 | // so that the user can get up and running with their 48 | // status page more quickly. Presumably, the info 49 | // returned from Provision should be used on the status 50 | // page side of things ot access the check files (like 51 | // a key pair that is used for read-only access). 52 | type Provisioner interface { 53 | Provision() (types.ProvisionInfo, error) 54 | } 55 | -------------------------------------------------------------------------------- /notifier/slack/slack.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | slack "github.com/ashwanthkumar/slack-go-webhook" 9 | 10 | "github.com/sourcegraph/checkup/types" 11 | ) 12 | 13 | // Type should match the package name 14 | const Type = "slack" 15 | 16 | // Notifier consist of all the sub components required to use Slack API 17 | type Notifier struct { 18 | Username string `json:"username"` 19 | Channel string `json:"channel"` 20 | Webhook string `json:"webhook"` 21 | } 22 | 23 | // New creates a new Notifier instance based on json config 24 | func New(config json.RawMessage) (Notifier, error) { 25 | var notifier Notifier 26 | err := json.Unmarshal(config, ¬ifier) 27 | return notifier, err 28 | } 29 | 30 | // Type returns the notifier package name 31 | func (Notifier) Type() string { 32 | return Type 33 | } 34 | 35 | // Notify implements notifier interface 36 | func (s Notifier) Notify(results []types.Result) error { 37 | errs := make(types.Errors, 0) 38 | for _, result := range results { 39 | if !result.Healthy { 40 | if err := s.Send(result); err != nil { 41 | errs = append(errs, err) 42 | } 43 | } 44 | } 45 | if len(errs) == 0 { 46 | return nil 47 | } 48 | return errs 49 | } 50 | 51 | // Send request via Slack API to create incident 52 | func (s Notifier) Send(result types.Result) error { 53 | color := "danger" 54 | attach := slack.Attachment{} 55 | attach.AddField(slack.Field{Title: result.Title, Value: result.Endpoint}) 56 | attach.AddField(slack.Field{Title: "Status", Value: strings.ToUpper(fmt.Sprint(result.Status()))}) 57 | attach.Color = &color 58 | payload := slack.Payload{ 59 | Text: result.Title, 60 | Username: s.Username, 61 | Channel: s.Channel, 62 | Attachments: []slack.Attachment{attach}, 63 | } 64 | 65 | return types.Errors(slack.Send(s.Webhook, "", payload)) 66 | } 67 | -------------------------------------------------------------------------------- /notifier/mailgun/mailgun.go: -------------------------------------------------------------------------------- 1 | package mailgun 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | mailgun "github.com/mailgun/mailgun-go/v4" 11 | "github.com/sourcegraph/checkup/types" 12 | ) 13 | 14 | const Type = "mailgun" 15 | 16 | type Notifier struct { 17 | // From contains the e-mail address notifications are sent from 18 | From string `json:"from"` 19 | 20 | // To contains a list of e-mail address destinations 21 | To []string `json:"to"` 22 | 23 | // Subject contains customizable subject line 24 | Subject string `json:"subject,omitempty"` 25 | 26 | // Mailgun specific API settings 27 | APIKey string `json:"apikey"` 28 | Domain string `json:"domain"` 29 | } 30 | 31 | func New(config json.RawMessage) (Notifier, error) { 32 | var notifier Notifier 33 | err := json.Unmarshal(config, ¬ifier) 34 | if strings.TrimSpace(notifier.Subject) == "" { 35 | notifier.Subject = "Checkup: Service Unavailable" 36 | } 37 | return notifier, err 38 | } 39 | 40 | func (Notifier) Type() string { 41 | return Type 42 | } 43 | 44 | func (m Notifier) Notify(results []types.Result) error { 45 | issues := []types.Result{} 46 | for _, result := range results { 47 | if !result.Healthy { 48 | issues = append(issues, result) 49 | } 50 | } 51 | 52 | if len(issues) == 0 { 53 | return nil 54 | } 55 | 56 | mg := mailgun.NewMailgun(m.Domain, m.APIKey) 57 | msg := mg.NewMessage(m.From, m.Subject, renderMessage(issues), m.To...) 58 | 59 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 60 | defer cancel() 61 | 62 | _, _, err := mg.Send(ctx, msg) 63 | return err 64 | } 65 | 66 | func renderMessage(issues []types.Result) string { 67 | body := []string{"Checkup has detected the following issues:", "

", "
    "} 68 | for _, issue := range issues { 69 | format := "
  • %s - Status %s
  • " 70 | body = append(body, fmt.Sprintf(format, issue.Title, issue.Status())) 71 | } 72 | body = append(body, "
") 73 | return strings.Join(body, "\n") 74 | } 75 | -------------------------------------------------------------------------------- /check/tcp/testdata/client.debug.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 4 (0x4) 5 | Signature Algorithm: ecdsa-with-SHA256 6 | Issuer: O = Acme Co, CN = Root CA 7 | Validity 8 | Not Before: Apr 3 11:56:34 2020 GMT 9 | Not After : Mar 10 11:56:34 2120 GMT 10 | Subject: O = Acme Co, CN = client_auth_test_cert 11 | Subject Public Key Info: 12 | Public Key Algorithm: id-ecPublicKey 13 | Public-Key: (256 bit) 14 | pub: 15 | 04:29:11:d6:05:0f:f5:a4:ff:82:b8:47:b8:7b:e1: 16 | 9f:c5:3e:4e:99:84:f6:58:4a:99:c0:db:4b:18:da: 17 | 52:d5:57:b5:9d:28:26:be:f3:fb:cd:80:ad:80:c9: 18 | 6b:ec:8f:48:19:cf:97:da:e6:67:e8:50:c1:0d:07: 19 | a2:90:01:9e:22 20 | ASN1 OID: prime256v1 21 | NIST CURVE: P-256 22 | X509v3 extensions: 23 | X509v3 Key Usage: critical 24 | Digital Signature 25 | X509v3 Extended Key Usage: 26 | TLS Web Client Authentication 27 | X509v3 Basic Constraints: critical 28 | CA:FALSE 29 | Signature Algorithm: ecdsa-with-SHA256 30 | 30:45:02:21:00:f9:85:ad:5d:f0:e3:1b:1a:06:95:b8:6b:2c: 31 | 9d:27:d2:0f:b9:10:b9:f4:5a:6d:95:02:eb:99:fa:da:3c:0b: 32 | bc:02:20:7e:d1:09:56:ba:6d:f6:b0:bf:fc:ca:89:1d:90:4b: 33 | e5:6e:90:bd:fd:77:76:b4:25:5b:d4:c0:a1:1d:b6:b1:fb 34 | -----BEGIN CERTIFICATE----- 35 | MIIBfDCCASKgAwIBAgIBBDAKBggqhkjOPQQDAjAkMRAwDgYDVQQKEwdBY21lIENv 36 | MRAwDgYDVQQDEwdSb290IENBMCAXDTIwMDQwMzExNTYzNFoYDzIxMjAwMzEwMTE1 37 | NjM0WjAyMRAwDgYDVQQKEwdBY21lIENvMR4wHAYDVQQDDBVjbGllbnRfYXV0aF90 38 | ZXN0X2NlcnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQpEdYFD/Wk/4K4R7h7 39 | 4Z/FPk6ZhPZYSpnA20sY2lLVV7WdKCa+8/vNgK2AyWvsj0gZz5fa5mfoUMENB6KQ 40 | AZ4iozUwMzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYD 41 | VR0TAQH/BAIwADAKBggqhkjOPQQDAgNIADBFAiEA+YWtXfDjGxoGlbhrLJ0n0g+5 42 | ELn0Wm2VAuuZ+to8C7wCIH7RCVa6bfawv/zKiR2QS+VukL39d3a0JVvUwKEdtrH7 43 | -----END CERTIFICATE----- 44 | -------------------------------------------------------------------------------- /check/tcp/testdata/root.debug.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 18:5f:e2:b0:ce:76:c6:8b:5f:f7:0c:17:dc:76:da:19 6 | Signature Algorithm: ecdsa-with-SHA256 7 | Issuer: O = Acme Co, CN = Root CA 8 | Validity 9 | Not Before: Apr 3 11:56:34 2020 GMT 10 | Not After : Mar 10 11:56:34 2120 GMT 11 | Subject: O = Acme Co, CN = Root CA 12 | Subject Public Key Info: 13 | Public Key Algorithm: id-ecPublicKey 14 | Public-Key: (256 bit) 15 | pub: 16 | 04:0c:6a:d9:96:25:9f:f7:ae:42:d6:42:87:d8:86: 17 | 74:73:42:a9:11:43:f2:1f:1f:8b:33:27:2f:3c:b5: 18 | 09:6f:d2:5b:45:55:2c:c3:a9:b3:c5:ce:ae:83:0f: 19 | 0d:c5:26:10:64:78:a3:60:93:b8:a6:6d:0f:32:3e: 20 | a3:b8:0a:d7:5f 21 | ASN1 OID: prime256v1 22 | NIST CURVE: P-256 23 | X509v3 extensions: 24 | X509v3 Key Usage: critical 25 | Certificate Sign 26 | X509v3 Extended Key Usage: 27 | TLS Web Server Authentication 28 | X509v3 Basic Constraints: critical 29 | CA:TRUE 30 | Signature Algorithm: ecdsa-with-SHA256 31 | 30:45:02:20:15:80:3b:4e:30:4c:d4:46:fa:d9:bd:a2:4a:1d: 32 | 30:df:24:fb:3e:00:17:aa:12:5c:82:93:1c:83:04:e8:6e:05: 33 | 02:21:00:92:89:32:81:03:3e:0a:7c:61:13:f1:32:79:d7:53: 34 | b2:d1:de:3d:b8:c7:e7:6a:05:61:db:2b:a1:e7:0b:9d:0c 35 | -----BEGIN CERTIFICATE----- 36 | MIIBgDCCASagAwIBAgIQGF/isM52xotf9wwX3HbaGTAKBggqhkjOPQQDAjAkMRAw 37 | DgYDVQQKEwdBY21lIENvMRAwDgYDVQQDEwdSb290IENBMCAXDTIwMDQwMzExNTYz 38 | NFoYDzIxMjAwMzEwMTE1NjM0WjAkMRAwDgYDVQQKEwdBY21lIENvMRAwDgYDVQQD 39 | EwdSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDGrZliWf965C1kKH 40 | 2IZ0c0KpEUPyHx+LMycvPLUJb9JbRVUsw6mzxc6ugw8NxSYQZHijYJO4pm0PMj6j 41 | uArXX6M4MDYwDgYDVR0PAQH/BAQDAgIEMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8G 42 | A1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgFYA7TjBM1Eb62b2iSh0w 43 | 3yT7PgAXqhJcgpMcgwTobgUCIQCSiTKBAz4KfGET8TJ511Oy0d49uMfnagVh2yuh 44 | 5wudDA== 45 | -----END CERTIFICATE----- 46 | -------------------------------------------------------------------------------- /cmd/message.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var about string 13 | 14 | var messageCmd = &cobra.Command{ 15 | Use: "message", 16 | Short: "Post a status message/update", 17 | Long: `The message subcommand allows you to post updates to 18 | to your status page for a certain endpoint. This is 19 | helpful (and responsible of you) when your service is 20 | experiencing a disruption or you are starting planned 21 | maintenance. 22 | 23 | Checkup will always report the facts, even if the 24 | disruption is planned. You can use status messages to 25 | give clarity and transparency to your customers 26 | or visitors. 27 | 28 | If your checkup configuration specifies more than one 29 | endpoint, you must use the --about flag to specify 30 | the name of the endpoint as defined in your config 31 | file.`, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | if len(args) != 1 { 34 | fmt.Println(cmd.Long) 35 | os.Exit(1) 36 | } 37 | 38 | contents := args[0] 39 | 40 | c := loadCheckup() 41 | if c.Storage == nil { 42 | log.Fatal("no storage configured") 43 | } 44 | 45 | results, err := c.Check() 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | if len(results) > 1 && about == "" { 51 | log.Fatal("more than one result; unable to guess which one to attach message to") 52 | } 53 | 54 | if len(results) == 1 && about == "" { 55 | results[0].Message = contents 56 | } else { 57 | found := false 58 | lowerAbout := strings.ToLower(about) 59 | for i, result := range results { 60 | if strings.ToLower(result.Title) == lowerAbout { 61 | results[i].Message = contents 62 | found = true 63 | break 64 | } 65 | } 66 | if !found { 67 | log.Fatalf("no result for endpoint with title '%s'", about) 68 | } 69 | } 70 | 71 | err = c.Storage.Store(results) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | fmt.Println("Message posted") 77 | }, 78 | } 79 | 80 | func init() { 81 | RootCmd.AddCommand(messageCmd) 82 | messageCmd.Flags().StringVarP(&about, "about", "a", "", "The name/title of the endpoint this message is about") 83 | } 84 | -------------------------------------------------------------------------------- /check/tcp/testdata/leaf.debug.crt: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 5 | 41:1d:fe:89:9c:0c:32:d8:56:1d:57:62:9c:92:53:b0 6 | Signature Algorithm: ecdsa-with-SHA256 7 | Issuer: O = Acme Co, CN = Root CA 8 | Validity 9 | Not Before: Apr 3 11:56:34 2020 GMT 10 | Not After : Mar 10 11:56:34 2120 GMT 11 | Subject: O = Acme Co, CN = test_cert_1 12 | Subject Public Key Info: 13 | Public Key Algorithm: id-ecPublicKey 14 | Public-Key: (256 bit) 15 | pub: 16 | 04:1b:cb:95:ee:e3:97:76:1b:34:d4:e7:7a:05:62: 17 | 7a:ca:cc:c8:e8:87:11:b0:c8:ab:57:b7:c0:6a:31: 18 | 26:dd:b5:5c:f4:46:8b:4d:b2:11:54:c3:74:f3:92: 19 | 0d:9c:a0:48:9e:4d:3b:3c:08:ce:06:e4:ad:1a:b3: 20 | 82:e0:7e:55:6b 21 | ASN1 OID: prime256v1 22 | NIST CURVE: P-256 23 | X509v3 extensions: 24 | X509v3 Key Usage: critical 25 | Digital Signature, Key Encipherment 26 | X509v3 Extended Key Usage: 27 | TLS Web Server Authentication 28 | X509v3 Basic Constraints: critical 29 | CA:FALSE 30 | X509v3 Subject Alternative Name: 31 | DNS:localhost, IP Address:127.0.0.1 32 | Signature Algorithm: ecdsa-with-SHA256 33 | 30:45:02:20:4d:a9:21:f8:a6:ae:ee:53:1b:6d:9c:1a:18:a7: 34 | ee:d5:d9:2c:71:e1:7d:66:84:ce:8f:cc:0a:47:4b:d4:53:dc: 35 | 02:21:00:8a:26:d8:48:7a:29:a6:c7:ff:db:2f:69:28:5a:88: 36 | b8:7b:dd:cb:bc:5b:f0:7c:ca:14:4a:f6:d0:ad:91:9b:55 37 | -----BEGIN CERTIFICATE----- 38 | MIIBnTCCAUOgAwIBAgIQQR3+iZwMMthWHVdinJJTsDAKBggqhkjOPQQDAjAkMRAw 39 | DgYDVQQKEwdBY21lIENvMRAwDgYDVQQDEwdSb290IENBMCAXDTIwMDQwMzExNTYz 40 | NFoYDzIxMjAwMzEwMTE1NjM0WjAoMRAwDgYDVQQKEwdBY21lIENvMRQwEgYDVQQD 41 | DAt0ZXN0X2NlcnRfMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBvLle7jl3Yb 42 | NNTnegViesrMyOiHEbDIq1e3wGoxJt21XPRGi02yEVTDdPOSDZygSJ5NOzwIzgbk 43 | rRqzguB+VWujUTBPMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcD 44 | ATAMBgNVHRMBAf8EAjAAMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAKBggq 45 | hkjOPQQDAgNIADBFAiBNqSH4pq7uUxttnBoYp+7V2Sxx4X1mhM6PzApHS9RT3AIh 46 | AIom2Eh6KabH/9svaShaiLh73cu8W/B8yhRK9tCtkZtV 47 | -----END CERTIFICATE----- 48 | -------------------------------------------------------------------------------- /statuspage/js/fs.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | FS Storage Adapter for Checkup.js 4 | 5 | **/ 6 | 7 | var checkup = checkup || {}; 8 | 9 | checkup.storageDriverLocal = (function() { 10 | var url; 11 | 12 | // getCheckFileList gets the list of check files within 13 | // the given timeframe (as a unit of nanoseconds) to 14 | // download. 15 | function getCheckFileList(timeframe, callback) { 16 | var after = time.Now() - timeframe; 17 | checkup.getJSON(url+'/index.json?t=' + Date.now(), function(index) { 18 | var names = []; 19 | for (var name in index) { 20 | if (index[name] >= after) { 21 | names.push(name); 22 | } 23 | } 24 | callback(names); 25 | }); 26 | }; 27 | 28 | // setup prepares this storage unit to operate. 29 | this.setup = function(cfg) { 30 | url = cfg.url; 31 | // trim trailing slash 32 | if (url.substring(-1) === "/") { 33 | url = url.substring(url, 0, -1) 34 | } 35 | }; 36 | 37 | // getChecksWithin gets all the checks within timeframe as a unit 38 | // of nanoseconds, and executes callback for each check file. 39 | this.getChecksWithin = function(timeframe, fileCallback, doneCallback) { 40 | var checksLoaded = 0, resultsLoaded = 0; 41 | getCheckFileList(timeframe, function(list) { 42 | if (list.length == 0 && (typeof doneCallback === 'function')) { 43 | doneCallback(checksLoaded); 44 | } else { 45 | for (var i = 0; i < list.length; i++) { 46 | checkup.getJSON(url+'/'+list[i], function(filename) { 47 | return function(json, url) { 48 | checksLoaded++; 49 | resultsLoaded += json.length; 50 | if (typeof fileCallback === 'function') 51 | fileCallback(json, filename); 52 | if (checksLoaded >= list.length && (typeof doneCallback === 'function')) 53 | doneCallback(checksLoaded, resultsLoaded); 54 | }; 55 | }(list[i])); 56 | } 57 | } 58 | }); 59 | }; 60 | 61 | // getNewChecks gets any checks since the timestamp on the file name 62 | // of the youngest check file that has been downloaded. If no check 63 | // files have been downloaded, no new check files will be loaded. 64 | this.getNewChecks = function(fileCallback, doneCallback) { 65 | if (!checkup.lastCheckTs == null) 66 | return; 67 | var timeframe = time.Now() - checkup.lastCheckTs; 68 | return this.getChecksWithin(timeframe, fileCallback, doneCallback); 69 | }; 70 | 71 | return this; 72 | }); 73 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | 10 | "github.com/sourcegraph/checkup" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var configFile string 15 | var storeResults bool 16 | 17 | // RootCmd represents the base command when called without any subcommands 18 | var RootCmd = &cobra.Command{ 19 | Use: "checkup", 20 | Short: "Perform checks on your services and sites", 21 | Long: `Checkup is distributed, lock-free, self-hosted health 22 | checks and status pages. 23 | 24 | Checkup will always look for a checkup.json file in 25 | the current working directory by default and use it. 26 | You can specify a different file location using the 27 | --config/-c flag. 28 | 29 | Running checkup without any arguments will invoke 30 | a single checkup and print results to stdout. To 31 | store the results of the check, use --store.`, 32 | 33 | Run: func(cmd *cobra.Command, args []string) { 34 | allHealthy := true 35 | c := loadCheckup() 36 | 37 | if storeResults { 38 | if c.Storage == nil { 39 | log.Fatal("no storage configured") 40 | } 41 | } 42 | 43 | results, err := c.Check() 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | if storeResults { 49 | err := c.Storage.Store(results) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | return 54 | } 55 | 56 | for _, result := range results { 57 | fmt.Println(result) 58 | if !result.Healthy { 59 | allHealthy = false 60 | } 61 | } 62 | 63 | if !allHealthy { 64 | os.Exit(1) 65 | } 66 | }, 67 | } 68 | 69 | func loadCheckup() checkup.Checkup { 70 | configBytes, err := ioutil.ReadFile(configFile) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | var c checkup.Checkup 76 | err = json.Unmarshal(configBytes, &c) 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | 81 | return c 82 | } 83 | 84 | // Execute adds all child commands to the root command sets flags appropriately. 85 | // This is called by main.main(). It only needs to happen once to the rootCmd. 86 | func Execute() { 87 | if err := RootCmd.Execute(); err != nil { 88 | fmt.Println(err) 89 | os.Exit(-1) 90 | } 91 | } 92 | 93 | func init() { 94 | RootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "checkup.json", "JSON config file") 95 | RootCmd.Flags().BoolVar(&storeResults, "store", false, "Store results") 96 | } 97 | -------------------------------------------------------------------------------- /notifier/mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "gopkg.in/gomail.v2" 9 | 10 | "github.com/sourcegraph/checkup/types" 11 | ) 12 | 13 | // Type should match the package name 14 | const Type = "mail" 15 | 16 | // Notifier consist of all the sub components required to send E-mail notifications 17 | type Notifier struct { 18 | // From contains the e-mail address notifications are sent from 19 | From string `json:"from"` 20 | 21 | // To contains a list of e-mail address destinations 22 | To []string `json:"to"` 23 | 24 | // Subject contains customizable subject line 25 | Subject string `json:"subject,omitempty"` 26 | 27 | // SMTP contains all relevant mail server settings 28 | SMTP struct { 29 | Server string `json:"server"` 30 | Port int `json:"port,omitempty"` 31 | Username string `json:"username,omitempty"` 32 | Password string `json:"password,omitempty"` 33 | } `json:"smtp"` 34 | } 35 | 36 | // New creates a new Notifier instance based on json config 37 | func New(config json.RawMessage) (Notifier, error) { 38 | var notifier Notifier 39 | err := json.Unmarshal(config, ¬ifier) 40 | // Fall back to port 25 if not defined 41 | if notifier.SMTP.Port == 0 { 42 | notifier.SMTP.Port = 25 43 | } 44 | if strings.TrimSpace(notifier.Subject) == "" { 45 | notifier.Subject = "Checkup: Service Unavailable" 46 | } 47 | return notifier, err 48 | } 49 | 50 | // Type returns the notifier package name 51 | func (Notifier) Type() string { 52 | return Type 53 | } 54 | 55 | // Notify implements notifier interface 56 | func (m Notifier) Notify(results []types.Result) error { 57 | issues := []types.Result{} 58 | for _, result := range results { 59 | if !result.Healthy { 60 | issues = append(issues, result) 61 | } 62 | } 63 | 64 | if len(issues) == 0 { 65 | return nil 66 | } 67 | 68 | message := gomail.NewMessage() 69 | message.SetHeader("From", m.From) 70 | message.SetHeader("To", m.To...) 71 | message.SetHeader("Subject", m.Subject) 72 | message.SetBody("text/html", renderMessage(issues)) 73 | 74 | dialer := gomail.NewDialer(m.SMTP.Server, m.SMTP.Port, m.SMTP.Username, m.SMTP.Password) 75 | return dialer.DialAndSend(message) 76 | } 77 | 78 | func renderMessage(issues []types.Result) string { 79 | body := []string{"Checkup has detected the following issues:", "

", "
    "} 80 | for _, issue := range issues { 81 | format := "
  • %s - Status %s
  • " 82 | body = append(body, fmt.Sprintf(format, issue.Title, issue.Status())) 83 | } 84 | body = append(body, "
") 85 | return strings.Join(body, "\n") 86 | } 87 | -------------------------------------------------------------------------------- /storage/mysql/mysql_test.go: -------------------------------------------------------------------------------- 1 | // +build mysql 2 | 3 | package mysql 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/sourcegraph/checkup/types" 10 | ) 11 | 12 | func TestSQL(t *testing.T) { 13 | results := []types.Result{{Title: "Testing"}} 14 | 15 | specimen := Storage{ 16 | DSN: "test:test@tcp(mysql-test-db:3306)/test", 17 | Create: true, 18 | } 19 | 20 | if err := specimen.Store(results); err != nil { 21 | t.Fatalf("Expected no error from Store(), got: %v", err) 22 | } 23 | 24 | // Test GetIndex (StorageReader interface) 25 | index, err := specimen.GetIndex() 26 | if err != nil { 27 | t.Fatalf("StoreReader: cannot read index: %v", err) 28 | } 29 | 30 | if len(index) != 1 { 31 | t.Fatalf("Expected length of index to be 1, but got %v", len(index)) 32 | } 33 | 34 | var ( 35 | name string 36 | nsec int64 37 | ) 38 | for name, nsec = range index { 39 | } 40 | 41 | // Make sure index has timestamp of check 42 | ts := time.Unix(0, nsec) 43 | if time.Since(ts) > 1*time.Second { 44 | t.Errorf("Timestamp of check is %s but expected something very recent", ts) 45 | } 46 | 47 | // Make sure stored data are correct 48 | testResults, err := specimen.Fetch(name) 49 | if err != nil { 50 | t.Fatalf("Could not fetch data, got: %v", err) 51 | } 52 | if len(testResults) != 1 { 53 | t.Fatalf("StoreReader: expected length of []Result to be 1, but got %v", len(testResults)) 54 | } 55 | 56 | if testResults[0].Title != results[0].Title { 57 | t.Fatalf("Expected test result title to be '%s', but got '%s'", results[0].Title, testResults[0].Title) 58 | } 59 | 60 | // Make sure the check is not deleted after maintain with CheckExpiry == 0 61 | if err := specimen.Maintain(); err != nil { 62 | t.Fatalf("Expected no error, got %v", err) 63 | } 64 | if _, err := specimen.Fetch(name); err != nil { 65 | t.Fatalf("Expected the check to be present in the DB, got: %v", err) 66 | } 67 | 68 | // Make sure the check is not deleted after maintain with CheckExpiry == 1 day 69 | specimen.CheckExpiry = 24 * time.Hour 70 | if err := specimen.Maintain(); err != nil { 71 | t.Fatalf("Expected no error, got %v", err) 72 | } 73 | if _, err := specimen.Fetch(name); err != nil { 74 | t.Fatalf("Expected the check to be present in the DB, got: %v", err) 75 | } 76 | 77 | // Make sure the check is deleted after maintain with CheckExpiry > 0 78 | specimen.CheckExpiry = 1 * time.Nanosecond 79 | if err := specimen.Maintain(); err != nil { 80 | t.Fatalf("Expected no error, got %v", err) 81 | } 82 | if _, err := specimen.Fetch(name); err == nil { 83 | t.Fatalf("Expected not to be able to fetch the result from the DB") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /storage/postgres/postgres_test.go: -------------------------------------------------------------------------------- 1 | // +build postgres 2 | 3 | package postgres 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/sourcegraph/checkup/types" 10 | ) 11 | 12 | func TestSQL(t *testing.T) { 13 | results := []types.Result{{Title: "Testing"}} 14 | 15 | specimen := Storage{ 16 | DSN: "host=postgres-test-db user=test password=test dbname=test sslmode=disable", 17 | Create: true, 18 | } 19 | 20 | if err := specimen.Store(results); err != nil { 21 | t.Fatalf("Expected no error from Store(), got: %v", err) 22 | } 23 | 24 | // Test GetIndex (StorageReader interface) 25 | index, err := specimen.GetIndex() 26 | if err != nil { 27 | t.Fatalf("StoreReader: cannot read index: %v", err) 28 | } 29 | 30 | if len(index) != 1 { 31 | t.Fatalf("Expected length of index to be 1, but got %v", len(index)) 32 | } 33 | 34 | var ( 35 | name string 36 | nsec int64 37 | ) 38 | for name, nsec = range index { 39 | } 40 | 41 | // Make sure index has timestamp of check 42 | ts := time.Unix(0, nsec) 43 | if time.Since(ts) > 1*time.Second { 44 | t.Errorf("Timestamp of check is %s but expected something very recent", ts) 45 | } 46 | 47 | // Make sure stored data are correct 48 | testResults, err := specimen.Fetch(name) 49 | if err != nil { 50 | t.Fatalf("Could not fetch data, got: %v", err) 51 | } 52 | if len(testResults) != 1 { 53 | t.Fatalf("StoreReader: expected length of []Result to be 1, but got %v", len(testResults)) 54 | } 55 | 56 | if testResults[0].Title != results[0].Title { 57 | t.Fatalf("Expected test result title to be '%s', but got '%s'", results[0].Title, testResults[0].Title) 58 | } 59 | 60 | // Make sure the check is not deleted after maintain with CheckExpiry == 0 61 | if err := specimen.Maintain(); err != nil { 62 | t.Fatalf("Expected no error, got %v", err) 63 | } 64 | if _, err := specimen.Fetch(name); err != nil { 65 | t.Fatalf("Expected the check to be present in the DB, got: %v", err) 66 | } 67 | 68 | // Make sure the check is not deleted after maintain with CheckExpiry == 1 day 69 | specimen.CheckExpiry = 24 * time.Hour 70 | if err := specimen.Maintain(); err != nil { 71 | t.Fatalf("Expected no error, got %v", err) 72 | } 73 | if _, err := specimen.Fetch(name); err != nil { 74 | t.Fatalf("Expected the check to be present in the DB, got: %v", err) 75 | } 76 | 77 | // Make sure the check is deleted after maintain with CheckExpiry > 0 78 | specimen.CheckExpiry = 1 * time.Nanosecond 79 | if err := specimen.Maintain(); err != nil { 80 | t.Fatalf("Expected no error, got %v", err) 81 | } 82 | if _, err := specimen.Fetch(name); err == nil { 83 | t.Fatalf("Expected not to be able to fetch the result from the DB") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /storage/sqlite3/sqlite_test.go: -------------------------------------------------------------------------------- 1 | // +build sqlite3 2 | 3 | package sqlite3 4 | 5 | import ( 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | "time" 11 | 12 | "github.com/sourcegraph/checkup/types" 13 | ) 14 | 15 | func TestSQL(t *testing.T) { 16 | results := []types.Result{{Title: "Testing"}} 17 | 18 | // Create temporary directory for the tests 19 | dir, err := ioutil.TempDir("", "checkup") 20 | if err != nil { 21 | t.Fatalf("Cannot create temporary directory: %v", err) 22 | } 23 | defer os.RemoveAll(dir) 24 | 25 | dbFile := filepath.Join(dir, "checkuptest.db") 26 | 27 | specimen := Storage{ 28 | DSN: dbFile, 29 | Create: true, 30 | } 31 | 32 | if err := specimen.Store(results); err != nil { 33 | t.Fatalf("Expected no error from Store(), got: %v", err) 34 | } 35 | 36 | // Test GetIndex (StorageReader interface) 37 | index, err := specimen.GetIndex() 38 | if err != nil { 39 | t.Fatalf("StoreReader: cannot read index: %v", err) 40 | } 41 | 42 | if len(index) != 1 { 43 | t.Fatalf("Expected length of index to be 1, but got %v", len(index)) 44 | } 45 | 46 | var ( 47 | name string 48 | nsec int64 49 | ) 50 | for name, nsec = range index { 51 | } 52 | 53 | // Make sure index has timestamp of check 54 | ts := time.Unix(0, nsec) 55 | if time.Since(ts) > 1*time.Second { 56 | t.Errorf("Timestamp of check is %s but expected something very recent", ts) 57 | } 58 | 59 | // Make sure stored data are correct 60 | testResults, err := specimen.Fetch(name) 61 | if err != nil { 62 | t.Fatalf("Could not fetch data, got: %v", err) 63 | } 64 | if len(testResults) != 1 { 65 | t.Fatalf("StoreReader: expected length of []Result to be 1, but got %v", len(testResults)) 66 | } 67 | 68 | if testResults[0].Title != results[0].Title { 69 | t.Fatalf("Expected test result title to be '%s', but got '%s'", results[0].Title, testResults[0].Title) 70 | } 71 | 72 | // Make sure the check is not deleted after maintain with CheckExpiry == 0 73 | if err := specimen.Maintain(); err != nil { 74 | t.Fatalf("Expected no error, got %v", err) 75 | } 76 | if _, err := specimen.Fetch(name); err != nil { 77 | t.Fatalf("Expected the check to be present in the DB, got: %v", err) 78 | } 79 | 80 | // Make sure the check is not deleted after maintain with CheckExpiry == 1 day 81 | specimen.CheckExpiry = 24 * time.Hour 82 | if err := specimen.Maintain(); err != nil { 83 | t.Fatalf("Expected no error, got %v", err) 84 | } 85 | if _, err := specimen.Fetch(name); err != nil { 86 | t.Fatalf("Expected the check to be present in the DB, got: %v", err) 87 | } 88 | 89 | // Make sure the check is deleted after maintain with CheckExpiry > 0 90 | specimen.CheckExpiry = 1 * time.Nanosecond 91 | if err := specimen.Maintain(); err != nil { 92 | t.Fatalf("Expected no error, got %v", err) 93 | } 94 | if _, err := specimen.Fetch(name); err == nil { 95 | t.Fatalf("Expected not to be able to fetch the result from the DB") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /notifier/discord/discord.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "strings" 12 | "time" 13 | 14 | "github.com/sourcegraph/checkup/types" 15 | ) 16 | 17 | // New creates a new Notifier instance based on json config 18 | func New(config json.RawMessage) (Notifier, error) { 19 | var notifier Notifier 20 | err := json.Unmarshal(config, ¬ifier) 21 | return notifier, err 22 | } 23 | 24 | // Type returns the notifier package name 25 | func (Notifier) Type() string { 26 | return Type 27 | } 28 | 29 | // Notify implements notifier interface 30 | func (s Notifier) Notify(results []types.Result) error { 31 | errs := make(types.Errors, 0) 32 | for _, result := range results { 33 | if !result.Healthy { 34 | if err := s.Send(result); err != nil { 35 | errs = append(errs, err) 36 | } 37 | } 38 | } 39 | if len(errs) == 0 { 40 | return nil 41 | } 42 | return errs 43 | } 44 | 45 | // Send request via Slack API to create incident 46 | func (s Notifier) Send(result types.Result) error { 47 | status := strings.ToUpper(fmt.Sprint(result.Status())) 48 | 49 | attach := &Payload{} 50 | attach.Title = "Checkup" 51 | embed := &Embed{ 52 | Color: 0xc21408, 53 | } 54 | embed.AddField(&Field{ 55 | Name: "Name", 56 | Value: result.Title, 57 | Inline: true, 58 | }) 59 | embed.AddField(&Field{ 60 | Name: "Status", 61 | Value: fmt.Sprintf("**%s**", status), 62 | Inline: true, 63 | }) 64 | embed.AddField(&Field{ 65 | Name: "Endpoint", 66 | Value: result.Endpoint, 67 | Inline: true, 68 | }) 69 | attach.AddEmbed(embed) 70 | attach.Avatar = "https://placekitten.com/400/400" 71 | 72 | requestBody, err := json.Marshal(attach) 73 | 74 | fmt.Println(string(requestBody)) 75 | 76 | if err != nil { 77 | return fmt.Errorf("discord: error marshalling body: %w", err) 78 | } 79 | 80 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) 81 | 82 | // client 83 | client := &http.Client{} 84 | 85 | // request with timeout 86 | req, err := http.NewRequestWithContext(ctx, "POST", s.Webhook, bytes.NewBuffer(requestBody)) 87 | if err != nil { 88 | return fmt.Errorf("discord: error creating request: %w", err) 89 | } 90 | req.Header.Set("Content-Type", "application/json") 91 | 92 | // response 93 | resp, err := client.Do(req) 94 | if err != nil { 95 | return fmt.Errorf("discord: error issuing request: %w", err) 96 | } 97 | defer resp.Body.Close() 98 | 99 | if resp.StatusCode != http.StatusNoContent { 100 | bodyBytes, err := ioutil.ReadAll(resp.Body) 101 | if err != nil { 102 | log.Println("discord: error reading response body", err) 103 | } 104 | bodyString := string(bodyBytes) 105 | log.Println("discord: response body:", bodyString) 106 | return fmt.Errorf("discord: expected status 200, got %d", resp.StatusCode) 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/sourcegraph/checkup" 13 | "github.com/sourcegraph/checkup/storage/fs" 14 | ) 15 | 16 | var listenAddr string 17 | 18 | var serveCmd = &cobra.Command{ 19 | Use: "serve", 20 | Short: "Serve files from a local/remote storage service", 21 | Long: `Use the serve command to start a minimal http server that will serve 22 | files from the configured storage provider. The intended use is to 23 | provide a web server that will read any stored checks from storages like 24 | fs, mysql, postgresql, sqlite3.... 25 | 26 | By default, checkup.json configuration file will be loaded and used.`, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | var prov checkup.StorageReader 29 | var err error 30 | 31 | prov, err = storageReaderConfig() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | fmt.Println("OK...") 37 | 38 | statuspage := http.FileServer(http.Dir("./statuspage/")) 39 | 40 | mux := http.NewServeMux() 41 | for _, folder := range []string{"js", "css", "images"} { 42 | mux.Handle("/"+folder+"/", statuspage) 43 | } 44 | mux.HandleFunc("/", serveHandler(prov)) 45 | 46 | if err := http.ListenAndServe(listenAddr, mux); err != nil { 47 | log.Fatal(err) 48 | } 49 | }, 50 | } 51 | 52 | func serveHandler(reader checkup.StorageReader) http.HandlerFunc { 53 | writeError := func(w http.ResponseWriter, err error) { 54 | response := struct { 55 | Error struct { 56 | Message string 57 | } 58 | }{} 59 | response.Error.Message = err.Error() 60 | json.NewEncoder(w).Encode(response) 61 | } 62 | return func(w http.ResponseWriter, r *http.Request) { 63 | requestedFile := strings.TrimLeft(r.URL.Path, "/") 64 | if requestedFile == "" || requestedFile == "index.html" { 65 | http.ServeFile(w, r, "statuspage/index.html") 66 | } 67 | index, err := reader.GetIndex() 68 | if err != nil { 69 | writeError(w, err) 70 | return 71 | } 72 | if requestedFile == fs.IndexName { 73 | json.NewEncoder(w).Encode(index) 74 | return 75 | } 76 | if _, ok := index[requestedFile]; ok { 77 | file, err := reader.Fetch(requestedFile) 78 | if err != nil { 79 | writeError(w, err) 80 | return 81 | } 82 | json.NewEncoder(w).Encode(file) 83 | return 84 | } 85 | writeError(w, fmt.Errorf("file not found: %s", requestedFile)) 86 | } 87 | } 88 | 89 | func storageReaderConfig() (checkup.StorageReader, error) { 90 | c := loadCheckup() 91 | if c.Storage == nil { 92 | return nil, fmt.Errorf("no storage configuration found") 93 | } 94 | prov, ok := c.Storage.(checkup.StorageReader) 95 | if !ok { 96 | return nil, fmt.Errorf("configured storage type does not have reading capabilities") 97 | } 98 | return prov, nil 99 | } 100 | 101 | func init() { 102 | RootCmd.AddCommand(serveCmd) 103 | serveCmd.Flags().StringVarP(&listenAddr, "listen", "", ":3000", "The listen address for the HTTP server") 104 | } 105 | -------------------------------------------------------------------------------- /storage/sql/sql_test.go: -------------------------------------------------------------------------------- 1 | // +build sql 2 | 3 | package sql 4 | 5 | import ( 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | "time" 11 | 12 | "github.com/sourcegraph/checkup/types" 13 | ) 14 | 15 | func TestSQL(t *testing.T) { 16 | results := []types.Result{{Title: "Testing"}} 17 | 18 | // Create temporary directory for the tests 19 | dir, err := ioutil.TempDir("", "checkup") 20 | if err != nil { 21 | t.Fatalf("Cannot create temporary directory: %v", err) 22 | } 23 | defer os.RemoveAll(dir) 24 | 25 | dbFile := filepath.Join(dir, "checkuptest.db") 26 | 27 | specimen := Storage{ 28 | SqliteDBFile: dbFile, 29 | } 30 | 31 | if err := specimen.initialize(); err != nil { 32 | t.Fatalf("Could not initialize test database, got: %v", err) 33 | } 34 | 35 | if err := specimen.Store(results); err != nil { 36 | t.Fatalf("Expected no error from Store(), got: %v", err) 37 | } 38 | 39 | // Test GetIndex (StorageReader interface) 40 | index, err := specimen.GetIndex() 41 | if err != nil { 42 | t.Fatalf("StoreReader: cannot read index: %v", err) 43 | } 44 | 45 | if len(index) != 1 { 46 | t.Fatalf("Expected length of index to be 1, but got %v", len(index)) 47 | } 48 | 49 | var ( 50 | name string 51 | nsec int64 52 | ) 53 | for name, nsec = range index { 54 | } 55 | 56 | // Make sure index has timestamp of check 57 | ts := time.Unix(0, nsec) 58 | if time.Since(ts) > 1*time.Second { 59 | t.Errorf("Timestamp of check is %s but expected something very recent", ts) 60 | } 61 | 62 | // Make sure stored data are correct 63 | testResults, err := specimen.Fetch(name) 64 | if err != nil { 65 | t.Fatalf("Could not fetch data, got: %v", err) 66 | } 67 | if len(testResults) != 1 { 68 | t.Fatalf("StoreReader: expected length of []Result to be 1, but got %v", len(testResults)) 69 | } 70 | 71 | if testResults[0].Title != results[0].Title { 72 | t.Fatalf("Expected test result title to be '%s', but got '%s'", results[0].Title, testResults[0].Title) 73 | } 74 | 75 | // Make sure the check is not deleted after maintain with CheckExpiry == 0 76 | if err := specimen.Maintain(); err != nil { 77 | t.Fatalf("Expected no error, got %v", err) 78 | } 79 | if _, err := specimen.Fetch(name); err != nil { 80 | t.Fatalf("Expected the check to be present in the DB, got: %v", err) 81 | } 82 | 83 | // Make sure the check is not deleted after maintain with CheckExpiry == 1 day 84 | specimen.CheckExpiry = 24 * time.Hour 85 | if err := specimen.Maintain(); err != nil { 86 | t.Fatalf("Expected no error, got %v", err) 87 | } 88 | if _, err := specimen.Fetch(name); err != nil { 89 | t.Fatalf("Expected the check to be present in the DB, got: %v", err) 90 | } 91 | 92 | // Make sure the check is deleted after maintain with CheckExpiry > 0 93 | specimen.CheckExpiry = 1 * time.Nanosecond 94 | if err := specimen.Maintain(); err != nil { 95 | t.Fatalf("Expected no error, got %v", err) 96 | } 97 | if _, err := specimen.Fetch(name); err == nil { 98 | t.Fatalf("Expected not to be able to fetch the result from the DB") 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cmd/provision.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/sourcegraph/checkup" 12 | "github.com/sourcegraph/checkup/storage/s3" 13 | ) 14 | 15 | var provisionCmd = &cobra.Command{ 16 | Use: "provision", 17 | Short: "Provision a storage service", 18 | Long: `Use the provision command to provision storage for your 19 | check files on any supported provider. Provisioning need 20 | only be done once per status page. After provisioning, 21 | you will be provided some credentials; use those to 22 | configure your status page and/or checker. 23 | 24 | By default, checkup.json will be loaded and used, if it 25 | exists in the current working directory. Otherwise, you 26 | may provision your storage manually according to the 27 | instructions below. 28 | 29 | To do it manually, run 'checkup provision ' 30 | with your provider of choice after setting the required 31 | environment variables. 32 | 33 | PROVIDERS 34 | 35 | s3 36 | Create an IAM user with at least these two permissions: 37 | 38 | - arn:aws:iam::aws:policy/IAMFullAccess 39 | - arn:aws:iam::aws:policy/AmazonS3FullAccess 40 | 41 | Then set these env variables: 42 | 43 | - AWS_ACCESS_KEY_ID= 44 | - AWS_SECRET_ACCESS_KEY= 45 | - AWS_BUCKET_NAME=`, 46 | Run: func(cmd *cobra.Command, args []string) { 47 | var prov checkup.Provisioner 48 | var err error 49 | 50 | switch len(args) { 51 | case 0: 52 | prov, err = provisionerConfig() 53 | case 1: 54 | prov, err = provisionerEnvVars(cmd, args) 55 | default: 56 | fmt.Println(cmd.Long) 57 | os.Exit(1) 58 | } 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | fmt.Println("One sec...") 64 | 65 | info, err := prov.Provision() 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | fmt.Println(info) 71 | }, 72 | } 73 | 74 | func provisionerConfig() (checkup.Provisioner, error) { 75 | c := loadCheckup() 76 | if c.Storage == nil { 77 | return nil, fmt.Errorf("no storage configuration found") 78 | } 79 | prov, ok := c.Storage.(checkup.Provisioner) 80 | if !ok { 81 | return nil, fmt.Errorf("configured storage type does not have provisioning capabilities") 82 | } 83 | return prov, nil 84 | } 85 | 86 | func provisionerEnvVars(cmd *cobra.Command, args []string) (checkup.Provisioner, error) { 87 | providerName := strings.ToLower(args[0]) 88 | switch providerName { 89 | case "s3": 90 | keyID := os.Getenv("AWS_ACCESS_KEY_ID") 91 | secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") 92 | bucket := os.Getenv("AWS_BUCKET_NAME") 93 | if keyID == "" || secretKey == "" || bucket == "" { 94 | fmt.Println(cmd.Long) 95 | os.Exit(1) 96 | } 97 | return s3.Storage{ 98 | AccessKeyID: keyID, 99 | SecretAccessKey: secretKey, 100 | Bucket: bucket, 101 | }, nil 102 | default: 103 | return nil, fmt.Errorf("unknown storage provider '%s'", providerName) 104 | } 105 | } 106 | 107 | func init() { 108 | RootCmd.AddCommand(provisionCmd) 109 | } 110 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | This Sourcegraph project is [MIT licensed](LICENSE) and accepts 4 | contributions via GitHub pull requests. This document outlines some of 5 | the conventions on development workflow, commit message formatting, 6 | contact points and other resources to make it easier to get your 7 | contribution accepted. 8 | 9 | # Certificate of Origin 10 | 11 | By contributing to this project you agree to the [Developer Certificate of Origin 12 | (DCO)](https://developercertificate.org/). This document was created by the Linux Kernel community 13 | and is a simple statement that you, as a contributor, have the legal right to make the 14 | contribution. See the [DCO](DCO) file for details. 15 | 16 | ## Getting Started 17 | 18 | You'll need Go 1.10 or newer installed. 19 | 20 | 1. [Fork this repo](https://github.com/sourcegraph/checkup). This makes a copy of the code you can write to. 21 | 2. If you don't already have this repo (sourcegraph/checkup.git) repo on your computer, get it with `go get github.com/sourcegraph/checkup/cmd/checkup`. 22 | 3. Tell git that it can push the sourcegraph/checkup.git repo to your fork by adding a remote: `git remote add myfork https://github.com/you/checkup.git` 23 | 4. Make your changes in the sourcegraph/checkup.git repo on your computer. 24 | 5. Push your changes to your fork: `git push myfork` 25 | 6. [Create a pull request](https://github.com/sourcegraph/checkup/pull/new/master) to merge your changes into sourcegraph/checkup @ master. (Click "compare across forks" and change the head fork.) 26 | 27 | You can test your changes with `go run main.go` or `go build` if you want a binary plopped on disk. Use `go test -race ./...` from the root of the repo to run tests and make sure they pass! 28 | 29 | 30 | ## Contribution Flow 31 | 32 | This is a rough outline of what a contributor's workflow looks like: 33 | 34 | - Create a topic branch from where you want to base your work (usually master). 35 | - Make commits of logical units. 36 | - Make sure your commit messages are in the proper format (see below). 37 | - Push your changes to a topic branch in your fork of the repository. 38 | - Make sure the tests pass, and add any new tests as appropriate. 39 | - Submit a pull request to the original repository. 40 | 41 | Thanks for your contributions! 42 | 43 | ### Format of the Commit Message 44 | 45 | We follow a rough convention for commit messages that is designed to answer two 46 | questions: what changed and why. The subject line should feature the what and 47 | the body of the commit should describe the why. 48 | 49 | ``` 50 | scripts: add the test-cluster command 51 | 52 | this uses tmux to setup a test cluster that you can easily kill and 53 | start for debugging. 54 | 55 | Fixes #38 56 | ``` 57 | 58 | The format can be described more formally as follows: 59 | 60 | ``` 61 | : 62 | 63 | 64 | 65 |