Skip to content
This repository has been archived by the owner on Mar 23, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1 from segmentio/Fauzyy/init
Browse files Browse the repository at this point in the history
Initial commit
  • Loading branch information
Fauzyy authored Jun 3, 2020
2 parents f9f2233 + b35ff3c commit 0b52dbb
Show file tree
Hide file tree
Showing 12 changed files with 533 additions and 15 deletions.
28 changes: 28 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
version: 2
jobs:
test:
docker:
- image: circleci/golang:1.14
steps:
- checkout
- run:
name: Check modules are tidy
command: |
go mod tidy
go mod vendor
if [ "$(git status --porcelain)" != "" ]; then
echo "git tree is dirty after tidying and vendoring modules"
echo "ensure go.mod and go.sum are tidy"
git status
exit 1
fi
- run:
name: Test
command: |
make test
workflows:
version: 2
test:
jobs:
- test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vendor/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright © 2017 Segment

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
14 changes: 1 addition & 13 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
.PHONY: deps
deps:
@echo "Install Deps"

.PHONY: test
test:
@echo "Perform Tests"

.PHONY: build
build:
@echo "Build code"

.PHONY: publish
publish:
@echo "Build code"
go test -v ./...
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,44 @@
# opslevel-go
# opslevel-go [![CircleCI](https://circleci.com/gh/segmentio/opslevel-go.svg?style=shield)](https://circleci.com/gh/segmentio/opslevel-go) [![Go Report Card](https://goreportcard.com/badge/github.com/segmentio/opslevel-go)](https://goreportcard.com/report/github.com/segmentio/opslevel-go) [![GoDoc](https://godoc.org/github.com/segmentio/opslevel-go?status.svg)](https://godoc.org/github.com/segmentio/opslevel-go)

This is a blank template for opslevel-go
`opslevel-go` is a client library for the [OpsLevel](https://www.opslevel.com/) integrations API

To get started create a new client:

```
client := opslevel.NewClient()
```

## Deploys Integration

The Deploys Integration requires the following fields:

```
deployRequest := opslevel.DeployRequest{
Service: "my-service",
Description: "my-service was deployed",
Deployer: rest.Deployer{
Email: "[email protected]",
},
Environment: "env",
DeployedAt: time.Now(),
}
err := client.Deploy(deployRequest, "my-integration-uuid")
```

For a full list fields, see the docs.

## Checks Integration

The Deploys Integration requires the following fields:

```
checkRequest := CheckRequest{
Service: "my_service",
Check: "my_check",
Message: "Deployed service",
Status: "passed",
}
err := client.Check(checkRequest, "my-integration-uuid")
```

The `Message` field is optional and `Status` should be one of `passed` or `failed`.
36 changes: 36 additions & 0 deletions check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package rest

import (
"bytes"
"encoding/json"
"fmt"

"gopkg.in/go-playground/validator.v9"
)

// CheckRequest represents a structured request to the OpsLevel checks webhook endpoint
type CheckRequest struct {
Service string `validate:"required" json:"service"`
Check string `validate:"required" json:"check"`
Status string `validate:"required,oneof=passed failed" json:"status"`
Message string `json:"message"`
}

// Check sends a CheckRequest to the OpsLevel deploy integration at integrationID
func (c *Client) Check(req CheckRequest, integrationID string) error {
v := validator.New()
if err := v.Struct(req); err != nil {
return err
}

b, err := json.Marshal(req)
if err != nil {
return err
}
var resp struct {
Result string `json:"result"`
}

fullURL := fmt.Sprintf("/integrations/check/%s", integrationID)
return c.do("POST", fullURL, bytes.NewReader(b), &resp)
}
98 changes: 98 additions & 0 deletions check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package rest

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestCheck(t *testing.T) {
t.Run("Doesn't return an error for a valid request", func(t *testing.T) {
checkRequest := CheckRequest{
Service: "my_service",
Check: "my_check",
Message: "Deployed service",
Status: "passed",
}

body := `
{
"result": "ok"
}
`
client, testServer := setupTest(202, body)
defer func() { testServer.Close() }()

err := client.Check(checkRequest, "uuid")
assert.NoError(t, err)
})

t.Run("Returns a Bad Request error on a 422", func(t *testing.T) {
checkRequest := CheckRequest{
Service: "my_service",
Check: "my_check",
Message: "Deployed service",
Status: "passed",
}

body := `
{
"errors":[
{"status":400,"title":"Check Error","detail":"param is missing or the value is empty: status"}
]
}
`
client, testServer := setupTest(400, body)
defer func() { testServer.Close() }()

err := client.Check(checkRequest, "uuid")
assert.Error(t, err)
assert.Contains(t, err.Error(), "status 400")
})

t.Run("Returns a Something Went Wrong error on all other status codes", func(t *testing.T) {
checkRequest := CheckRequest{
Service: "my_service",
Check: "my_check",
Message: "Deployed service",
Status: "passed",
}

body := `
{
"errors":[
{"status":503,"title":"Service Unavailable"}
]
}
`
client, testServer := setupTest(503, body)
defer func() { testServer.Close() }()

err := client.Check(checkRequest, "uuid")
assert.Error(t, err)
assert.Contains(t, err.Error(), "status 503")
})

t.Run("Returns an error for an invalid request", func(t *testing.T) {
checkRequest := CheckRequest{
Service: "my_service",
Check: "my_check",
Message: "Deployed service",
Status: "bad status",
}

body := `
{
"result": "ok"
}
`
client, testServer := setupTest(202, body)
defer func() { testServer.Close() }()

err := client.Check(checkRequest, "uuid")
assert.EqualError(t,
err,
"Key: 'CheckRequest.Status' Error:Field validation for 'Status' failed on the 'oneof' tag",
)
})
}
73 changes: 73 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package rest

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"

log "github.com/sirupsen/logrus"
)

var (
// ErrServiceUnavailable represents a 422
ErrServiceUnavailable = errors.New("Service Not Found")
// ErrBadRequest represents a 400
ErrBadRequest = errors.New("Bad Request")
// ErrSWW represents all other http errors
ErrSWW = errors.New("Something Went Wrong")
)

// Client represents a rest http client and is used to send requests to OpsLevel integrations
type Client struct {
baseURL *url.URL
httpClient *http.Client
Logger *log.Logger
}

// NewClient returns a Client pointer
func NewClient() *Client {
baseURL, _ := url.Parse("https://app.opslevel.com")
return &Client{
baseURL: baseURL,
httpClient: &http.Client{},
Logger: log.StandardLogger(),
}
}

func (c *Client) do(method string, path string, body io.Reader, recv interface{}) error {
var err error
url := fmt.Sprintf("%s%s", c.baseURL, path)
c.Logger.Debugf("Sending request to OpsLevel endpoint %s", url)
req, err := http.NewRequest(method, url, body)
if err != nil {
return err
}

req.Header.Add("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
c.Logger.Debugf("Failed to send request to OpsLevel: %s", err.Error())
return err
}
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)

c.Logger.Debugf("Received status code %d", resp.StatusCode)
if resp.StatusCode != http.StatusAccepted {
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(resp.Body)
s := buf.String()
return fmt.Errorf("status %d; %s", resp.StatusCode, s)
}

err = decoder.Decode(&recv)
if err != nil {
c.Logger.Debugf("Failed to decode response from OpsLevel: %s", err.Error())
return err
}
return nil
}
60 changes: 60 additions & 0 deletions deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package rest

import (
"bytes"
"encoding/json"
"fmt"
"time"

"gopkg.in/go-playground/validator.v9"
)

// Deployer represents the entity taking the action
type Deployer struct {
Email string `validate:"required" json:"email"`
Name string `json:"name"`
}

// Commit represents the commit being deployed
type Commit struct {
SHA string `json:"sha"`
Message string `json:"message"`
Branch string `json:"branch"`
Date time.Time `json:"date"`
CommitterName string `json:"committer_name"`
CommiterEmail string `json:"committer_email"`
AuthorName string `json:"author_name"`
AuthorEmail string `json:"author_email"`
AuthoringDate time.Time `json:"authoring_date"`
}

// DeployRequest represents a structured request to the OpsLevel deploys webhook endpoint
type DeployRequest struct {
Service string `validate:"required" json:"service"`
Deployer Deployer `validate:"required" json:"deployer"`
Environment string `validate:"required" json:"environment"`
DeployedAt time.Time `validate:"required" json:"deployed_at"`
Description string `validate:"required" json:"description"`
DeployURL string `json:"deploy_url"`
DeployNumber string `json:"deploy_number"`
Commit Commit `json:"commit"`
}

// Deploy sends a DeployRequest to the OpsLevel deploy integration at integrationID
func (c *Client) Deploy(req DeployRequest, integrationID string) error {
v := validator.New()
if err := v.Struct(req); err != nil {
return err
}

b, err := json.Marshal(req)
if err != nil {
return err
}
var resp struct {
Result string `json:"result"`
}

fullURL := fmt.Sprintf("/integrations/deploy/%s", integrationID)
return c.do("POST", fullURL, bytes.NewReader(b), &resp)
}
Loading

0 comments on commit 0b52dbb

Please sign in to comment.