Created
October 21, 2015 23:30
-
-
Save tyndyll/32c778039086c3173f28 to your computer and use it in GitHub Desktop.
A handy shortcut for deploying apps to Marathon.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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