Skip to content

Commit

Permalink
Add skeleton for the whole app
Browse files Browse the repository at this point in the history
  • Loading branch information
pcarranza committed Sep 22, 2018
1 parent b1335aa commit 1873bf7
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 56 deletions.
22 changes: 22 additions & 0 deletions core/propaganda.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package core

// Announcement represents a message to be shouted out
type Announcement interface {
Title() string
Text() string
URL() string
ShouldAnnounce() bool
ProjectName() string
}

// Parser provides an interface that allows to identify if a request can be
// parsed, and then will extract the announcement if there is any.
type Parser interface {
Match(map[string][]string) bool
Parse([]byte) (Announcement, error)
}

// Announcer provides a simple interface to announce things
type Announcer interface {
Announce(Announcement)
}
1 change: 1 addition & 0 deletions github/github_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package github_test
72 changes: 56 additions & 16 deletions gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,76 @@ package gitlab
import (
"encoding/json"
"fmt"

"gitlab.com/yakshaving.art/propaganda/core"
)

// Parser implements the core.Parser type for GitLab merge webhooks
type Parser struct {
}

// Match indicates that the headers match with the kind of request
func (Parser) Match(headers map[string][]string) bool {
if _, ok := headers["X-Gitlab-Event"]; ok {
return true
}
return false
}

// Parse creates a new merge request object from the passed payload
func (Parser) Parse(payload []byte) (core.Announcement, error) {
var mr MergeRequest
if err := json.Unmarshal(payload, &mr); err != nil {
return mr, fmt.Errorf("could not parse json payload: %s", err)
}
if mr.Kind != "merge_request" {
return MergeRequest{}, fmt.Errorf("json payload is not a merge request but a %s", mr.Kind)
}
return mr, nil
}

// MergeRequest is the MR object
type MergeRequest struct {
Kind string `json:"object_kind"`
Project Project `json:"project"`
Attributes Attributes `json:"object_attributes"`
}

// Title implements Annoucement
func (m MergeRequest) Title() string {
return m.Attributes.Title
}

// Text implements Annoucement
func (m MergeRequest) Text() string {
return ""
}

// URL implements Annoucement
func (m MergeRequest) URL() string {
return m.Attributes.URL
}

// ShouldAnnounce implements Announcement
func (m MergeRequest) ShouldAnnounce() bool {
return m.Attributes.State == "merged"
}

// ProjectName implements Announcement
func (m MergeRequest) ProjectName() string {
return m.Project.PathWithNamespace
}

// Project is used to identify which project it is including the namespace
type Project struct {
PathWithNamespace string `json:"path_with_namespace"`
}

// Attributes represent things like state, title, url or action
type Attributes struct {
State string `json:"state"`
Title string `json:"title"`
URL string `json:"url"`
Action string `json:"action"`
}

// ParseMergeRequest creates a new merge request object from the passed payload
func ParseMergeRequest(payload []byte) (MergeRequest, error) {
var mr MergeRequest
if err := json.Unmarshal(payload, &mr); err != nil {
return mr, fmt.Errorf("could not parse json payload: %s", err)
}
if mr.Kind != "merge_request" {
return MergeRequest{}, fmt.Errorf("json payload is not a merge request but a %s", mr.Kind)
}
return mr, nil
State string `json:"state"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
Action string `json:"action"`
}
49 changes: 32 additions & 17 deletions gitlab/gitlab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
)

func TestParsingPayloads(t *testing.T) {
parser := gitlab.Parser{}
tt := []struct {
name string
jsonFilename string
expected gitlab.MergeRequest
name string
jsonFilename string
expected gitlab.MergeRequest
shouldAnnounce bool
}{
{
"MR Create",
Expand All @@ -23,12 +25,14 @@ func TestParsingPayloads(t *testing.T) {
PathWithNamespace: "pablo/testing-webhooks",
},
Attributes: gitlab.Attributes{
State: "opened",
Title: "[announce] Update README.md",
URL: "https://git.yakshaving.art/pablo/testing-webhooks/merge_requests/1",
Action: "open",
State: "opened",
Title: "[announce] Update README.md",
Description: "Something in the description",
URL: "https://git.yakshaving.art/pablo/testing-webhooks/merge_requests/1",
Action: "open",
},
},
false,
},
{
"MR Merged",
Expand All @@ -39,12 +43,14 @@ func TestParsingPayloads(t *testing.T) {
PathWithNamespace: "pablo/testing-webhooks",
},
Attributes: gitlab.Attributes{
State: "merged",
Title: "[announce] Update README.md",
URL: "https://git.yakshaving.art/pablo/testing-webhooks/merge_requests/1",
Action: "merge",
State: "merged",
Title: "[announce] Update README.md",
Description: "Something in the description",
URL: "https://git.yakshaving.art/pablo/testing-webhooks/merge_requests/1",
Action: "merge",
},
},
true,
},
{
"MR Closed without a merge",
Expand All @@ -55,12 +61,14 @@ func TestParsingPayloads(t *testing.T) {
PathWithNamespace: "pablo/testing-webhooks",
},
Attributes: gitlab.Attributes{
State: "closed",
Title: "Update README.md",
URL: "https://git.yakshaving.art/pablo/testing-webhooks/merge_requests/2",
Action: "close",
State: "closed",
Title: "Update README.md",
Description: "other description",
URL: "https://git.yakshaving.art/pablo/testing-webhooks/merge_requests/2",
Action: "close",
},
},
false,
},
}

Expand All @@ -71,10 +79,16 @@ func TestParsingPayloads(t *testing.T) {
a.Nilf(err, "could not read fixture file %s", tc.jsonFilename)
a.NotNilf(b, "content should not be nil")

mr, err := gitlab.ParseMergeRequest(b)
mr, err := parser.Parse(b)
a.NoErrorf(err, "could not unmarshal MR json")

a.EqualValuesf(tc.expected, mr, "parsed merge request is not as expected")

a.Equal(tc.expected.Title(), mr.Title())
a.Equal(tc.expected.Text(), mr.Text())
a.Equal(tc.expected.URL(), mr.URL())
a.Equal(tc.expected.ProjectName(), mr.ProjectName())
a.Equal(tc.shouldAnnounce, mr.ShouldAnnounce())
})
}
}
Expand All @@ -86,7 +100,8 @@ func TestInvalidPayloadErrs(t *testing.T) {
a.Nil(err, "could not read fixture file")
a.NotNilf(b, "content should not be nil")

mr, err := gitlab.ParseMergeRequest(b)
parser := gitlab.Parser{}
mr, err := parser.Parse(b)
a.Errorf(err, "json payload is not a merge request but a push")
a.Equalf(gitlab.MergeRequest{}, mr, "merge request should be empty")
}
39 changes: 16 additions & 23 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package main

import (
"flag"
"fmt"
"io/ioutil"
"net/http"

"gitlab.com/yakshaving.art/propaganda/core"
"gitlab.com/yakshaving.art/propaganda/gitlab"
"gitlab.com/yakshaving.art/propaganda/metrics"
"gitlab.com/yakshaving.art/propaganda/server"
"gitlab.com/yakshaving.art/propaganda/slack"

"github.com/onrik/logrus/filename"
"github.com/sirupsen/logrus"
Expand All @@ -15,27 +18,15 @@ func main() {

args := parseArgs()

http.HandleFunc("/", handle)

logrus.Infof("listening on %s", args.Address)
logrus.Fatal(http.ListenAndServe(args.Address, nil))
}

func handle(w http.ResponseWriter, r *http.Request) {
// This requires registering the webhooks using json format and only receive
// pull request events

body, err := ioutil.ReadAll(r.Body)
if err != nil {
logrus.Errorf("failed to read body: %s", err)
http.Error(w, fmt.Sprintf("bad request: %s", err), http.StatusBadRequest)
return
}
defer r.Body.Close()
metrics.Register(args.MetricsPath)

w.WriteHeader(http.StatusAccepted)
s := server.New(
slack.Announcer{},
[]core.Parser{
gitlab.Parser{},
})

logrus.Infof("received Webhook\nHeaders: %#v\nPayload: %s", r.Header, string(body))
logrus.Fatal(s.ListenAndServe(args.Address))
}

func setupLogger() {
Expand All @@ -48,13 +39,15 @@ func setupLogger() {

// Args represents the commandline arguments
type Args struct {
Address string
Address string
MetricsPath string
}

func parseArgs() Args {
var args Args

flag.StringVar(&args.Address, "address", ":9092", "listening address")
flag.StringVar(&args.MetricsPath, "metrics", "/metrics", "metrics path")
flag.Parse()

return args
Expand Down
76 changes: 76 additions & 0 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package metrics

import (
"github.com/prometheus/client_golang/prometheus"
"net/http"
"time"
)

var namespace = "propaganda"

// Metrics provided through prometheus
var (
bootTime = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "boot_time_seconds",
Help: "unix timestamp of when the service was started",
})

Up = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "up",
Help: "wether the service is up or not",
})
WebhooksReceived = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: "webhooks",
Name: "received_total",
Help: "total number of received webhooks",
})
WebhooksBytesRead = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: "webhooks",
Name: "bytes_read_total",
Help: "total number of incoming bytes",
})
WebhooksErrors = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: "webhooks",
Name: "errors_total",
Help: "total number of webhooks errors",
})
WebhooksInvalid = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: "webhooks",
Name: "invalid_total",
Help: "total number of invalid webhooks",
}, []string{"reason"})
WebhooksValid = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: "webhooks",
Name: "valid_total",
Help: "total number of valid webhooks",
}, []string{"project"})
AnnouncementSuccesses = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: "announcer",
Name: "success_total",
Help: "total number of announcement successes",
})
AnnouncementErrors = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: "announcer",
Name: "errors_total",
Help: "total number of announcement errors",
})
)

// Register registers all the metrics and sets the http handler
func Register(metricsPath string) {
bootTime.Set(float64(time.Now().Unix()))
Up.Set(0)

prometheus.MustRegister(bootTime)

http.Handle(metricsPath, prometheus.Handler())
}
Loading

0 comments on commit 1873bf7

Please sign in to comment.