Skip to content

Instantly share code, notes, and snippets.

@tyndyll
Created October 21, 2015 23:30
Show Gist options
  • Select an option

  • Save tyndyll/32c778039086c3173f28 to your computer and use it in GitHub Desktop.

Select an option

Save tyndyll/32c778039086c3173f28 to your computer and use it in GitHub Desktop.
A handy shortcut for deploying apps to Marathon.
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
)
const (
STAGING_URL = "cfn-marathon-serenity-mesos-stage-us-east-1.posrip.com"
PRODUCTION_URL = "cfn-marathon-serenity-mesos-prod-us-east-1.posrip.com"
SERVER_PATH_TEMPLATE = "http://%s:8080/v2/apps/%s"
ERR_BROKEN_DEPLOY = "The call to the server was unsuccessful"
ERR_INVALID_ENV = "Environment must be stage or prod"
ERR_INVALID_JSON = "Invalid JSON"
ERR_INVALID_FILE = "Could not read file"
ERR_INVALID_DEPLOY = "Error in Deploying"
ERR_USAGE = "USAGE: marathon-deploy <prod|stage> /path/to/config"
)
type Deployment struct {
HttpClient *http.Client
AppName string
Url string
Payload []byte
}
func NewDeployment() *Deployment {
return &Deployment{
HttpClient: &http.Client{},
}
}
func (d *Deployment) SetEnvironment(environment string) error {
switch environment {
case "stage":
d.Url = STAGING_URL
case "prod":
d.Url = PRODUCTION_URL
default:
return errors.New(ERR_INVALID_ENV)
}
return nil
}
func (d *Deployment) ExtractAppName(data []byte) (string, error) {
appDetails := struct {
Id string `json:"id"`
}{}
err := json.Unmarshal(data, &appDetails)
if err != nil {
return "", err
}
if appDetails.Id == "" {
return "", errors.New(ERR_INVALID_JSON)
}
return appDetails.Id, nil
}
func (d *Deployment) LoadFile(filepath string) error {
var err error
if d.Payload, err = ioutil.ReadFile(filepath); err != nil {
return errors.New(ERR_INVALID_FILE)
}
if d.AppName, err = d.ExtractAppName(d.Payload); err != nil {
return errors.New(ERR_INVALID_JSON)
}
return nil
}
func (d *Deployment) MakeServerPath() string {
return fmt.Sprintf(SERVER_PATH_TEMPLATE, d.Url, d.AppName)
}
func (d *Deployment) MakeRequest() ([]byte, error) {
var err error
body := bytes.NewBuffer(d.Payload)
req, _ := http.NewRequest("POST", d.MakeServerPath(), body)
req.Header.Add("Content-Type", "application/json")
response, err := d.HttpClient.Do(req)
if err != nil {
return []byte(fmt.Sprintf("%s", err)), errors.New(ERR_BROKEN_DEPLOY)
}
responseBody, _ := ioutil.ReadAll(response.Body)
if response.StatusCode != http.StatusOK {
err = errors.New(ERR_INVALID_DEPLOY)
}
return responseBody, err
}
func (d *Deployment) Deploy(args []string) ([]byte, error) {
if len(args) != 2 {
return nil, errors.New(ERR_USAGE)
}
if err := d.SetEnvironment(args[0]); err != nil {
return nil, err
}
if err := d.LoadFile(args[1]); err != nil {
return nil, err
}
return d.MakeRequest()
}
func main() {
d := NewDeployment()
response, err := d.Deploy(flag.Args())
if len(response) != 0 {
fmt.Println(string(response))
}
if err != nil {
log.Fatalln(err)
}
}
package main
import (
. "github.com/smartystreets/goconvey/convey"
"testing"
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
)
const (
TEST_APP_NAME = "demo-app"
)
func TestNewDeployment(t *testing.T) {
Convey(`When I call NewDeployment`, t, func() {
d := NewDeployment()
Convey(`Then the return value will be a Deployment instance`, func() {
So(d, ShouldHaveSameTypeAs, &Deployment{})
})
Convey(`Then the HttpClient field should not be nil`, func() {
So(d.HttpClient, ShouldNotBeNil)
})
})
}
func TestSetEnvironment(t *testing.T) {
Convey(`Given I have a Deployment instance`, t, func() {
d := &Deployment{}
Convey(`And I have a valid environment`, func() {
Convey(`When I pass "stage" to GetEnvironment`, func() {
err := d.SetEnvironment("stage")
Convey(`Then the Deplyment Url will equal STAGING`, func() {
So(d.Url, ShouldEqual, STAGING_URL)
})
Convey(`Then the error will be nil`, func() {
So(err, ShouldBeNil)
})
})
Convey(`When I pass "prod" to GetEnvironment`, func() {
err := d.SetEnvironment("prod")
Convey(`Then the Deployment Url will equal PRODUCTION`, func() {
So(d.Url, ShouldEqual, PRODUCTION_URL)
})
Convey(`Then the error will be nil`, func() {
So(err, ShouldBeNil)
})
})
})
Convey(`When I pass a non valid value to GetEnvironment`, func() {
err := d.SetEnvironment("NonValidEnvironemtn")
Convey(`Then the deployment Url will be empty`, func() {
So(d.Url, ShouldEqual, "")
})
Convey(`Then the error will be "Environment must be staging or prod`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_INVALID_ENV)
})
})
})
}
func TestExtractAppName(t *testing.T) {
Convey(`Given I have a Deployment instance`, t, func() {
d := &Deployment{}
Convey(`When I pass a valid JSON string to ExtractAppName`, func() {
name, err := d.ExtractAppName([]byte(validFileContents))
Convey(`Then the error will be nil`, func() {
So(err, ShouldBeNil)
})
Convey(`Then the name should be TEST_APP_NAME`, func() {
So(name, ShouldEqual, TEST_APP_NAME)
})
})
Convey(`When I pass an invalid JSON string to ExtractAppName`, func() {
name, err := d.ExtractAppName(invalidFileContents)
Convey(`Then the error will not be nil`, func() {
So(err, ShouldNotBeNil)
})
Convey(`Then the name should be empty`, func() {
So(name, ShouldEqual, "")
})
})
Convey(`When an app ID has not been set`, func() {
name, err := d.ExtractAppName([]byte(`{"cpus": "lots"}`))
Convey(`Then the error will not be nil`, func() {
So(err, ShouldNotBeNil)
})
Convey(`Then the name should be empty`, func() {
So(name, ShouldEqual, "")
})
})
})
}
func TestLoadFile(t *testing.T) {
Convey(`Given I have a Deployment instance`, t, func() {
d := &Deployment{}
Convey(`And I have a valid file path`, func() {
file, err := ioutil.TempFile("", "")
if err != nil {
panic(err)
}
defer func() {
file.Close()
os.Remove(file.Name())
}()
Convey(`And the file contains valid JSON`, func() {
_, err := file.Write(validFileContents)
if err != nil {
panic(err)
}
Convey(`When I call LoadFile`, func() {
err := d.LoadFile(file.Name())
Convey(`Then the error will be nil`, func() {
So(err, ShouldBeNil)
})
Convey(`Then the Deployment Payload should equal the file contents`, func() {
So(string(d.Payload), ShouldEqual, string(validFileContents))
})
Convey(`Then the Deployment AppName will be set`, func() {
So(d.AppName, ShouldEqual, TEST_APP_NAME)
})
})
})
Convey(`And the file contains invalid JSON`, func() {
_, err := file.Write(invalidFileContents)
if err != nil {
panic(err)
}
Convey(`When I call LoadFile`, func() {
err := d.LoadFile(file.Name())
Convey(`Then the error will be ERR_INVALID_JSON`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_INVALID_JSON)
})
Convey(`Then the Deployment AppName will be empty`, func() {
So(d.AppName, ShouldEqual, "")
})
})
})
})
Convey(`And I have an invalid filepath`, func() {
Convey(`When I call LoadFile`, func() {
err := d.LoadFile("/road/to/nowhere")
Convey(`Then the error will be ERR_INVALID_FILE`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_INVALID_FILE)
})
})
})
})
}
func TestMakeServerPath(t *testing.T) {
Convey(`Given I have a Deployment instance`, t, func() {
d := &Deployment{}
Convey(`Given I have set the environment and app name`, func() {
d.Url = STAGING_URL
d.AppName = TEST_APP_NAME
Convey(`When I call MakeServerPath`, func() {
serverPath := d.MakeServerPath()
Convey(`Then the server path will be correct`, func() {
So(serverPath, ShouldEqual, fmt.Sprintf(SERVER_PATH_TEMPLATE, STAGING_URL, TEST_APP_NAME))
})
})
})
})
}
func TestMakeCall(t *testing.T) {
Convey(`Given I have a Deployment instance`, t, func() {
stubTransport := &StubRoundTripper{}
d := &Deployment{
HttpClient: &http.Client{
Transport: stubTransport,
},
}
Convey(`And I have set the environment, app name and payload`, func() {
d.AppName = TEST_APP_NAME
d.Url = STAGING_URL
d.Payload = validFileContents
Convey(`When I call MakeRequest with a valid request`, func() {
Convey(`And I receive a valid response`, func() {
requestMethod := ""
requestBody := ""
requestPath := ""
requestContentType := ""
responseText := []byte("This is the response text")
stubTransport.RoundTripFunc = func(req *http.Request) (*http.Response, error) {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
panic(err)
}
requestBody = string(body)
requestMethod = req.Method
requestPath = req.URL.String()
requestContentType = req.Header.Get("Content-Type")
responseBuffer := ioutil.NopCloser(bytes.NewBuffer(responseText))
return &http.Response{Body: responseBuffer}, nil
}
response, _ := d.MakeRequest()
Convey(`Then I will make a HTTP POST call`, func() {
So(requestMethod, ShouldEqual, "POST")
})
Convey(`Then the Payload will be in the request body`, func() {
So(requestBody, ShouldEqual, string(d.Payload))
})
Convey(`Then the request will be sent to the correct URL`, func() {
So(requestPath, ShouldEqual, fmt.Sprintf(SERVER_PATH_TEMPLATE, STAGING_URL, TEST_APP_NAME))
})
Convey(`Then the Content-Path header will be set to application/json`, func() {
So(requestContentType, ShouldEqual, "application/json")
})
Convey(`Then the response will be the HTTP response body`, func() {
So(string(response), ShouldEqual, string(responseText))
})
})
Convey(`And I receive a non StatusOK status code`, func() {
responseText := []byte("This is the error message")
stubTransport.RoundTripFunc = func(req *http.Request) (*http.Response, error) {
responseBuffer := ioutil.NopCloser(bytes.NewBuffer(responseText))
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: responseBuffer}, nil
}
response, err := d.MakeRequest()
Convey(`Then the error will be ERR_INVALID_DEPLOY`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_INVALID_DEPLOY)
})
Convey(`Then the response will be the HTTP response body`, func() {
So(string(response), ShouldEqual, string(responseText))
})
})
Convey(`And I receive an error while making the call`, func() {
errorText := "This is the error text"
stubTransport.RoundTripFunc = func(req *http.Request) (*http.Response, error) {
return &http.Response{}, errors.New(errorText)
}
response, err := d.MakeRequest()
Convey(`Then the error will be ERR_BROKEN_DEPLOY`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_BROKEN_DEPLOY)
})
Convey(`Then the response will be the error message`, func() {
So(string(response), ShouldContainSubstring, string(errorText))
})
})
})
})
})
}
func TestDeploy(t *testing.T) {
Convey(`Given I have a Deployment instance`, t, func() {
stubTransport := &StubRoundTripper{}
d := &Deployment{
HttpClient: &http.Client{
Transport: stubTransport,
},
}
Convey(`And I have 2 arguments`, func() {
args := []string{"stage", "filename"}
Convey(`And I have an invalid environment`, func() {
args[0] = "non-existent-environment"
Convey(`When I call Deploy`, func() {
_, err := d.Deploy(args)
Convey(`Then the error will be ERR_INVALID_ENV`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_INVALID_ENV)
})
})
})
Convey(`And I have an invalid file`, func() {
Convey(`When I call Deploy`, func() {
_, err := d.Deploy(args)
Convey(`Then the error will be ERR_INVALID_FILE`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_INVALID_FILE)
})
})
})
Convey(`And the server responds with a non StatusOK code`, func() {
file, err := ioutil.TempFile("", "")
if err != nil {
panic(err)
}
defer func() {
file.Close()
os.Remove(file.Name())
}()
_, err = file.Write(validFileContents)
if err != nil {
panic(err)
}
args[1] = file.Name()
responseText := []byte("This is the error message")
stubTransport.RoundTripFunc = func(req *http.Request) (*http.Response, error) {
responseBuffer := ioutil.NopCloser(bytes.NewBuffer(responseText))
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: responseBuffer}, nil
}
Convey(`When I call Deploy`, func() {
response, err := d.Deploy(args)
Convey(`Then err will be ERR_INVALID_DEPLOY`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_INVALID_DEPLOY)
})
Convey(`Then the response will be the HTTP response body`, func() {
So(string(response), ShouldEqual, string(responseText))
})
})
})
})
Convey(`And I have an invalid number of arguments`, func() {
args := []string{"stage"}
Convey(`When I call Deploy`, func() {
_, err := d.Deploy(args)
Convey(`Then I will the error will be ERR_USAGE`, func() {
So(fmt.Sprintf("%s", err), ShouldEqual, ERR_USAGE)
})
})
})
})
}
type StubRoundTripper struct {
RoundTripFunc func(*http.Request) (*http.Response, error)
}
func (stub *StubRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return stub.RoundTripFunc(req)
}
var validFileContents []byte = []byte(fmt.Sprintf(`{
"id": "%s",
"cpus": 0.1
}`, TEST_APP_NAME))
var invalidFileContents []byte = []byte(`{"unclosedJson": "whoops"`)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment