From ac097c2aca978cd906f7ed61276f3a21df9bd63a Mon Sep 17 00:00:00 2001 From: Thomas Orrick Date: Tue, 11 Aug 2015 11:36:52 -0500 Subject: [PATCH] initial commit --- .gitignore | 3 + Makefile | 16 ++++ README.md | 10 +++ build.sh | 4 + cli.go | 133 +++++++++++++++++++++++++++++++ docker.sh | 1 + openaperture/auth.go | 79 +++++++++++++++++++ openaperture/openaperture.go | 148 +++++++++++++++++++++++++++++++++++ 8 files changed, 394 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100755 build.sh create mode 100644 cli.go create mode 100755 docker.sh create mode 100644 openaperture/auth.go create mode 100644 openaperture/openaperture.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce193d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +spyglass +spyglass-* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..241e644 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +ARCH = amd64 +.PHONY: all build_linux build_darwin build_windows + +all: build_linux build_darwin build_windows + +build_linux: + ./docker.sh linux $(arch) + +build_darwin: + ./docker.sh darwin $(arch) + +build_windows: + ./docker.sh windows $(arch) + +clean: + rm -f spyglass-* diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a8e445 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +Spyglass +======== + +Build Server deployment CLI + +# Usage +* Set `APERTURE_SERVER_URL` environment variable to your OpenAperture dns name. +* Run `spyglass configure` to set your credentials or use `APERTURE_USERNAME` and `APERTURE_PASSWORD` environment variables. + +More usage and help information can be found by running `spyglass --help` diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..dabe518 --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#! /bin/bash + +go get +go build -v -o spyglass-$GOOS-$GOARCH diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..3bf46c1 --- /dev/null +++ b/cli.go @@ -0,0 +1,133 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + "time" + + "github.com/codegangsta/cli" + "github.com/segmentio/go-prompt" + "github.com/torrick/spyglass/openaperture" +) + +func main() { + app := cli.NewApp() + environmentFlag := cli.StringFlag{Name: "environment, e", Usage: "environment to build or deploy"} + commitHashFlag := cli.StringFlag{Name: "commit, c", Usage: "commit hash or branch to build or deploy"} + app.Name = "spyglass" + app.Version = "0.2.5" + app.Author = "Thomas Orrick" + app.Email = "thomas.orrick@lexmark.com" + app.Flags = []cli.Flag{ + cli.BoolFlag{Name: "follow, f", Usage: "continually check status of a build or deploy"}, + cli.StringFlag{Name: "server, s", Usage: "build server url", EnvVar: "APERTURE_SERVER_URL"}, + } + app.Commands = []cli.Command{ + { + Name: "deploy", + Usage: "build and deploy a docker repository", + Action: func(c *cli.Context) { + validate(c) + fmt.Printf("Sending deploy request for:\n Project: %s\n Environment: %s\n", c.Args().First(), c.String("environment")) + project := deploy(openaperture.NewProject(c.Args().First(), c.String("environment"), c.String("commit"), c.GlobalString("server"), + c.String("build-exchange"), c.String("deploy-exchange"), c.Bool("force"))) + if c.GlobalBool("follow") { + checkStatus(project) + } + }, + Flags: []cli.Flag{ + environmentFlag, + commitHashFlag, + cli.StringFlag{Name: "build-exchange", Usage: "Set the build exchange id"}, + cli.StringFlag{Name: "deploy-exchange", Usage: "Set the deploy exchange id"}, + cli.BoolFlag{Name: "force", Usage: "Force a docker build"}, + }, + }, + { + Name: "configure", + Usage: "set configuration options", + Action: func(c *cli.Context) { + configure(c) + }, + }, + } + app.Run(os.Args) +} + +func configure(c *cli.Context) { + username := prompt.String("Username") + password := prompt.Password("Password") + config := map[string]string{"username": username, "password": password} + configJSON, _ := json.Marshal(config) + ioutil.WriteFile(path.Join(os.Getenv("HOME"), ".aperturecfg"), configJSON, 0600) +} + +func checkStatus(project *openaperture.Project) { + auth, err := openaperture.GetAuth() + if err != nil { + panic(err.Error()) + } + ticker := time.NewTicker(5 * time.Second) + quit := make(chan struct{}) + for { + select { + case <-ticker.C: + workflow, err := project.Status(auth) + if err != nil { + close(quit) + fmt.Println(err.Error()) + os.Exit(1) + } else if workflow.WorkflowError { + close(quit) + fmt.Println("Workflow failed") + fmt.Println(workflow.EventLog) + os.Exit(1) + } else if workflow.WorkflowCompleted { + fmt.Printf("Workflow completed in %s\n", workflow.ElapsedWorkflowTime) + close(quit) + } else { + fmt.Printf("Milestone: %s in progress\n", workflow.CurrentStep) + } + case <-quit: + ticker.Stop() + os.Exit(0) + } + } +} + +func validate(c *cli.Context) { + if c.String("environment") == "" { + fmt.Printf("Environment was not set. Please specify an environment to %s\n", c.Command.Name) + os.Exit(1) + } + if c.String("commit") == "" { + fmt.Printf("Commit hash or branch was not set. Please specify a commit hash or branch to %s\n", c.Command.Name) + os.Exit(1) + } + if c.Args().First() == "" { + fmt.Printf("Project name was not set. Please specify a project to %s\n", c.Command.Name) + os.Exit(1) + } +} + +func deploy(project *openaperture.Project) *openaperture.Project { + operations := []string{"build", "deploy"} + token, err := openaperture.GetAuth() + if err != nil { + panic(err.Error()) + } + resp, err := project.CreateWorkflow(token, operations) + if err != nil { + panic(err.Error()) + } + fmt.Printf("Workflow created: %s\n", resp.Location) + err = project.ExecuteWorkflow(token, resp.Location) + if err != nil { + panic(err.Error()) + } + fmt.Println("Successfully sent deploy request") + return project +} diff --git a/docker.sh b/docker.sh new file mode 100755 index 0000000..29d6419 --- /dev/null +++ b/docker.sh @@ -0,0 +1 @@ +docker run --rm -it -e GOOS=$1 -e GOARCH=amd64 -v $GOPATH:/go -w /go/src/github.com/torrick/spyglass golang:1.4-cross ./build.sh diff --git a/openaperture/auth.go b/openaperture/auth.go new file mode 100644 index 0000000..15a3281 --- /dev/null +++ b/openaperture/auth.go @@ -0,0 +1,79 @@ +package openaperture + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "os" + "path" +) + +//Auth struct +type Auth struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn string `json:"expires_in"` + Scope string `json:"scope"` +} + +//GetAuthorizationHeader build a proper authorization header for all api calls +func (oauth *Auth) GetAuthorizationHeader() string { + return "Bearer access_token=" + oauth.AccessToken +} + +// GetAuth returns authentication config from various sources +func GetAuth() (*Auth, error) { + if _, err := os.Stat(path.Join(os.Getenv("HOME"), ".aperturecfg")); os.IsNotExist(err) { + return EnvAuth() + } + return SharedAuth() +} + +// SharedAuth generates an authentication config from local json file +func SharedAuth() (*Auth, error) { + var config map[string]string + configPath := path.Join(os.Getenv("HOME"), ".aperturecfg") + configBytes, err := ioutil.ReadFile(configPath) + if err != nil { + return nil, err + } + json.Unmarshal(configBytes, &config) + return NewAuth(config["username"], config["password"]) +} + +// EnvAuth pulls authentication information from environment variables +func EnvAuth() (*Auth, error) { + username := os.Getenv("APERTURE_USERNAME") + password := os.Getenv("APERTURE_PASSWORD") + if username == "" || password == "" { + return nil, errors.New("username or password is blank") + } + return NewAuth(username, password) +} + +// NewAuth requests a new token from idp +func NewAuth(username string, password string) (*Auth, error) { + var auth Auth + url := "https://auth.psft.co/oauth/token" + credentials := map[string]string{ + "grant_type": "password", + "username": username, + "password": password, + } + payload, _ := json.Marshal(credentials) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + json.Unmarshal(body, &auth) + return &auth, nil +} diff --git a/openaperture/openaperture.go b/openaperture/openaperture.go new file mode 100644 index 0000000..a76d67c --- /dev/null +++ b/openaperture/openaperture.go @@ -0,0 +1,148 @@ +package openaperture + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +// Project Struct +type Project struct { + Name string + Environment string + Commit string + Server string + WorkflowID string + ForceBuild bool + BuildExchangeID string + DeployExchangeID string +} + +// Workflow Struct +type Workflow struct { + EventLog []string `json:"event_log"` + WorkflowCompleted bool `json:"workflow_completed"` + WorkflowError bool `json:"workflow_error"` + CreatedAt string `json:"created_at"` + CurrentStep string `json:"current_step"` + DeploymentRepo string `json:"deployment_repo"` + DeploymentRepoGitRef string `json:"deployment_repo_git_ref"` + ElapsedStepTime string `json:"elapsed_step_time"` + ElapsedWorkflowTime string `json:"elapsed_workflow_time"` + ID string `json:"id"` + Milestones []string `json:"milestones"` + SourceCommitHash string `json:"source_commit_hash"` + SourceRepo string `json:"source_repo"` + SourceRepoGitRef string `json:"source_repo_git_ref"` + UpdatedAt string `json:"updated_at"` + WorkflowDuration string `json:"workflow_duration"` + WorkflowStepDurations map[string]string `json:"workflow_step_durations"` +} + +//Request struct +type Request struct { + DeploymentRepo string `json:"deployment_repo"` + DeploymentGitRef string `json:"deployment_repo_git_ref"` + SourceRepo string `json:"source_repo"` + SourceRepoGitRef string `json:"source_repo_git_ref"` + Milestones []string `json:"milestones"` +} + +//ExecuteRequest struct +type ExecuteRequest struct { + BuildExchangeID string `json:"build_messaging_exchange_id,omitempty"` + DeployExchangeID string `json:"deploy_messaging_exchange_id, omitempty"` + ForceBuild bool `json:"force_build"` +} + +//ApertureResponse struct +type ApertureResponse struct { + Status string + StatusCode int + Location string + Body []byte +} + +// NewProject Builds a Project object +func NewProject(projectName string, environment string, commit string, server string, buildExchangeID string, + deployExchangeID string, forceBuild bool) *Project { + return &Project{Name: projectName, Environment: environment, Commit: commit, Server: server, + BuildExchangeID: buildExchangeID, DeployExchangeID: deployExchangeID, ForceBuild: forceBuild} +} + +// NewRequest builds a new OpenAperture request +func (project *Project) NewRequest(operations []string) *Request { + return &Request{DeploymentRepo: project.Name, DeploymentGitRef: project.Environment, + SourceRepoGitRef: project.Commit, Milestones: operations} +} + +// NewExecuteRequest builds a new ExecuteRequest +func (project *Project) NewExecuteRequest(forceBuild bool) *ExecuteRequest { + return &ExecuteRequest{BuildExchangeID: project.BuildExchangeID, DeployExchangeID: project.DeployExchangeID, ForceBuild: forceBuild} +} + +// CreateWorkflow initalizes the specifed project workflow +func (project *Project) CreateWorkflow(auth *Auth, operations []string) (*ApertureResponse, error) { + uri := fmt.Sprintf("%s/workflows", project.Server) + payload, _ := json.Marshal(project.NewRequest(operations)) + resp, err := httpRequest("POST", auth, uri, payload) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusCreated { + return nil, errors.New("Failed to create workflow: " + resp.Status) + } + project.WorkflowID = strings.Split(resp.Location, "/")[2] + return resp, nil +} + +// ExecuteWorkflow sends the request to OpenAperture to execute the specified workflow +func (project *Project) ExecuteWorkflow(auth *Auth, workflowURI string) error { + uri := fmt.Sprintf("%s%s/execute", project.Server, workflowURI) + payload, _ := json.Marshal(project.NewExecuteRequest(project.ForceBuild)) + resp, err := httpRequest("POST", auth, uri, payload) + if err != nil { + return err + } + if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusAccepted { + return nil + } + return errors.New("Failed to execute workflow: " + resp.Status) +} + +// Status checks the status of a workflow +func (project *Project) Status(auth *Auth) (*Workflow, error) { + var workflow *Workflow + path := fmt.Sprintf("%s/workflows/%s", project.Server, project.WorkflowID) + resp, err := httpRequest("GET", auth, path, nil) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("Failed to retrieve status: " + resp.Status) + } + json.Unmarshal(resp.Body, &workflow) + return workflow, nil +} + +func httpRequest(method string, auth *Auth, uri string, payload []byte) (*ApertureResponse, error) { + var location string + req, _ := http.NewRequest(method, uri, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", auth.GetAuthorizationHeader()) + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + if resp.Header["Location"] != nil { + location = resp.Header["Location"][0] + } + return &ApertureResponse{Status: resp.Status, StatusCode: resp.StatusCode, Body: body, Location: location}, nil +}