diff --git a/arc/cmd/status.go b/arc/cmd/status.go index a0382ef..ffea234 100644 --- a/arc/cmd/status.go +++ b/arc/cmd/status.go @@ -2,148 +2,41 @@ package cmd import ( "fmt" - "time" + "github.com/hubci/arc/arc/statuses" "github.com/spf13/cobra" ) -type spPage struct { - ID string `json:"id"` - Name string `json:"name"` - URL string `url:"url"` - TimeZone string `json:"time_zone"` - UpdatedAt time.Time `json:"updated_at"` -} - -type spStatus struct { - Indicator string `json:"indicator"` - Description string `json:"description"` -} - -// StatusPage.io Response -type spResponse struct { - Page *spPage `json:"page"` - Status *spStatus `json:"status"` -} - -type sStatus struct { - Updated time.Time `json:"updated"` - Status string `json:"status"` - StatusCode int `json:"status_code"` -} - -type sResult struct { - StatusOverall *sStatus `json:"status_overall"` -} - -// Status.io Response -type sResponse struct { - Result *sResult `json:"result"` -} - var ( cciFl bool statusCmd = &cobra.Command{ - Use: "status", - Short: "Provides the status page results for various DevOps services", + Use: "status ", + Short: "Provides the status page result for the provided name", + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - var cciResp *spResponse - var cfResp *spResponse - var ghResp *spResponse - var gitlabResp *sResponse - var linodeResp *spResponse - var doResp *spResponse - var dockerResp *sResponse - - cciURL := "https://status.circleci.com/api/v2/status.json" - cfURL := "https://www.cloudflarestatus.com/api/v2/status.json" - ghURL := "https://www.githubstatus.com/api/v2/status.json" - gitlabURL := "https://status.gitlab.com/1.0/status/5b36dc6502d06804c08349f7" - linodeURL := "https://status.linode.com/api/v2/status.json" - doURL := "https://status.digitalocean.com/api/v2/status.json" - dockerURL := "https://status.docker.com/1.0/status/533c6539221ae15e3f000031" - - client := New() - - errCCI := client.getJSON(cciURL, &cciResp) - errCF := client.getJSON(cfURL, &cfResp) - errGH := client.getJSON(ghURL, &ghResp) - errGitlab := client.getJSON(gitlabURL, &gitlabResp) - errLinode := client.getJSON(linodeURL, &linodeResp) - errDO := client.getJSON(doURL, &doResp) - errDocker := client.getJSON(dockerURL, &dockerResp) - - if errCCI != nil { - cciResp.Status.Indicator = "can't connect" - } - - if errCF != nil { - cfResp.Status.Indicator = "can't connect" - } - - if errGH != nil { - ghResp.Status.Indicator = "can't connect" - } - - if errGitlab != nil { - gitlabResp.Result.StatusOverall.Status = "can't connect" - } - - if errLinode != nil { - linodeResp.Status.Indicator = "can't connect" - } - - if errDO != nil { - doResp.Status.Indicator = "can't connect" + statusPage, err := statuses.Page(args[0]) + if err != nil { + fmt.Println(err) + return } - if errDocker != nil { - dockerResp.Result.StatusOverall.Status = "can't connect" - } - - var cciTabs = "" - if cciResp.Status.Indicator == "none" { - cciResp.Status.Indicator = "" - cciTabs = "\t" - } - - var cfTabs = "" - if cfResp.Status.Indicator == "none" { - cfResp.Status.Indicator = "" - cfTabs = "\t" - } - - var ghTabs = "" - if ghResp.Status.Indicator == "none" { - ghResp.Status.Indicator = "" - ghTabs = "\t" - } + client := New() - var linodeTabs = "" - if linodeResp.Status.Indicator == "none" { - linodeResp.Status.Indicator = "" - linodeTabs = "\t" + err = statusPage.Fetch(client.c) + if err != nil { + fmt.Println(err) + return } - var doTabs = "" - if doResp.Status.Indicator == "none" { - doResp.Status.Indicator = "" - doTabs = "\t" + status, err := statusPage.Status() + if err != nil { + fmt.Println(err) + return } - fmt.Println("Reporting status page results...") - fmt.Println("") - fmt.Printf("CircleCI:\t%s%s\t%s\n", cciResp.Status.Indicator, cciTabs, cciResp.Status.Description) - fmt.Printf("GitHub:\t\t%s%s\t%s\n", ghResp.Status.Indicator, ghTabs, ghResp.Status.Description) - fmt.Printf("Gitlab:\t\t\t\t%s\n", gitlabResp.Result.StatusOverall.Status) - fmt.Printf("Docker:\t\t\t\t%s\n", dockerResp.Result.StatusOverall.Status) - if !cciFl { - fmt.Printf("Cloudflare:\t\t%s%s\t%s\n", cfResp.Status.Indicator, cfTabs, cfResp.Status.Description) - fmt.Printf("Linode:\t\t%s%s\t%s\n", linodeResp.Status.Indicator, linodeTabs, linodeResp.Status.Description) - fmt.Printf("DigitalOcean:\t%s%s\t%s\n", doResp.Status.Indicator, doTabs, doResp.Status.Description) - } + fmt.Println(status) }, } ) diff --git a/arc/statuses/circleci.go b/arc/statuses/circleci.go new file mode 100644 index 0000000..7fa1fbb --- /dev/null +++ b/arc/statuses/circleci.go @@ -0,0 +1,8 @@ +package statuses + +/* + * Register the CircleCI status page. + */ +func init() { + RegisterStatusPageIOPage("circleci", "https://status.circleci.com/api/v2/status.json") +} diff --git a/arc/statuses/cloudflare.go b/arc/statuses/cloudflare.go new file mode 100644 index 0000000..e6cd3d9 --- /dev/null +++ b/arc/statuses/cloudflare.go @@ -0,0 +1,8 @@ +package statuses + +/* + * Register the CircleCI status page. + */ +func init() { + RegisterStatusPageIOPage("cloudflare", "https://www.cloudflarestatus.com/api/v2/status.json") +} diff --git a/arc/statuses/digitalocean.go b/arc/statuses/digitalocean.go new file mode 100644 index 0000000..671f7ad --- /dev/null +++ b/arc/statuses/digitalocean.go @@ -0,0 +1,8 @@ +package statuses + +/* + * Register the CircleCI status page. + */ +func init() { + RegisterStatusPageIOPage("digitalocean", "https://status.digitalocean.com/api/v2/status.json") +} diff --git a/arc/statuses/docker.go b/arc/statuses/docker.go new file mode 100644 index 0000000..ea0455b --- /dev/null +++ b/arc/statuses/docker.go @@ -0,0 +1,8 @@ +package statuses + +/* + * Register the Docker (company) status page. + */ +func init() { + RegisterStatusIOPage("docker", "https://www.dockerstatus.com/1.0/status/533c6539221ae15e3f000031") +} diff --git a/arc/statuses/errors.go b/arc/statuses/errors.go new file mode 100644 index 0000000..a1899e4 --- /dev/null +++ b/arc/statuses/errors.go @@ -0,0 +1,25 @@ +package statuses + +import "fmt" + +/* + * NameTakenError occurs when a status page is registered with a name that is + * already registered. + */ +type NameTakenError struct { + Name string +} + +/* + * Error returns the string representation of NameTakenError. + */ +func (e *NameTakenError) Error() string { + return fmt.Sprintf("Failed to register the name %s. It has already been taken.", e.Name) +} + +/* + * newNameTakenError creates a new NameTakenError. + */ +func newNameTakenError(name string) *NameTakenError { + return &NameTakenError{Name: name} +} diff --git a/arc/statuses/github.go b/arc/statuses/github.go new file mode 100644 index 0000000..419da16 --- /dev/null +++ b/arc/statuses/github.go @@ -0,0 +1,8 @@ +package statuses + +/* + * Register the CircleCI status page. + */ +func init() { + RegisterStatusPageIOPage("github", "https://www.githubstatus.com/api/v2/status.json") +} diff --git a/arc/statuses/gitlab.go b/arc/statuses/gitlab.go new file mode 100644 index 0000000..482f685 --- /dev/null +++ b/arc/statuses/gitlab.go @@ -0,0 +1,8 @@ +package statuses + +/* + * Register the Docker (company) status page. + */ +func init() { + RegisterStatusIOPage("gitlab", "https://status.gitlab.com/1.0/status/5b36dc6502d06804c08349f7") +} diff --git a/arc/statuses/linode.go b/arc/statuses/linode.go new file mode 100644 index 0000000..c291261 --- /dev/null +++ b/arc/statuses/linode.go @@ -0,0 +1,8 @@ +package statuses + +/* + * Register the CircleCI status page. + */ +func init() { + RegisterStatusPageIOPage("linode", "https://status.linode.com/api/v2/status.json") +} diff --git a/arc/statuses/status.go b/arc/statuses/status.go new file mode 100644 index 0000000..acfe772 --- /dev/null +++ b/arc/statuses/status.go @@ -0,0 +1,48 @@ +package statuses + +import ( + "fmt" + "net/url" +) + +// This is how the package keeps track of all the status pages that it knows +// about. +var registeredPages map[string]StatusPage = map[string]StatusPage{} + +/* + * GetPageURL returns a status page's API URL. + */ +func Page(name string) (StatusPage, error) { + + sp, ok := registeredPages[name] + if !ok { + return nil, fmt.Errorf("Not a registered page.") + } + + return sp, nil +} + +/* + * registerPage adds a new status page to the list for a specific company / + * provider. + * + * name - should be a unique slug + */ +func registerPage(provider StatusPage) error { + + name := provider.Name() + + // If the name has already been used. + if _, ok := registeredPages[name]; ok { + return newNameTakenError(name) + } + + // If the URL provided is no good. + if _, err := url.Parse(provider.URL()); err != nil { + return err + } + + registeredPages[name] = provider + + return nil +} diff --git a/arc/statuses/status_io.go b/arc/statuses/status_io.go new file mode 100644 index 0000000..1c76c08 --- /dev/null +++ b/arc/statuses/status_io.go @@ -0,0 +1,75 @@ +package statuses + +import ( + "fmt" + "net/http" + "time" +) + +/* + * This file defines the structs and other components needed to represent a + * status page hosted by Status.io. + */ + +/* + * statusIOStatus represents the individual component status as well as most + * of the overall status. + */ +type StatusIOStatus struct { + Status string `json:"status"` + StatusCode int `json:"status_code"` +} + +/* + * statusIOReponse represents the JSON response from the StatusPage.io API. + */ +type statusIOResponse struct { + Result struct { + StatusOverall struct { + Updated time.Time `json:"updated"` + *StatusIOStatus + } `json:"status_overall"` + } `json:"result"` +} + +/* + * statusIOProvider represents the way to connect to the API and retrieve a + * response. + */ +type statusIOProvider struct { + name string + apiURL string + data *statusIOResponse +} + +/* + * Fetch pulls in a response from the API. + */ +func (p *statusIOProvider) Fetch(c *http.Client) error { + + return fetchJSON(c, p.apiURL, &p.data) +} + +func (p *statusIOProvider) Name() string { + return p.name +} + +func (p *statusIOProvider) URL() string { + return p.apiURL +} + +func (p *statusIOProvider) Status() (string, error) { + + if p.data == nil { + return "", fmt.Errorf("Data hasn't been retrieved.") + } + + return p.data.Result.StatusOverall.Status, nil +} + +/* + * RegisterStatusIOPage creates a new provider. + */ +func RegisterStatusIOPage(name, apiURL string) error { + return registerPage(&statusIOProvider{name: name, apiURL: apiURL}) +} diff --git a/arc/statuses/status_page.go b/arc/statuses/status_page.go new file mode 100644 index 0000000..5438100 --- /dev/null +++ b/arc/statuses/status_page.go @@ -0,0 +1,40 @@ +package statuses + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +type StatusPage interface { + Fetch(*http.Client) error + Name() string + URL() string + Status() (string, error) +} + +func fetchJSON(c *http.Client, url string, payload interface{}) error { + + resp, err := c.Get(url) + if err != nil { + return fmt.Errorf("Failed to GET URL. Err: %s", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Error: API response was not HTTP 200.") + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("Failed to read GET response. Err: %s", err) + } + + err = json.Unmarshal(body, &payload) + if err != nil { + return fmt.Errorf("Failed to unmarshal GET response. Err: %s", err) + } + + return nil +} diff --git a/arc/statuses/statuspage_io.go b/arc/statuses/statuspage_io.go new file mode 100644 index 0000000..15afd33 --- /dev/null +++ b/arc/statuses/statuspage_io.go @@ -0,0 +1,72 @@ +package statuses + +import ( + "fmt" + "net/http" + "time" +) + +/* + * This file defines the structs and other components needed to represent a + * status page hosted by StatusPage.io. + */ + +type statusPageIOPageField struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `url:"url"` + TimeZone string `json:"time_zone"` + UpdatedAt time.Time `json:"updated_at"` +} + +type statusPageIOStatus struct { + Indicator string `json:"indicator"` + Description string `json:"description"` +} + +type statusPageIOResponse struct { + Page *statusPageIOPageField `json:"page"` + Status *statusPageIOStatus `json:"status"` +} + +/* + * statusPageIOPage represents the way to connect to the API and retrieve a + * response. + */ +type statusPageIOPage struct { + name string + apiURL string + data *statusPageIOResponse +} + +/* + * Fetch pulls in a response from the API. + */ +func (p *statusPageIOPage) Fetch(c *http.Client) error { + + return fetchJSON(c, p.apiURL, &p.data) +} + +func (p *statusPageIOPage) Name() string { + return p.name +} + +func (p *statusPageIOPage) URL() string { + return p.apiURL +} + +func (p *statusPageIOPage) Status() (string, error) { + + if p.data == nil { + return "", fmt.Errorf("Data hasn't been retrieved.") + } + + return p.data.Status.Description, nil +} + +/* + * RegisterStatusPageIOPage creates a new provider. + */ +func RegisterStatusPageIOPage(name, apiURL string) error { + return registerPage(&statusPageIOPage{name: name, apiURL: apiURL}) +}