118 lines
2.6 KiB
Go
118 lines
2.6 KiB
Go
|
package origin
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/cloudflare/golibs/lrucache"
|
||
|
)
|
||
|
|
||
|
// StatusPage.io API docs:
|
||
|
// https://www.cloudflarestatus.com/api/v2/#incidents-unresolved
|
||
|
const (
|
||
|
activeIncidentsURL = "https://yh6f0r4529hb.statuspage.io/api/v2/incidents/unresolved.json"
|
||
|
argoTunnelKeyword = "argo tunnel"
|
||
|
incidentDetailsPrefix = "https://www.cloudflarestatus.com/incidents/"
|
||
|
)
|
||
|
|
||
|
// IncidentLookup is an object that checks for active incidents in
|
||
|
// the Cloudflare infrastructure.
|
||
|
type IncidentLookup interface {
|
||
|
ActiveIncidents() []Incident
|
||
|
}
|
||
|
|
||
|
// NewIncidentLookup returns a new IncidentLookup instance that caches its
|
||
|
// results with a 1-minute TTL.
|
||
|
func NewIncidentLookup() IncidentLookup {
|
||
|
return newCachedIncidentLookup(fetchActiveIncidents)
|
||
|
}
|
||
|
|
||
|
type IncidentUpdate struct {
|
||
|
Body string
|
||
|
}
|
||
|
|
||
|
type Incident struct {
|
||
|
Name string
|
||
|
ID string `json:"id"`
|
||
|
Updates []IncidentUpdate `json:"incident_updates"`
|
||
|
}
|
||
|
|
||
|
type StatusPage struct {
|
||
|
Incidents []Incident
|
||
|
}
|
||
|
|
||
|
func (i Incident) URL() string {
|
||
|
return incidentDetailsPrefix + i.ID
|
||
|
}
|
||
|
|
||
|
func parseStatusPage(data []byte) (*StatusPage, error) {
|
||
|
var result StatusPage
|
||
|
err := json.Unmarshal(data, &result)
|
||
|
return &result, err
|
||
|
}
|
||
|
|
||
|
func isArgoTunnelIncident(i Incident) bool {
|
||
|
if strings.Contains(strings.ToLower(i.Name), argoTunnelKeyword) {
|
||
|
return true
|
||
|
}
|
||
|
for _, u := range i.Updates {
|
||
|
if strings.Contains(strings.ToLower(u.Body), argoTunnelKeyword) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func fetchActiveIncidents() (incidents []Incident) {
|
||
|
resp, err := http.Get(activeIncidentsURL)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
body, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
statusPage, err := parseStatusPage(body)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
for _, i := range statusPage.Incidents {
|
||
|
if isArgoTunnelIncident(i) {
|
||
|
incidents = append(incidents, i)
|
||
|
}
|
||
|
}
|
||
|
return incidents
|
||
|
}
|
||
|
|
||
|
type cachedIncidentLookup struct {
|
||
|
cache *lrucache.LRUCache
|
||
|
ttl time.Duration
|
||
|
uncachedLookup func() []Incident
|
||
|
}
|
||
|
|
||
|
func newCachedIncidentLookup(uncachedLookup func() []Incident) *cachedIncidentLookup {
|
||
|
return &cachedIncidentLookup{
|
||
|
cache: lrucache.NewLRUCache(1),
|
||
|
ttl: time.Minute,
|
||
|
uncachedLookup: uncachedLookup,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We only need one cache entry. Always use the empty string as its key.
|
||
|
const cacheKey = ""
|
||
|
|
||
|
func (c *cachedIncidentLookup) ActiveIncidents() []Incident {
|
||
|
if cached, ok := c.cache.GetNotStale(cacheKey); ok {
|
||
|
if incidents, ok := cached.([]Incident); ok {
|
||
|
return incidents
|
||
|
}
|
||
|
}
|
||
|
incidents := c.uncachedLookup()
|
||
|
c.cache.Set(cacheKey, incidents, time.Now().Add(c.ttl))
|
||
|
return incidents
|
||
|
}
|