switch stannis to use bosh-curl-api library which uses 'bosh curl' https://github.com/cloudfoundry/bosh-cli/pull/408
This commit is contained in:
parent
7c64f011bd
commit
a7a8aeffb2
3
Procfile
3
Procfile
|
@ -1 +1,2 @@
|
|||
web: stannis webserver
|
||||
web: stannis webserver --config config/webserver.config.example.yml
|
||||
agent: stannis agent --config config/agent.config.example.yml
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/stannis/config"
|
||||
"github.com/drnic/bosh-curl-api/boshcli"
|
||||
)
|
||||
|
||||
// ToBOSH is the outbound data from a BOSH
|
||||
type ToBOSH struct {
|
||||
Name string `json:"name"`
|
||||
Target string `json:"target"`
|
||||
ReallyUUID string `json:"reallyuuid"`
|
||||
UUID string `json:"uuid"`
|
||||
Version string `json:"version"`
|
||||
CPI string `json:"cpi"`
|
||||
Deployments models.Deployments `json:"deployments"`
|
||||
Name string `json:"name"`
|
||||
Target string `json:"target"`
|
||||
ReallyUUID string `json:"reallyuuid"`
|
||||
UUID string `json:"uuid"`
|
||||
Version string `json:"version"`
|
||||
CPI string `json:"cpi"`
|
||||
Deployments boshcli.Deployments `json:"deployments"`
|
||||
}
|
||||
|
||||
// Agent is the parent model for agent runtime behavior
|
||||
|
|
|
@ -11,11 +11,8 @@ import (
|
|||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh"
|
||||
"github.com/cloudfoundry-community/gogobosh/api"
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
"github.com/cloudfoundry-community/stannis/config"
|
||||
"github.com/drnic/bosh-curl-api/boshcli"
|
||||
)
|
||||
|
||||
// NewAgent constructs Agent parent struct
|
||||
|
@ -27,20 +24,10 @@ func NewAgent(agentConfig *config.AgentConfig) (agent Agent) {
|
|||
|
||||
// FetchAndUpload fetches deployments from BOSH and uploads to collector API
|
||||
func (agent Agent) FetchAndUpload() {
|
||||
director := gogobosh.NewDirector(agent.Config.BOSHTarget, agent.Config.BOSHUsername, agent.Config.BOSHPassword)
|
||||
repo := api.NewBoshDirectorRepository(&director, net.NewDirectorGateway())
|
||||
boshcli.Check()
|
||||
|
||||
info, apiResponse := repo.GetInfo()
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch BOSH info")
|
||||
return
|
||||
}
|
||||
|
||||
boshDeployments, apiResponse := repo.GetDeployments()
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch BOSH deployments")
|
||||
return
|
||||
}
|
||||
info := boshcli.GetInfo()
|
||||
boshDeployments := boshcli.GetDeployments()
|
||||
|
||||
reallyUUID := ReallyUUID(agent.Config.BOSHTarget, info.UUID)
|
||||
|
||||
|
@ -51,7 +38,7 @@ func (agent Agent) FetchAndUpload() {
|
|||
ReallyUUID: reallyUUID,
|
||||
Version: info.Version,
|
||||
CPI: info.CPI,
|
||||
Deployments: models.Deployments{},
|
||||
Deployments: *boshDeployments,
|
||||
}
|
||||
|
||||
fmt.Println("Data to upload", uploadData)
|
||||
|
@ -64,7 +51,7 @@ func (agent Agent) FetchAndUpload() {
|
|||
uploadEndpoint := fmt.Sprintf("%s/upload", agent.Config.WebserverTarget)
|
||||
uploadDeploymentData(agent.Config, uploadEndpoint, bytes.NewReader(b))
|
||||
|
||||
for _, boshDeployment := range boshDeployments {
|
||||
for _, boshDeployment := range *boshDeployments {
|
||||
deploymentName := boshDeployment.Name
|
||||
b, err = json.Marshal(boshDeployment)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
---
|
||||
bosh_target: https://54.161.140.194:25555
|
||||
bosh_username: admin
|
||||
bosh_password: admin
|
||||
|
||||
webserver_target: http://localhost:3000
|
||||
webserver_target: http://localhost:5000
|
||||
webserver_username: admin
|
||||
webserver_password: admin
|
||||
|
|
|
@ -3,24 +3,20 @@ auth:
|
|||
username: admin
|
||||
password: admin
|
||||
pipelines:
|
||||
- name: Cloud Foundry
|
||||
tag: cf
|
||||
- name: Redis
|
||||
tag: redis
|
||||
filter:
|
||||
using_bosh_releases: [cf]
|
||||
- name: RDPG
|
||||
tag: rdpg
|
||||
using_bosh_releases: [redis]
|
||||
- name: BPM
|
||||
tag: bpm
|
||||
filter:
|
||||
using_bosh_releases: [rdpg]
|
||||
- name: Logstash Service
|
||||
tag: logstash-service
|
||||
filter:
|
||||
using_bosh_releases: [docker, logstash-docker]
|
||||
using_bosh_releases: [bpm]
|
||||
tiers:
|
||||
- name: try-anything
|
||||
- name: local
|
||||
slots:
|
||||
- name: bosh-lite
|
||||
- name: warden-cpi
|
||||
filter:
|
||||
deployment_name_regexp: try-anything
|
||||
deployment_name_regexp: .*
|
||||
- name: dc
|
||||
slots:
|
||||
- name: sandbox
|
||||
|
|
5
main.go
5
main.go
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/codegangsta/cli"
|
||||
"github.com/codegangsta/martini-contrib/auth"
|
||||
"github.com/codegangsta/martini-contrib/binding"
|
||||
"github.com/drnic/bosh-curl-api/boshcli"
|
||||
"github.com/go-martini/martini"
|
||||
"github.com/martini-contrib/render"
|
||||
)
|
||||
|
@ -92,6 +93,8 @@ func runAgent(c *cli.Context) {
|
|||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
boshcli.Check()
|
||||
|
||||
fmt.Println(agentConfig)
|
||||
|
||||
agent.NewAgent(agentConfig).FetchAndUpload()
|
||||
|
@ -120,7 +123,7 @@ func runWebserver(c *cli.Context) {
|
|||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "stannis"
|
||||
app.Version = "0.4.0"
|
||||
app.Version = "0.5.0"
|
||||
app.Usage = "What deployments are running in which BOSH?"
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
/pkg
|
||||
/bin
|
|
@ -1 +0,0 @@
|
|||
Godeps/_workspace
|
|
@ -1,12 +0,0 @@
|
|||
language: go
|
||||
|
||||
go: 1.4.2
|
||||
|
||||
install:
|
||||
- export PATH=$PATH:$HOME/gopath/bin
|
||||
- go get github.com/onsi/ginkgo/ginkgo
|
||||
- go get github.com/tools/godep
|
||||
- godep restore
|
||||
|
||||
script:
|
||||
- ginkgo -r
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"ImportPath": "github.com/cloudfoundry-community/gogobosh",
|
||||
"GoVersion": "go1.4",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/onsi/ginkgo",
|
||||
"Comment": "v1.1.0-29-g18c73cf",
|
||||
"Rev": "18c73cfeca1095f984c036a04c42ac0b08048685"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/onsi/gomega",
|
||||
"Comment": "v1.0-23-g2cd6d99",
|
||||
"Rev": "2cd6d99ccf3ac7ae8398d8296429161bf7061ae2"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
|
@ -1,116 +0,0 @@
|
|||
# Go Go BOSH - BOSH client API for golang applications
|
||||
|
||||
This project is a golang library for applications wanting to talk to a BOSH/MicroBOSH or bosh-lite.
|
||||
|
||||
* [![GoDoc](https://godoc.org/github.com/cloudfoundry-community/gogobosh?status.png)](https://godoc.org/github.com/cloudfoundry-community/gogobosh)
|
||||
* Test status [![Build Status](https://travis-ci.org/cloudfoundry-community/gogobosh.svg)](https://travis-ci.org/cloudfoundry-community/gogobosh)
|
||||
|
||||
|
||||
## API
|
||||
|
||||
The following client functions are available, as a subset of the full BOSH Director API.
|
||||
|
||||
* repo.GetInfo()
|
||||
* repo.GetStemcells()
|
||||
* repo.DeleteStemcell("bosh-stemcell", "993")
|
||||
* repo.GetReleases()
|
||||
* repo.DeleteReleases("cf")
|
||||
* repo.DeleteRelease("cf", "144")
|
||||
* repo.GetDeployments()
|
||||
* repo.ListDeploymentVMs("cf-warden")
|
||||
* repo.FetchVMsStatus("cf-warden")
|
||||
* repo.GetTaskStatus(123)
|
||||
|
||||
### API compatibility
|
||||
|
||||
Note: Development is currently being done against bosh-lite v147.
|
||||
|
||||
The BOSH Core team nor its Product Managers do not claim that a BOSH director has a public API; and they want to make changes to the API in the future. It will may be tricky for golang apps to support different BOSH APIs. We'll figure this out as we go.
|
||||
|
||||
The best way to describe the API support in this library is to document what version of bosh-lite is being tested against, the date that it was published. Hopefully bosh-lite is always approximately parallel (via rebasing) in its API with the main BOSH project; and the same timestamps can map to the continuously delivered releases of BOSH & its RubyGems.
|
||||
|
||||
Trying to write a client library for an API without any versioning strategy could get messy for client applications. Please write your own integration tests that work against running BOSHes that you'll use in production.
|
||||
|
||||
If you are using this library, or the Ruby library within the `bosh_cli` rubygem, or talking directly with the BOSH director API - please announce yourself on the bosh-users google group and/or to the PM of BOSH. This way they can be aware of who many be affected by API changes.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
go get github.com/cloudfoundry-community/gogobosh
|
||||
````
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is published to [https://godoc.org/github.com/cloudfoundry-community/gogobosh](https://godoc.org/github.com/cloudfoundry-community/gogobosh).
|
||||
|
||||
Also, view the documentation locally with:
|
||||
|
||||
```
|
||||
godoc -goroot=$GOPATH github.com/cloudfoundry-community/gogobosh
|
||||
```
|
||||
|
||||
### Use
|
||||
|
||||
There is an extensive [example application](https://github.com/cloudfoundry-community/gogobosh/blob/master/example/bosh-lite-example.go) showing usage of many of the read-only functions.
|
||||
|
||||
As a short getting started guide:
|
||||
|
||||
``` golang
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh"
|
||||
"github.com/cloudfoundry-community/gogobosh/api"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
"github.com/cloudfoundry-community/gogobosh/utils"
|
||||
"fmt"
|
||||
"flag"
|
||||
)
|
||||
|
||||
func main() {
|
||||
utils.Logger = utils.NewLogger()
|
||||
|
||||
target := flag.String("target", "https://192.168.50.4:25555", "BOSH director host")
|
||||
username := flag.String("username", "admin", "Login with username")
|
||||
password := flag.String("password", "admin", "Login with password")
|
||||
flag.Parse()
|
||||
|
||||
director := gogobosh.NewDirector(*target, *username, *password)
|
||||
repo := api.NewBoshDirectorRepository(&director, net.NewDirectorGateway())
|
||||
|
||||
info, apiResponse := repo.GetInfo()
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch BOSH info")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Director")
|
||||
fmt.Printf(" Name %s\n", info.Name)
|
||||
fmt.Printf(" URL %s\n", info.URL)
|
||||
fmt.Printf(" Version %s\n", info.Version)
|
||||
fmt.Printf(" User %s\n", info.User)
|
||||
fmt.Printf(" UUID %s\n", info.UUID)
|
||||
fmt.Printf(" CPI %s\n", info.CPI)
|
||||
}
|
||||
```
|
||||
|
||||
You can automatically detect the current director target, and username/password, from the BOSH CLI's `~/.bosh_config` file as well (see `example/current_target.go`)
|
||||
|
||||
```golang
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh"
|
||||
"github.com/cloudfoundry-community/gogobosh/local"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath, err := local.DefaultBoshConfigPath()
|
||||
config, err := local.LoadBoshConfig(configPath)
|
||||
target, username, password, err := config.CurrentBoshTarget()
|
||||
director := gogobosh.NewDirector(target, username, password)
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Tests are all local currently; and do not test against a running bosh or bosh-lite. I'd like to at least do integration tests against a bosh-lite in future.
|
122
vendor/github.com/cloudfoundry-community/gogobosh/api/director_deployments.go
generated
vendored
122
vendor/github.com/cloudfoundry-community/gogobosh/api/director_deployments.go
generated
vendored
|
@ -1,122 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"launchpad.net/goyaml"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
)
|
||||
|
||||
// GetDeployments returns a list of deployments, and the releases/stemcells being used
|
||||
func (repo BoshDirectorRepository) GetDeployments() (deployments models.Deployments, apiResponse net.ApiResponse) {
|
||||
response := []deploymentResponse{}
|
||||
|
||||
path := "/deployments"
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &response)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
list := []*models.Deployment{}
|
||||
for _, resource := range response {
|
||||
list = append(list, resource.ToModel())
|
||||
}
|
||||
deployments = models.Deployments(list)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetDeploymentManifest returns a deployment manifest
|
||||
func (repo BoshDirectorRepository) GetDeploymentManifest(deploymentName string) (manifest *models.DeploymentManifest, apiResponse net.ApiResponse) {
|
||||
deploymentManifestResponse := deploymentManifestResponse{}
|
||||
|
||||
path := fmt.Sprintf("/deployments/%s", deploymentName)
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &deploymentManifestResponse)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
return deploymentManifestResponse.ToModel(), apiResponse
|
||||
}
|
||||
|
||||
// DeleteDeployment asks the director to delete a deployment
|
||||
func (repo BoshDirectorRepository) DeleteDeployment(deploymentName string) (apiResponse net.ApiResponse) {
|
||||
path := fmt.Sprintf("/deployments/%s?force=true", deploymentName)
|
||||
apiResponse = repo.gateway.DeleteResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
if !apiResponse.IsRedirection() {
|
||||
return
|
||||
}
|
||||
|
||||
var taskStatus models.TaskStatus
|
||||
taskURL, err := url.Parse(apiResponse.RedirectLocation)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+taskURL.Path, repo.config.Username, repo.config.Password, &taskStatus)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
/* Progression should be: queued, progressing, done */
|
||||
/* TODO task might fail; end states: done, error, cancelled */
|
||||
for taskStatus.State != "done" {
|
||||
time.Sleep(1)
|
||||
taskStatus, apiResponse = repo.GetTaskStatus(taskStatus.ID)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type deploymentResponse struct {
|
||||
Name string `json:"name"`
|
||||
Releases []nameVersion `json:"releases"`
|
||||
Stemcells []nameVersion `json:"stemcells"`
|
||||
}
|
||||
|
||||
type deploymentManifestResponse struct {
|
||||
RawManifest string `json:"manifest"`
|
||||
}
|
||||
|
||||
type nameVersion struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func (resource deploymentResponse) ToModel() (deployment *models.Deployment) {
|
||||
deployment = &models.Deployment{}
|
||||
deployment.Name = resource.Name
|
||||
for _, releaseResponse := range resource.Releases {
|
||||
release := models.NameVersion{}
|
||||
release.Name = releaseResponse.Name
|
||||
release.Version = releaseResponse.Version
|
||||
|
||||
deployment.Releases = append(deployment.Releases, release)
|
||||
}
|
||||
|
||||
for _, stemcellResponse := range resource.Stemcells {
|
||||
stemcell := models.NameVersion{}
|
||||
stemcell.Name = stemcellResponse.Name
|
||||
stemcell.Version = stemcellResponse.Version
|
||||
|
||||
deployment.Stemcells = append(deployment.Stemcells, stemcell)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ToModel converts a GetDeploymentManifest API response into models.DeploymentManifest
|
||||
func (resource deploymentManifestResponse) ToModel() (manifest *models.DeploymentManifest) {
|
||||
manifest = &models.DeploymentManifest{}
|
||||
goyaml.Unmarshal([]byte(resource.RawManifest), manifest)
|
||||
return
|
||||
}
|
119
vendor/github.com/cloudfoundry-community/gogobosh/api/director_deployments_test.go
generated
vendored
119
vendor/github.com/cloudfoundry-community/gogobosh/api/director_deployments_test.go
generated
vendored
|
@ -1,119 +0,0 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Deployments", func() {
|
||||
It("GetDeployments() - list of deployments", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/deployments",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `[
|
||||
{
|
||||
"name": "cf-warden",
|
||||
"releases": [
|
||||
{
|
||||
"name": "cf",
|
||||
"version": "153"
|
||||
}
|
||||
],
|
||||
"stemcells": [
|
||||
{
|
||||
"name": "bosh-stemcell",
|
||||
"version": "993"
|
||||
}
|
||||
]
|
||||
}
|
||||
]`}})
|
||||
ts, handler, repo := createDirectorRepo(request)
|
||||
defer ts.Close()
|
||||
|
||||
deployments, apiResponse := repo.GetDeployments()
|
||||
|
||||
deployment := deployments[0]
|
||||
Expect(deployment.Name).To(Equal("cf-warden"))
|
||||
|
||||
deploymentRelease := deployment.Releases[0]
|
||||
Expect(deploymentRelease.Name).To(Equal("cf"))
|
||||
Expect(deploymentRelease.Version).To(Equal("153"))
|
||||
|
||||
deploymentStemcell := deployment.Stemcells[0]
|
||||
Expect(deploymentStemcell.Name).To(Equal("bosh-stemcell"))
|
||||
Expect(deploymentStemcell.Version).To(Equal("993"))
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
|
||||
It("GetDeployment(name) - get deployment, including manifest", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/deployments/cf-warden",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `{
|
||||
"manifest": "name: cf-warden"
|
||||
}`,
|
||||
}})
|
||||
ts, handler, repo := createDirectorRepo(request)
|
||||
defer ts.Close()
|
||||
|
||||
manifest, apiResponse := repo.GetDeploymentManifest("cf-warden")
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
|
||||
Expect(manifest.Name).To(Equal("cf-warden"))
|
||||
})
|
||||
|
||||
It("DeleteDeployment(name) forcefully", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "DELETE",
|
||||
Path: "/deployments/cf-warden?force=true",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusFound,
|
||||
Header: http.Header{
|
||||
"Location": {"https://some.host/tasks/20"},
|
||||
},
|
||||
}})
|
||||
ts, handler, repo := createDirectorRepo(
|
||||
request,
|
||||
taskTestRequest(20, "queued"),
|
||||
taskTestRequest(20, "processing"),
|
||||
taskTestRequest(20, "done"),
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
apiResponse := repo.DeleteDeployment("cf-warden")
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
})
|
||||
|
||||
// Shared helper for asserting that a /tasks/ID is requested and returns a models.TaskStatus response
|
||||
func taskTestRequest(taskID int, state string) testhelpers.TestRequest {
|
||||
baseJSON := `{
|
||||
"id": %d,
|
||||
"state": "%s",
|
||||
"description": "some task",
|
||||
"timestamp": 1390174354,
|
||||
"result": null,
|
||||
"user": "admin"
|
||||
}`
|
||||
return testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: fmt.Sprintf("/tasks/%d", taskID),
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: fmt.Sprintf(baseJSON, taskID, state),
|
||||
},
|
||||
})
|
||||
}
|
122
vendor/github.com/cloudfoundry-community/gogobosh/api/director_fetch_vms_state.go
generated
vendored
122
vendor/github.com/cloudfoundry-community/gogobosh/api/director_fetch_vms_state.go
generated
vendored
|
@ -1,122 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
)
|
||||
|
||||
func (repo BoshDirectorRepository) FetchVMsStatus(deploymentName string) (vmsStatuses []models.VMStatus, apiResponse net.ApiResponse) {
|
||||
var taskStatus models.TaskStatus
|
||||
|
||||
/*
|
||||
* Two API calls
|
||||
* 1. GET /deployments/%s/vms?format=full and be redirected to a /tasks/123
|
||||
* 2. Streaming GET on /tasks/123/output?type=result - each line is a models.VMStatus
|
||||
*/
|
||||
path := fmt.Sprintf("/deployments/%s/vms?format=full", deploymentName)
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &taskStatus)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
/* Progression should be: queued, progressing, done */
|
||||
/* TODO task might fail; end states: done, error, cancelled */
|
||||
for taskStatus.State != "done" {
|
||||
time.Sleep(1)
|
||||
taskStatus, apiResponse = repo.GetTaskStatus(taskStatus.ID)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
path = fmt.Sprintf("/tasks/%d/output?type=result", taskStatus.ID)
|
||||
request, apiResponse := repo.gateway.NewRequest("GET", repo.config.TargetURL+path, repo.config.Username, repo.config.Password, nil)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
bytes, _, apiResponse := repo.gateway.PerformRequestForResponseBytes(request)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
if apiResponse.StatusCode > 203 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, vmStatusItem := range strings.Split(string(bytes), "\n") {
|
||||
resource := vmStatusResponse{}
|
||||
err := json.Unmarshal([]byte(vmStatusItem), &resource)
|
||||
if err == nil {
|
||||
vmsStatuses = append(vmsStatuses, resource.ToModel())
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type vmStatusResponse struct {
|
||||
JobName string `json:"job_name"`
|
||||
Index int `json:"index"`
|
||||
JobState string `json:"job_state"`
|
||||
VMCid string `json:"vm_cid"`
|
||||
AgentID string `json:"agent_id"`
|
||||
IPs []string `json:"ips"`
|
||||
DNSs []string `json:"dns"`
|
||||
ResourcePool string `json:"resource_pool"`
|
||||
ResurrectionPaused bool `json:"resurrection_paused"`
|
||||
Vitals vitalsResponse `json:"vitals"`
|
||||
}
|
||||
|
||||
type vitalsResponse struct {
|
||||
Load []string `json:"load"`
|
||||
CPU cpuResponse `json:"cpu"`
|
||||
Memory percentKbResponse `json:"mem"`
|
||||
Swap percentKbResponse `json:"swap"`
|
||||
Disk diskResponse `json:"disk"`
|
||||
}
|
||||
|
||||
type cpuResponse struct {
|
||||
User float64 `json:"user,string"`
|
||||
System float64 `json:"sys,string"`
|
||||
Wait float64 `json:"wait,string"`
|
||||
}
|
||||
|
||||
type diskResponse struct {
|
||||
Persistent percentKbResponse `json:"persistent"`
|
||||
}
|
||||
|
||||
type percentKbResponse struct {
|
||||
Percent float64 `json:"percent,string"`
|
||||
Kb int `json:"kb,string"`
|
||||
}
|
||||
|
||||
func (resource vmStatusResponse) ToModel() (status models.VMStatus) {
|
||||
status = models.VMStatus{}
|
||||
status.JobName = resource.JobName
|
||||
status.Index = resource.Index
|
||||
status.JobState = resource.JobState
|
||||
status.VMCid = resource.VMCid
|
||||
status.AgentID = resource.AgentID
|
||||
status.ResourcePool = resource.ResourcePool
|
||||
status.ResurrectionPaused = resource.ResurrectionPaused
|
||||
|
||||
status.IPs = resource.IPs
|
||||
status.DNSs = resource.DNSs
|
||||
|
||||
status.CPUUser = resource.Vitals.CPU.User
|
||||
status.CPUSys = resource.Vitals.CPU.System
|
||||
status.CPUWait = resource.Vitals.CPU.Wait
|
||||
status.MemoryPercent = resource.Vitals.Memory.Percent
|
||||
status.MemoryKb = resource.Vitals.Memory.Kb
|
||||
status.SwapPercent = resource.Vitals.Swap.Percent
|
||||
status.SwapKb = resource.Vitals.Swap.Kb
|
||||
status.DiskPersistentPercent = resource.Vitals.Disk.Persistent.Percent
|
||||
|
||||
return
|
||||
}
|
60
vendor/github.com/cloudfoundry-community/gogobosh/api/director_fetch_vms_state_test.go
generated
vendored
60
vendor/github.com/cloudfoundry-community/gogobosh/api/director_fetch_vms_state_test.go
generated
vendored
|
@ -1,60 +0,0 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var _ = Describe("parse full vms task output", func() {
|
||||
It("GET /deployments/cf-warden/vms?format=full to return Director{}", func() {
|
||||
vmsRequest := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/deployments/cf-warden/vms?format=full",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusFound,
|
||||
Header: http.Header{
|
||||
"Location": {"https://some.host/tasks/12"},
|
||||
},
|
||||
},
|
||||
})
|
||||
taskOutputRequest := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/tasks/12/output?type=result",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `{"vm_cid":"vm-a1a3d634-367d-4b75-940c-ef7742a970d9","ips":["10.244.1.14"],"dns":[],"agent_id":"c0da6161-e66f-4910-a0eb-dc6fc19b4b25","job_name":"hm9000_z1","index":0,"job_state":"running","resource_pool":"medium_z1","vitals":{"load":["0.11","0.21","0.18"],"cpu":{"user":"1.5","sys":"2.8","wait":"0.1"},"mem":{"percent":"46.8","kb":"2864212"},"swap":{"percent":"0.0","kb":"0"},"disk":{"system":{"percent":null},"persistent":{"percent":"1"}}},"resurrection_paused":false}
|
||||
{"vm_cid":"vm-affdbbdb-b91e-4838-b068-f1a057242169","ips":["10.244.0.38"],"dns":[],"agent_id":"bec309f8-0e2d-4843-9db3-a419adab4d38","job_name":"etcd_leader_z1","index":0,"job_state":"running","resource_pool":"medium_z1","vitals":{"load":["0.13","0.22","0.18"],"cpu":{"user":"0.4","sys":"2.0","wait":"0.1"},"mem":{"percent":"46.8","kb":"2863012"},"swap":{"percent":"0.0","kb":"0"},"disk":{"system":{"percent":null},"persistent":{"percent":"1"}}},"resurrection_paused":false}
|
||||
`}})
|
||||
|
||||
ts, handler, repo := createDirectorRepo(
|
||||
vmsRequest,
|
||||
taskTestRequest(12, "queued"),
|
||||
taskTestRequest(12, "processing"),
|
||||
taskTestRequest(12, "done"),
|
||||
taskOutputRequest)
|
||||
defer ts.Close()
|
||||
|
||||
vmStatuses, apiResponse := repo.FetchVMsStatus("cf-warden")
|
||||
|
||||
/* TODO convert vmStatuses to a chan and pluck first item from chan */
|
||||
Expect(len(vmStatuses)).To(Equal(2))
|
||||
vmStatus := vmStatuses[0]
|
||||
Expect(vmStatus.JobName).To(Equal("hm9000_z1"))
|
||||
Expect(vmStatus.Index).To(Equal(0))
|
||||
Expect(vmStatus.JobState).To(Equal("running"))
|
||||
Expect(vmStatus.VMCid).To(Equal("vm-a1a3d634-367d-4b75-940c-ef7742a970d9"))
|
||||
Expect(vmStatus.AgentID).To(Equal("c0da6161-e66f-4910-a0eb-dc6fc19b4b25"))
|
||||
Expect(vmStatus.ResourcePool).To(Equal("medium_z1"))
|
||||
Expect(vmStatus.ResurrectionPaused).To(Equal(false))
|
||||
|
||||
Expect(len(vmStatus.IPs)).To(Equal(1))
|
||||
Expect(vmStatus.IPs[0]).To(Equal("10.244.1.14"))
|
||||
|
||||
Expect(len(vmStatus.DNSs)).To(Equal(0))
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
})
|
|
@ -1,74 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
)
|
||||
|
||||
func (repo BoshDirectorRepository) GetInfo() (directorInfo models.DirectorInfo, apiResponse net.ApiResponse) {
|
||||
infoResource := directorInfoResponse{}
|
||||
|
||||
path := "/info"
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &infoResource)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
directorInfo = infoResource.ToModel()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type directorInfoResponse struct {
|
||||
Name string `json:"name"`
|
||||
UUID string `json:"uuid"`
|
||||
Version string `json:"version"`
|
||||
User string `json:"user"`
|
||||
CPI string `json:"cpi"`
|
||||
Features directorInfoFeaturesResponse `json:"features"`
|
||||
}
|
||||
|
||||
type directorInfoFeaturesResponse struct {
|
||||
DNS directorInfoFeaturesDNS `json:"dns"`
|
||||
CompiledPackageCache directorInfoFeaturesCompiledPackageCache `json:"compiled_package_cache"`
|
||||
Snapshots directorInfoFeaturesSnapshots `json:"snapshots"`
|
||||
}
|
||||
|
||||
type directorInfoFeaturesDNS struct {
|
||||
Status bool `json:"status"`
|
||||
Extras directorInfoFeaturesDNSExtras `json:"extras"`
|
||||
}
|
||||
|
||||
type directorInfoFeaturesDNSExtras struct {
|
||||
DomainName string `json:"domain_name"`
|
||||
}
|
||||
|
||||
type directorInfoFeaturesCompiledPackageCache struct {
|
||||
Status bool `json:"status"`
|
||||
Extras directorInfoFeaturesCompiledPackageCacheExtras `json:"extras"`
|
||||
}
|
||||
|
||||
type directorInfoFeaturesCompiledPackageCacheExtras struct {
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
type directorInfoFeaturesSnapshots struct {
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
func (resource directorInfoResponse) ToModel() (director models.DirectorInfo) {
|
||||
director = models.DirectorInfo{}
|
||||
director.Name = resource.Name
|
||||
director.Version = resource.Version
|
||||
director.User = resource.User
|
||||
director.UUID = resource.UUID
|
||||
director.CPI = resource.CPI
|
||||
|
||||
director.DNSEnabled = resource.Features.DNS.Status
|
||||
director.DNSDomainName = resource.Features.DNS.Extras.DomainName
|
||||
director.CompiledPackageCacheEnabled = resource.Features.CompiledPackageCache.Status
|
||||
director.CompiledPackageCacheProvider = resource.Features.CompiledPackageCache.Extras.Provider
|
||||
director.SnapshotsEnabled = resource.Features.Snapshots.Status
|
||||
|
||||
return
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/api"
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
"github.com/cloudfoundry-community/gogobosh/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("get director info", func() {
|
||||
It("GET /info to return Director{}", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/info",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `{
|
||||
"name": "Bosh Lite Director",
|
||||
"uuid": "bd462a15-213d-448c-aa5b-66624dad3f0e",
|
||||
"version": "1.5.0.pre.1657 (14bc162c)",
|
||||
"user": "admin",
|
||||
"cpi": "warden",
|
||||
"features": {
|
||||
"dns": {
|
||||
"status": false,
|
||||
"extras": {
|
||||
"domain_name": "bosh"
|
||||
}
|
||||
},
|
||||
"compiled_package_cache": {
|
||||
"status": true,
|
||||
"extras": {
|
||||
"provider": "local"
|
||||
}
|
||||
},
|
||||
"snapshots": {
|
||||
"status": false
|
||||
}
|
||||
}
|
||||
}`}})
|
||||
ts, handler, repo := createDirectorRepo(request)
|
||||
defer ts.Close()
|
||||
|
||||
info, apiResponse := repo.GetInfo()
|
||||
|
||||
Expect(info.Name).To(Equal("Bosh Lite Director"))
|
||||
Expect(info.UUID).To(Equal("bd462a15-213d-448c-aa5b-66624dad3f0e"))
|
||||
Expect(info.Version).To(Equal("1.5.0.pre.1657 (14bc162c)"))
|
||||
Expect(info.User).To(Equal("admin"))
|
||||
Expect(info.CPI).To(Equal("warden"))
|
||||
Expect(info.DNSEnabled).To(Equal(false))
|
||||
Expect(info.DNSDomainName).To(Equal("bosh"))
|
||||
Expect(info.CompiledPackageCacheEnabled).To(Equal(true))
|
||||
Expect(info.CompiledPackageCacheProvider).To(Equal("local"))
|
||||
Expect(info.SnapshotsEnabled).To(Equal(false))
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
})
|
||||
|
||||
func createDirectorRepo(reqs ...testhelpers.TestRequest) (ts *httptest.Server, handler *testhelpers.TestHandler, repo api.DirectorRepository) {
|
||||
ts, handler = testhelpers.NewTLSServer(reqs)
|
||||
config := &models.Director{
|
||||
TargetURL: ts.URL,
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
}
|
||||
gateway := net.NewDirectorGateway()
|
||||
repo = api.NewBoshDirectorRepository(config, gateway)
|
||||
return
|
||||
}
|
41
vendor/github.com/cloudfoundry-community/gogobosh/api/director_list_deployment_vms.go
generated
vendored
41
vendor/github.com/cloudfoundry-community/gogobosh/api/director_list_deployment_vms.go
generated
vendored
|
@ -1,41 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
)
|
||||
|
||||
func (repo BoshDirectorRepository) ListDeploymentVMs(deploymentName string) (deploymentVMs []models.DeploymentVM, apiResponse net.ApiResponse) {
|
||||
resources := []deploymentVMResponse{}
|
||||
|
||||
path := fmt.Sprintf("/deployments/%s/vms", deploymentName)
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &resources)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
deploymentVMs = append(deploymentVMs, resource.ToModel())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type deploymentVMResponse struct {
|
||||
JobName string `json:"job"`
|
||||
Index int `json:"index"`
|
||||
VMCid string `json:"cid"`
|
||||
AgentID string `json:"agent_id"`
|
||||
}
|
||||
|
||||
func (resource deploymentVMResponse) ToModel() (vm models.DeploymentVM) {
|
||||
vm = models.DeploymentVM{}
|
||||
vm.JobName = resource.JobName
|
||||
vm.Index = resource.Index
|
||||
vm.VMCid = resource.VMCid
|
||||
vm.AgentID = resource.AgentID
|
||||
|
||||
return
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var _ = Describe("simple list of vms", func() {
|
||||
It("GET /deployments/$name/vms to return []models.DeploymentVM{}", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/deployments/cf-warden/vms",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `[
|
||||
{
|
||||
"agent_id": "b11f259c-79dd-4d6d-8aa5-5969d569a2a6",
|
||||
"cid": "vm-8a03a314-6f16-45f6-a377-1a36e763ee45",
|
||||
"job": "ha_proxy_z1",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"agent_id": "5c7708c9-1631-48b3-9833-6b7d0f6c6cd4",
|
||||
"cid": "vm-37926289-487d-4ee9-b556-9684350d1d14",
|
||||
"job": "login_z1",
|
||||
"index": 0
|
||||
}
|
||||
]`}})
|
||||
ts, handler, repo := createDirectorRepo(request)
|
||||
defer ts.Close()
|
||||
|
||||
deploymentVMs, apiResponse := repo.ListDeploymentVMs("cf-warden")
|
||||
|
||||
Expect(len(deploymentVMs)).To(Equal(2))
|
||||
|
||||
vm := deploymentVMs[0]
|
||||
Expect(vm.JobName).To(Equal("ha_proxy_z1"))
|
||||
Expect(vm.Index).To(Equal(0))
|
||||
Expect(vm.AgentID).To(Equal("b11f259c-79dd-4d6d-8aa5-5969d569a2a6"))
|
||||
Expect(vm.VMCid).To(Equal("vm-8a03a314-6f16-45f6-a377-1a36e763ee45"))
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
})
|
|
@ -1,126 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
)
|
||||
|
||||
// GetReleases returns the list of releases, and versions available
|
||||
func (repo BoshDirectorRepository) GetReleases() (releases models.Releases, apiResponse net.ApiResponse) {
|
||||
response := []releaseResponse{}
|
||||
|
||||
path := "/releases"
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &response)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
list := []*models.Release{}
|
||||
for _, resource := range response {
|
||||
list = append(list, resource.ToModel())
|
||||
}
|
||||
releases = models.Releases(list)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteReleases deletes all versions of a release from the BOSH director
|
||||
func (repo BoshDirectorRepository) DeleteReleases(name string) (apiResponse net.ApiResponse) {
|
||||
path := fmt.Sprintf("/releases/%s?force=true", name)
|
||||
apiResponse = repo.gateway.DeleteResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
if !apiResponse.IsRedirection() {
|
||||
return
|
||||
}
|
||||
|
||||
var taskStatus models.TaskStatus
|
||||
taskURL, err := url.Parse(apiResponse.RedirectLocation)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+taskURL.Path, repo.config.Username, repo.config.Password, &taskStatus)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
/* Progression should be: queued, progressing, done */
|
||||
/* TODO task might fail; end states: done, error, cancelled */
|
||||
for taskStatus.State != "done" {
|
||||
time.Sleep(1)
|
||||
taskStatus, apiResponse = repo.GetTaskStatus(taskStatus.ID)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteRelease deletes a specific version of a release from the BOSH director
|
||||
func (repo BoshDirectorRepository) DeleteRelease(name string, version string) (apiResponse net.ApiResponse) {
|
||||
path := fmt.Sprintf("/releases/%s?force=true&version=%s", name, version)
|
||||
apiResponse = repo.gateway.DeleteResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
if !apiResponse.IsRedirection() {
|
||||
return
|
||||
}
|
||||
|
||||
var taskStatus models.TaskStatus
|
||||
taskURL, err := url.Parse(apiResponse.RedirectLocation)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+taskURL.Path, repo.config.Username, repo.config.Password, &taskStatus)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
/* Progression should be: queued, progressing, done */
|
||||
/* TODO task might fail; end states: done, error, cancelled */
|
||||
for taskStatus.State != "done" {
|
||||
time.Sleep(1)
|
||||
taskStatus, apiResponse = repo.GetTaskStatus(taskStatus.ID)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type releaseResponse struct {
|
||||
Name string `json:"name"`
|
||||
Versions []releaseVersionResponse `json:"release_versions"`
|
||||
}
|
||||
|
||||
type releaseVersionResponse struct {
|
||||
Version string `json:"version"`
|
||||
CommitHash string `json:"commit_hash"`
|
||||
UncommittedChanges bool `json:"uncommitted_changes"`
|
||||
CurrentlyDeployed bool `json:"currently_deployed"`
|
||||
}
|
||||
|
||||
func (resource releaseResponse) ToModel() (release *models.Release) {
|
||||
release = &models.Release{}
|
||||
release.Name = resource.Name
|
||||
for _, versionResponse := range resource.Versions {
|
||||
version := models.ReleaseVersion{}
|
||||
version.Version = versionResponse.Version
|
||||
version.CommitHash = versionResponse.CommitHash
|
||||
version.UncommittedChanges = versionResponse.UncommittedChanges
|
||||
version.CurrentlyDeployed = versionResponse.CurrentlyDeployed
|
||||
|
||||
release.Versions = append(release.Versions, version)
|
||||
}
|
||||
return
|
||||
}
|
118
vendor/github.com/cloudfoundry-community/gogobosh/api/director_releases_test.go
generated
vendored
118
vendor/github.com/cloudfoundry-community/gogobosh/api/director_releases_test.go
generated
vendored
|
@ -1,118 +0,0 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var _ = Describe("get list of releases", func() {
|
||||
It("GET /releases to return []DirectorRelease{}", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/releases",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `[
|
||||
{
|
||||
"name": "cf",
|
||||
"release_versions": [
|
||||
{
|
||||
"version": "153",
|
||||
"commit_hash": "009fdd9a",
|
||||
"uncommitted_changes": true,
|
||||
"currently_deployed": true,
|
||||
"job_names": [
|
||||
"cloud_controller_ng",
|
||||
"nats",
|
||||
"dea_next",
|
||||
"login",
|
||||
"health_manager_next",
|
||||
"uaa",
|
||||
"debian_nfs_server",
|
||||
"loggregator",
|
||||
"postgres",
|
||||
"dea_logging_agent",
|
||||
"syslog_aggregator",
|
||||
"narc",
|
||||
"haproxy",
|
||||
"hm9000",
|
||||
"saml_login",
|
||||
"nats_stream_forwarder",
|
||||
"collector",
|
||||
"pivotal_login",
|
||||
"loggregator_trafficcontroller",
|
||||
"etcd",
|
||||
"gorouter"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`}})
|
||||
ts, handler, repo := createDirectorRepo(request)
|
||||
defer ts.Close()
|
||||
|
||||
releases, apiResponse := repo.GetReleases()
|
||||
|
||||
release := releases[0]
|
||||
Expect(release.Name).To(Equal("cf"))
|
||||
|
||||
releaseVersion := release.Versions[0]
|
||||
Expect(releaseVersion.Version).To(Equal("153"))
|
||||
Expect(releaseVersion.CommitHash).To(Equal("009fdd9a"))
|
||||
Expect(releaseVersion.UncommittedChanges).To(Equal(true))
|
||||
Expect(releaseVersion.CurrentlyDeployed).To(Equal(true))
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
|
||||
It("DeleteReleases(name)", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "DELETE",
|
||||
Path: "/releases/cf?force=true",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusFound,
|
||||
Header: http.Header{
|
||||
"Location": {"https://some.host/tasks/25"},
|
||||
},
|
||||
}})
|
||||
ts, handler, repo := createDirectorRepo(
|
||||
request,
|
||||
taskTestRequest(25, "queued"),
|
||||
taskTestRequest(25, "processing"),
|
||||
taskTestRequest(25, "done"),
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
apiResponse := repo.DeleteReleases("cf")
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
|
||||
It("DeleteRelease(name, version)", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "DELETE",
|
||||
Path: "/releases/cf?force=true&version=144",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusFound,
|
||||
Header: http.Header{
|
||||
"Location": {"https://some.host/tasks/26"},
|
||||
},
|
||||
}})
|
||||
ts, handler, repo := createDirectorRepo(
|
||||
request,
|
||||
taskTestRequest(26, "queued"),
|
||||
taskTestRequest(26, "processing"),
|
||||
taskTestRequest(26, "done"),
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
apiResponse := repo.DeleteRelease("cf", "144")
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
})
|
|
@ -1,40 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
)
|
||||
|
||||
// DirectorRepository is the interface for accessing a BOSH director
|
||||
type DirectorRepository interface {
|
||||
GetInfo() (directorInfo models.DirectorInfo, apiResponse net.ApiResponse)
|
||||
|
||||
GetStemcells() (stemcells models.Stemcells, apiResponse net.ApiResponse)
|
||||
DeleteStemcell(name string, version string) (apiResponse net.ApiResponse)
|
||||
|
||||
GetReleases() (releases models.Releases, apiResponse net.ApiResponse)
|
||||
DeleteReleases(name string) (apiResponse net.ApiResponse)
|
||||
DeleteRelease(name string, version string) (apiResponse net.ApiResponse)
|
||||
|
||||
GetDeployments() (deployments models.Deployments, apiResponse net.ApiResponse)
|
||||
GetDeploymentManifest(deploymentName string) (manifest *models.DeploymentManifest, apiResponse net.ApiResponse)
|
||||
DeleteDeployment(deploymentName string) (apiResponse net.ApiResponse)
|
||||
ListDeploymentVMs(deploymentName string) (deploymentVMs []models.DeploymentVM, apiResponse net.ApiResponse)
|
||||
FetchVMsStatus(deploymentName string) (vmsStatus []models.VMStatus, apiResponse net.ApiResponse)
|
||||
|
||||
GetTaskStatuses() (task []models.TaskStatus, apiResponse net.ApiResponse)
|
||||
GetTaskStatus(taskID int) (task models.TaskStatus, apiResponse net.ApiResponse)
|
||||
}
|
||||
|
||||
// BoshDirectorRepository represents a Director
|
||||
type BoshDirectorRepository struct {
|
||||
config *models.Director
|
||||
gateway net.Gateway
|
||||
}
|
||||
|
||||
// NewBoshDirectorRepository is a constructor for a BoshDirectorRepository
|
||||
func NewBoshDirectorRepository(config *models.Director, gateway net.Gateway) (repo BoshDirectorRepository) {
|
||||
repo.config = config
|
||||
repo.gateway = gateway
|
||||
return
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
)
|
||||
|
||||
// GetStemcells returns the list of stemcells & versions available
|
||||
func (repo BoshDirectorRepository) GetStemcells() (stemcells models.Stemcells, apiResponse net.ApiResponse) {
|
||||
response := []stemcellResponse{}
|
||||
|
||||
path := "/stemcells"
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &response)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
list := []*models.Stemcell{}
|
||||
for _, resource := range response {
|
||||
list = append(list, resource.ToModel())
|
||||
}
|
||||
stemcells = models.Stemcells(list)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteStemcell deletes a specific stemcell version
|
||||
func (repo BoshDirectorRepository) DeleteStemcell(name string, version string) (apiResponse net.ApiResponse) {
|
||||
path := fmt.Sprintf("/stemcells/%s/%s?force=true", name, version)
|
||||
apiResponse = repo.gateway.DeleteResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
if !apiResponse.IsRedirection() {
|
||||
return
|
||||
}
|
||||
|
||||
var taskStatus models.TaskStatus
|
||||
taskURL, err := url.Parse(apiResponse.RedirectLocation)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+taskURL.Path, repo.config.Username, repo.config.Password, &taskStatus)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
/* Progression should be: queued, progressing, done */
|
||||
/* TODO task might fail; end states: done, error, cancelled */
|
||||
for taskStatus.State != "done" {
|
||||
time.Sleep(1)
|
||||
taskStatus, apiResponse = repo.GetTaskStatus(taskStatus.ID)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type stemcellResponse struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Cid string `json:"cid"`
|
||||
}
|
||||
|
||||
func (resource stemcellResponse) ToModel() (stemcell *models.Stemcell) {
|
||||
stemcell = &models.Stemcell{}
|
||||
stemcell.Name = resource.Name
|
||||
stemcell.Version = resource.Version
|
||||
stemcell.Cid = resource.Cid
|
||||
|
||||
return
|
||||
}
|
72
vendor/github.com/cloudfoundry-community/gogobosh/api/director_stemcells_test.go
generated
vendored
72
vendor/github.com/cloudfoundry-community/gogobosh/api/director_stemcells_test.go
generated
vendored
|
@ -1,72 +0,0 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var _ = Describe("get list of stemcells", func() {
|
||||
It("GET /stemcells to return []DirectorStemcell{}", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/stemcells",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `[
|
||||
{
|
||||
"name": "bosh-stemcell",
|
||||
"version": "993",
|
||||
"cid": "stemcell-6e6b9689-8b03-42cd-a6de-7784e3c421ec",
|
||||
"deployments": [
|
||||
"#<Bosh::Director::Models::Deployment:0x0000000474bdb0>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bosh-warden-boshlite-ubuntu",
|
||||
"version": "24",
|
||||
"cid": "stemcell-6936d497-b8cd-4e12-af0a-5f2151834a1a",
|
||||
"deployments": [
|
||||
|
||||
]
|
||||
}
|
||||
]`}})
|
||||
ts, handler, repo := createDirectorRepo(request)
|
||||
defer ts.Close()
|
||||
|
||||
stemcells, apiResponse := repo.GetStemcells()
|
||||
stemcell := stemcells[0]
|
||||
|
||||
Expect(stemcell.Name).To(Equal("bosh-stemcell"))
|
||||
Expect(stemcell.Version).To(Equal("993"))
|
||||
Expect(stemcell.Cid).To(Equal("stemcell-6e6b9689-8b03-42cd-a6de-7784e3c421ec"))
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
|
||||
It("DeleteStemcell(name, version)", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "DELETE",
|
||||
Path: "/stemcells/bosh-stemcell/993?force=true",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusFound,
|
||||
Header: http.Header{
|
||||
"Location": {"https://some.host/tasks/24"},
|
||||
},
|
||||
}})
|
||||
ts, handler, repo := createDirectorRepo(
|
||||
request,
|
||||
taskTestRequest(24, "queued"),
|
||||
taskTestRequest(24, "processing"),
|
||||
taskTestRequest(24, "done"),
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
apiResponse := repo.DeleteStemcell("bosh-stemcell", "993")
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
})
|
|
@ -1,79 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
)
|
||||
|
||||
// GetTaskStatuses returns a list of most recent task statuses
|
||||
func (repo BoshDirectorRepository) GetTaskStatuses() (tasks []models.TaskStatus, apiResponse net.ApiResponse) {
|
||||
taskResponses := []taskStatusResponse{}
|
||||
|
||||
path := fmt.Sprintf("/tasks")
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &taskResponses)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, resource := range taskResponses {
|
||||
tasks = append(tasks, resource.ToModel())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetTaskStatusesWithLimit returns a max of 'limit' task statuses
|
||||
func (repo BoshDirectorRepository) GetTaskStatusesWithLimit(limit int) (tasks []models.TaskStatus, apiResponse net.ApiResponse) {
|
||||
taskResponses := []taskStatusResponse{}
|
||||
|
||||
path := fmt.Sprintf("/tasks?limit=%d", limit)
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &taskResponses)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, resource := range taskResponses {
|
||||
tasks = append(tasks, resource.ToModel())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetTaskStatus returns details of a specific task to allow polling for state change
|
||||
func (repo BoshDirectorRepository) GetTaskStatus(taskID int) (task models.TaskStatus, apiResponse net.ApiResponse) {
|
||||
taskResponse := taskStatusResponse{}
|
||||
|
||||
path := fmt.Sprintf("/tasks/%d", taskID)
|
||||
apiResponse = repo.gateway.GetResource(repo.config.TargetURL+path, repo.config.Username, repo.config.Password, &taskResponse)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
task = taskResponse.ToModel()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type taskStatusResponse struct {
|
||||
ID int `json:"id"`
|
||||
State string `json:"state"`
|
||||
Description string `json:"description"`
|
||||
TimeStamp int `json:"timestamp"`
|
||||
Result string `json:"result"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
func (resource taskStatusResponse) ToModel() (task models.TaskStatus) {
|
||||
task = models.TaskStatus{}
|
||||
|
||||
task.ID = resource.ID
|
||||
task.State = resource.State
|
||||
task.Description = resource.Description
|
||||
task.TimeStamp = resource.TimeStamp
|
||||
task.Result = resource.Result
|
||||
task.User = resource.User
|
||||
|
||||
return
|
||||
}
|
92
vendor/github.com/cloudfoundry-community/gogobosh/api/director_task_status_test.go
generated
vendored
92
vendor/github.com/cloudfoundry-community/gogobosh/api/director_task_status_test.go
generated
vendored
|
@ -1,92 +0,0 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var _ = Describe("models.TaskStatus", func() {
|
||||
It("GetTaskStatus returns models.TaskStatus{}", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/tasks/1",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `{
|
||||
"id": 1,
|
||||
"state": "done",
|
||||
"description": "create release",
|
||||
"timestamp": 1390068518,
|
||||
"result": "Created release cf/153",
|
||||
"user": "admin"
|
||||
}`}})
|
||||
ts, handler, repo := createDirectorRepo(request)
|
||||
defer ts.Close()
|
||||
|
||||
task, apiResponse := repo.GetTaskStatus(1)
|
||||
|
||||
Expect(task.ID).To(Equal(1))
|
||||
Expect(task.State).To(Equal("done"))
|
||||
Expect(task.Description).To(Equal("create release"))
|
||||
Expect(task.TimeStamp).To(Equal(1390068518))
|
||||
Expect(task.Result).To(Equal("Created release cf/153"))
|
||||
Expect(task.User).To(Equal("admin"))
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
|
||||
It("() returns []models.TaskStatus{}", func() {
|
||||
request := testhelpers.NewDirectorTestRequest(testhelpers.TestRequest{
|
||||
Method: "GET",
|
||||
Path: "/tasks",
|
||||
Response: testhelpers.TestResponse{
|
||||
Status: http.StatusOK,
|
||||
Body: `[{
|
||||
"id": 2,
|
||||
"state": "done",
|
||||
"description": "create release",
|
||||
"timestamp": 1390068525,
|
||||
"result": "Created release 'etcd/3'",
|
||||
"user": "admin"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"state": "done",
|
||||
"description": "create release",
|
||||
"timestamp": 1390068518,
|
||||
"result": "Created release 'cf/153'",
|
||||
"user": "admin"
|
||||
}
|
||||
]`}})
|
||||
ts, handler, repo := createDirectorRepo(request)
|
||||
defer ts.Close()
|
||||
|
||||
tasks, apiResponse := repo.GetTaskStatuses()
|
||||
|
||||
Expect(len(tasks)).To(Equal(2))
|
||||
|
||||
task := tasks[1]
|
||||
Expect(task.ID).To(Equal(1))
|
||||
Expect(task.State).To(Equal("done"))
|
||||
Expect(task.Description).To(Equal("create release"))
|
||||
Expect(task.TimeStamp).To(Equal(1390068518))
|
||||
Expect(task.Result).To(Equal("Created release 'cf/153'"))
|
||||
Expect(task.User).To(Equal("admin"))
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(handler.AllRequestsCalled()).To(Equal(true))
|
||||
})
|
||||
|
||||
// verbose: true/false - show internal tasks
|
||||
|
||||
// limit: nil or integer limit
|
||||
|
||||
// states: all, processing,cancelling,queued ("running"), or specific list
|
||||
XIt("GetRunningTaskStatuses", func() {
|
||||
// states: processing,cancelling,queued
|
||||
})
|
||||
|
||||
})
|
|
@ -1,2 +0,0 @@
|
|||
// Package api implements the BOSH API client functions
|
||||
package api
|
|
@ -1,12 +0,0 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "GoGoBOSH API suite")
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package constants
|
||||
|
||||
const (
|
||||
Version = "0.1.0"
|
||||
)
|
|
@ -1,2 +0,0 @@
|
|||
// Package gogobosh is a client library for applications wanting to talk to a Cloud Foundry BOSH/MicroBOSH or bosh-lite.
|
||||
package gogobosh
|
124
vendor/github.com/cloudfoundry-community/gogobosh/example/bosh-lite-example.go
generated
vendored
124
vendor/github.com/cloudfoundry-community/gogobosh/example/bosh-lite-example.go
generated
vendored
|
@ -1,124 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh"
|
||||
"github.com/cloudfoundry-community/gogobosh/api"
|
||||
"github.com/cloudfoundry-community/gogobosh/net"
|
||||
"github.com/cloudfoundry-community/gogobosh/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
utils.Logger = utils.NewLogger()
|
||||
|
||||
target := flag.String("target", "https://192.168.50.4:25555", "BOSH director host")
|
||||
username := flag.String("username", "admin", "Login with username")
|
||||
password := flag.String("password", "admin", "Login with password")
|
||||
flag.Parse()
|
||||
|
||||
director := gogobosh.NewDirector(*target, *username, *password)
|
||||
repo := api.NewBoshDirectorRepository(&director, net.NewDirectorGateway())
|
||||
|
||||
info, apiResponse := repo.GetInfo()
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch BOSH info")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Director")
|
||||
fmt.Printf(" Name %s\n", info.Name)
|
||||
fmt.Printf(" URL %s\n", info.URL)
|
||||
fmt.Printf(" Version %s\n", info.Version)
|
||||
fmt.Printf(" User %s\n", info.User)
|
||||
fmt.Printf(" UUID %s\n", info.UUID)
|
||||
fmt.Printf(" CPI %s\n", info.CPI)
|
||||
if info.DNSEnabled {
|
||||
fmt.Printf(" dns %#v (%s)\n", info.DNSEnabled, info.DNSDomainName)
|
||||
} else {
|
||||
fmt.Printf(" dns %#v\n", info.DNSEnabled)
|
||||
}
|
||||
if info.CompiledPackageCacheEnabled {
|
||||
fmt.Printf(" compiled_package_cache %#v (provider: %s)\n", info.CompiledPackageCacheEnabled, info.CompiledPackageCacheProvider)
|
||||
} else {
|
||||
fmt.Printf(" compiled_package_cache %#v\n", info.CompiledPackageCacheEnabled)
|
||||
}
|
||||
fmt.Printf(" snapshots %#v\n", info.SnapshotsEnabled)
|
||||
fmt.Println("")
|
||||
fmt.Printf("%#v\n", info)
|
||||
fmt.Println("")
|
||||
|
||||
stemcells, apiResponse := repo.GetStemcells()
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch BOSH stemcells")
|
||||
return
|
||||
} else {
|
||||
for _, stemcell := range stemcells {
|
||||
fmt.Printf("%#v\n", stemcell)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
releases, apiResponse := repo.GetReleases()
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch BOSH releases")
|
||||
return
|
||||
} else {
|
||||
for _, release := range releases {
|
||||
fmt.Printf("%#v\n", release)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
deployments, apiResponse := repo.GetDeployments()
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch BOSH deployments")
|
||||
return
|
||||
} else {
|
||||
for _, deployment := range deployments {
|
||||
fmt.Printf("%#v\n", deployment)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
tasks, apiResponse := repo.GetTaskStatusesWithLimit(3)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch tasks")
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("%#v\n", tasks)
|
||||
}
|
||||
|
||||
task, apiResponse := repo.GetTaskStatus(tasks[0].ID)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch BOSH task 1")
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("%#v\n", task)
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Println("VMs in cf-warden deployment:")
|
||||
vms, apiResponse := repo.ListDeploymentVMs("cf-warden")
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not get list of VM for cf-warden")
|
||||
return
|
||||
} else {
|
||||
for _, vm := range vms {
|
||||
fmt.Printf("%#v\n", vm)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Println("VMs status in cf-warden deployment:")
|
||||
vmsStatuses, apiResponse := repo.FetchVMsStatus("cf-warden")
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
fmt.Println("Could not fetch VMs status for cf-warden")
|
||||
return
|
||||
} else {
|
||||
for _, vmStatus := range vmsStatuses {
|
||||
fmt.Printf("%s/%d is %s, IPs %#v\n", vmStatus.JobName, vmStatus.Index, vmStatus.JobState, vmStatus.IPs)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package gogobosh
|
||||
|
||||
import "github.com/cloudfoundry-community/gogobosh/models"
|
||||
|
||||
// NewDirector constructs a Director
|
||||
func NewDirector(targetURL string, username string, password string) (director models.Director) {
|
||||
director = models.Director{}
|
||||
director.TargetURL = targetURL
|
||||
director.Username = username
|
||||
director.Password = password
|
||||
return
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package gogobosh_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGoGoBosh(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "GoGoBOSH suite")
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package gogobosh_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
)
|
||||
|
||||
var _ = Describe("GoGoBOSH", func() {
|
||||
It("parse response", func() {
|
||||
})
|
||||
})
|
|
@ -1,65 +0,0 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"launchpad.net/goyaml"
|
||||
)
|
||||
|
||||
// BoshConfig describes a local ~/.bosh_config file
|
||||
// See testhelpers/fixtures/bosh_config.yml
|
||||
type BoshConfig struct {
|
||||
Target string
|
||||
Name string `yaml:"target_name"`
|
||||
Version string `yaml:"target_version"`
|
||||
UUID string `yaml:"target_uuid"`
|
||||
Aliases map[string]map[string]string
|
||||
Authentication map[string]*authentication `yaml:"auth"`
|
||||
Deployments map[string]string `yaml:"deployment"`
|
||||
}
|
||||
|
||||
type authentication struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// LoadBoshConfig loads and unmarshals ~/.bosh_config
|
||||
func LoadBoshConfig(configPath string) (config *BoshConfig, err error) {
|
||||
config = &BoshConfig{}
|
||||
|
||||
contents, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
goyaml.Unmarshal(contents, config)
|
||||
return
|
||||
}
|
||||
|
||||
// DefaultBoshConfigPath returns the path to ~/.bosh_config
|
||||
func DefaultBoshConfigPath() (configPath string, err error) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Abs(usr.HomeDir + "/.bosh_config")
|
||||
}
|
||||
|
||||
// CurrentBoshTarget returns the connection information for local user's current target BOSH
|
||||
func (config *BoshConfig) CurrentBoshTarget() (target, username, password string, err error) {
|
||||
if config.Target == "" {
|
||||
return "", "", "", errors.New("Please target a BOSH first. Run 'bosh target DIRECTOR_IP'.")
|
||||
}
|
||||
auth := config.Authentication[config.Target]
|
||||
if auth == nil {
|
||||
return "", "", "", errors.New("Current target has not been authenticated yet. Run 'bosh login'.")
|
||||
}
|
||||
return config.Target, auth.Username, auth.Password, nil
|
||||
}
|
||||
|
||||
// CurrentDeploymentManifest returns the path to the deployment manifest for the currently target BOSH
|
||||
func (config *BoshConfig) CurrentDeploymentManifest() (manifestPath string) {
|
||||
return config.Deployments[config.Target]
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package local_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudfoundry-community/gogobosh/local"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Local config", func() {
|
||||
It("Loads BOSH config", func() {
|
||||
configPath, err := filepath.Abs("../testhelpers/fixtures/bosh_config.yml")
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
config, err := local.LoadBoshConfig(configPath)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(config).ToNot(BeNil())
|
||||
Expect(config.Name).To(Equal("Bosh Lite Director"))
|
||||
Expect(config.Authentication["https://192.168.50.4:25555"].Username).To(Equal("admin"))
|
||||
})
|
||||
|
||||
It("CurrentBoshTarget", func() {
|
||||
configPath, err := filepath.Abs("../testhelpers/fixtures/bosh_config.yml")
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
config, err := local.LoadBoshConfig(configPath)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(config).ToNot(BeNil())
|
||||
|
||||
target, username, password, err := config.CurrentBoshTarget()
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(target).To(Equal("https://192.168.50.4:25555"))
|
||||
Expect(username).To(Equal("admin"))
|
||||
Expect(password).To(Equal("password"))
|
||||
})
|
||||
|
||||
It("CurrentBoshDeployment", func() {
|
||||
configPath, err := filepath.Abs("../testhelpers/fixtures/bosh_config.yml")
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
config, err := local.LoadBoshConfig(configPath)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(config).ToNot(BeNil())
|
||||
|
||||
manifestPath := config.CurrentDeploymentManifest()
|
||||
Expect(manifestPath).To(Equal("path/to/manifest.yml"))
|
||||
})
|
||||
})
|
|
@ -1,13 +0,0 @@
|
|||
package local_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "GoGoBOSH Local suite")
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package models
|
||||
|
||||
// Deployments is a collection of deployments in the Director
|
||||
type Deployments []*Deployment
|
||||
|
||||
// Deployment describes a running BOSH deployment and the
|
||||
// Releases and Stemcells it is using.
|
||||
type Deployment struct {
|
||||
Name string
|
||||
Releases []NameVersion
|
||||
Stemcells []NameVersion
|
||||
}
|
||||
|
||||
// DeploymentVM describes the association of a running server
|
||||
// within a Deployment
|
||||
type DeploymentVM struct {
|
||||
JobName string
|
||||
Index int
|
||||
VMCid string
|
||||
AgentID string
|
||||
}
|
||||
|
||||
// FindByRelease returns a list of deployments that use a release
|
||||
func (deployments Deployments) FindByRelease(releaseName string) Deployments {
|
||||
subset := []*Deployment{}
|
||||
for _, deployment := range deployments {
|
||||
for _, release := range deployment.Releases {
|
||||
if release.Name == releaseName {
|
||||
subset = append(subset, deployment)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Deployments(subset)
|
||||
}
|
87
vendor/github.com/cloudfoundry-community/gogobosh/models/deployment_manifest.go
generated
vendored
87
vendor/github.com/cloudfoundry-community/gogobosh/models/deployment_manifest.go
generated
vendored
|
@ -1,87 +0,0 @@
|
|||
package models
|
||||
|
||||
// DeploymentManifest describes all the configuration for any BOSH deployment
|
||||
type DeploymentManifest struct {
|
||||
Meta map[string]interface{} `yaml:"meta,omitempty"`
|
||||
Name string
|
||||
DirectorUUID string `yaml:"director_uuid"`
|
||||
Releases []*NameVersion
|
||||
Compilation *manifestCompilation
|
||||
Update *manifestUpdate
|
||||
Networks []*manifestNetwork
|
||||
ResourcePools []*manifestResourcePool `yaml:"resource_pools"`
|
||||
Jobs []*ManifestJob
|
||||
Properties *map[string]interface{} `yaml:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type manifestCompilation struct {
|
||||
Workers int `yaml:"workers"`
|
||||
NetworkName string `yaml:"network"`
|
||||
ReuseCompilationVMs bool `yaml:"reuse_compilation_vms"`
|
||||
CloudProperties *map[string]interface{} `yaml:"cloud_properties"`
|
||||
}
|
||||
|
||||
type manifestUpdate struct {
|
||||
Canaries int
|
||||
MaxInFlight int `yaml:"max_in_flight"`
|
||||
CanaryWatchTime string `yaml:"canary_watch_time"`
|
||||
UpdateWatchTime string `yaml:"update_watch_time"`
|
||||
Serial bool
|
||||
}
|
||||
|
||||
type manifestNetwork struct {
|
||||
Name string
|
||||
Type string
|
||||
CloudProperties *map[string]interface{} `yaml:"cloud_properties"`
|
||||
Subnets interface{}
|
||||
}
|
||||
|
||||
type manifestResourcePool struct {
|
||||
Name string
|
||||
NetworkName string `yaml:"network"`
|
||||
Stemcell *manifestStemcell
|
||||
CloudProperties *map[string]interface{} `yaml:"cloud_properties"`
|
||||
}
|
||||
|
||||
// ManifestJob describes a cluster of VMs each running the same set of job templates
|
||||
type ManifestJob struct {
|
||||
Name string
|
||||
JobTemplates []*ManifestJobTemplate `yaml:"templates"`
|
||||
Instances int `yaml:"instances"`
|
||||
ResourcePoolName string `yaml:"resource_pool"`
|
||||
PersistentDisk int `yaml:"persistent_disk,omitempty"`
|
||||
Lifecycle string `yaml:"lifecycle,omitempty"`
|
||||
Update *manifestUpdate `yaml:"update,omitempty"`
|
||||
Networks []*manifestJobNetwork
|
||||
Properties *map[string]interface{} `yaml:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// ManifestJobTemplate describes a job template included in a ManifestJob
|
||||
type ManifestJobTemplate struct {
|
||||
Name string
|
||||
Release string
|
||||
}
|
||||
|
||||
type manifestJobNetwork struct {
|
||||
Name string
|
||||
Default *[]string `yaml:"default,omitempty"`
|
||||
StaticIPs *[]string `yaml:"static_ips,omitempty"`
|
||||
}
|
||||
|
||||
type manifestStemcell struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
// FindByJobTemplates returns the subnet of ManifestJobs that include a specific job template
|
||||
func (manifest *DeploymentManifest) FindByJobTemplates(jobTemplateName string) (jobs []*ManifestJob) {
|
||||
jobs = []*ManifestJob{}
|
||||
for _, job := range manifest.Jobs {
|
||||
for _, jobTemplate := range job.JobTemplates {
|
||||
if jobTemplate.Name == jobTemplateName {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
22
vendor/github.com/cloudfoundry-community/gogobosh/models/deployment_manifest_test.go
generated
vendored
22
vendor/github.com/cloudfoundry-community/gogobosh/models/deployment_manifest_test.go
generated
vendored
|
@ -1,22 +0,0 @@
|
|||
package models_test
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("DeploymentManifest", func() {
|
||||
It("FindByJobTemplates", func() {
|
||||
manifest := &models.DeploymentManifest{
|
||||
Jobs: []*models.ManifestJob{
|
||||
{Name: "job1", JobTemplates: []*models.ManifestJobTemplate{{Name: "common"}}},
|
||||
{Name: "job2", JobTemplates: []*models.ManifestJobTemplate{{Name: "common"}}},
|
||||
{Name: "other", JobTemplates: []*models.ManifestJobTemplate{{Name: "other"}}},
|
||||
},
|
||||
}
|
||||
jobs := manifest.FindByJobTemplates("common")
|
||||
Expect(len(jobs)).To(Equal(2))
|
||||
})
|
||||
})
|
|
@ -1,31 +0,0 @@
|
|||
package models_test
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/models"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Deployments", func() {
|
||||
It("FindByRelease", func() {
|
||||
manifest := &models.Deployments{
|
||||
{
|
||||
Name: "cf-warden",
|
||||
Releases: []models.NameVersion{
|
||||
{Name: "nagios"},
|
||||
{Name: "cf"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "other",
|
||||
Releases: []models.NameVersion{
|
||||
{Name: "nagios"},
|
||||
{Name: "other"},
|
||||
},
|
||||
},
|
||||
}
|
||||
deployments := manifest.FindByRelease("cf")
|
||||
Expect(len(deployments)).To(Equal(1))
|
||||
})
|
||||
})
|
|
@ -1,2 +0,0 @@
|
|||
// Package models contains the public repsentations of BOSH resources
|
||||
package models
|
|
@ -1,88 +0,0 @@
|
|||
package models
|
||||
|
||||
// Director is a targeted BOSH director and login credentials
|
||||
type Director struct {
|
||||
TargetURL string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// DirectorInfo contains the status of a target Director
|
||||
type DirectorInfo struct {
|
||||
Name string
|
||||
URL string
|
||||
Version string
|
||||
User string
|
||||
UUID string
|
||||
CPI string
|
||||
DNSEnabled bool
|
||||
DNSDomainName string
|
||||
CompiledPackageCacheEnabled bool
|
||||
CompiledPackageCacheProvider string
|
||||
SnapshotsEnabled bool
|
||||
}
|
||||
|
||||
// Stemcells is a collection of stemcell in the Director
|
||||
type Stemcells []*Stemcell
|
||||
|
||||
// Stemcell describes an available versioned stemcell
|
||||
type Stemcell struct {
|
||||
Name string
|
||||
Version string
|
||||
Cid string
|
||||
}
|
||||
|
||||
// Releases is a collection of releases in the Director
|
||||
type Releases []*Release
|
||||
|
||||
// Release describes a release and all available versions
|
||||
type Release struct {
|
||||
Name string
|
||||
Versions []ReleaseVersion
|
||||
}
|
||||
|
||||
// ReleaseVersion describes an available versioned release
|
||||
type ReleaseVersion struct {
|
||||
Version string
|
||||
CommitHash string
|
||||
UncommittedChanges bool
|
||||
CurrentlyDeployed bool
|
||||
}
|
||||
|
||||
// NameVersion is a reusable structure for Name/Version information
|
||||
type NameVersion struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
// TaskStatus summarizes the current status of a Task
|
||||
type TaskStatus struct {
|
||||
ID int
|
||||
State string
|
||||
Description string
|
||||
TimeStamp int
|
||||
Result string
|
||||
User string
|
||||
}
|
||||
|
||||
// VMStatus summarizes the current status of a VM/server
|
||||
// within a running deployment
|
||||
type VMStatus struct {
|
||||
JobName string
|
||||
Index int
|
||||
JobState string
|
||||
VMCid string
|
||||
AgentID string
|
||||
ResourcePool string
|
||||
ResurrectionPaused bool
|
||||
IPs []string
|
||||
DNSs []string
|
||||
CPUUser float64
|
||||
CPUSys float64
|
||||
CPUWait float64
|
||||
MemoryPercent float64
|
||||
MemoryKb int
|
||||
SwapPercent float64
|
||||
SwapKb int
|
||||
DiskPersistentPercent float64
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package models_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "GoGoBOSH Models suite")
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package net
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ApiResponse struct {
|
||||
Message string
|
||||
ErrorCode string
|
||||
StatusCode int
|
||||
RedirectLocation string
|
||||
|
||||
isError bool
|
||||
isNotFound bool
|
||||
isRedirection bool
|
||||
}
|
||||
|
||||
func NewApiResponse(message string, errorCode string, statusCode int) (apiResponse ApiResponse) {
|
||||
return ApiResponse{
|
||||
Message: message,
|
||||
ErrorCode: errorCode,
|
||||
StatusCode: statusCode,
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiResponseWithRedirect(location string) (apiResponse ApiResponse) {
|
||||
return ApiResponse{
|
||||
StatusCode: http.StatusFound,
|
||||
RedirectLocation: location,
|
||||
isRedirection: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiResponseWithStatusCode(statusCode int) (apiResponse ApiResponse) {
|
||||
return ApiResponse{
|
||||
StatusCode: statusCode,
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiResponseWithMessage(message string, a ...interface{}) (apiResponse ApiResponse) {
|
||||
return ApiResponse{
|
||||
Message: fmt.Sprintf(message, a...),
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiResponseWithError(message string, err error) (apiResponse ApiResponse) {
|
||||
return ApiResponse{
|
||||
Message: fmt.Sprintf("%s: %s", message, err.Error()),
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewNotFoundApiResponse(message string, a ...interface{}) (apiResponse ApiResponse) {
|
||||
return ApiResponse{
|
||||
Message: fmt.Sprintf(message, a...),
|
||||
isNotFound: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSuccessfulApiResponse() (apiResponse ApiResponse) {
|
||||
return ApiResponse{}
|
||||
}
|
||||
|
||||
func (apiResponse ApiResponse) IsError() bool {
|
||||
return apiResponse.isError
|
||||
}
|
||||
|
||||
func (apiResponse ApiResponse) IsNotFound() bool {
|
||||
return apiResponse.isNotFound
|
||||
}
|
||||
|
||||
func (apiResponse ApiResponse) IsRedirection() bool {
|
||||
return apiResponse.isRedirection
|
||||
}
|
||||
|
||||
func (apiResponse ApiResponse) IsSuccessful() bool {
|
||||
return !apiResponse.IsNotSuccessful()
|
||||
}
|
||||
|
||||
func (apiResponse ApiResponse) IsNotSuccessful() bool {
|
||||
return apiResponse.IsError() || apiResponse.IsNotFound()
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package net
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func NewDirectorGateway() Gateway {
|
||||
invalidTokenCode := "1000"
|
||||
|
||||
type ccErrorResponse struct {
|
||||
Code int
|
||||
Description string
|
||||
}
|
||||
|
||||
errorHandler := func(response *http.Response) errorResponse {
|
||||
jsonBytes, _ := ioutil.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
|
||||
ccResp := ccErrorResponse{}
|
||||
json.Unmarshal(jsonBytes, &ccResp)
|
||||
|
||||
code := strconv.Itoa(ccResp.Code)
|
||||
if code == invalidTokenCode {
|
||||
code = INVALID_TOKEN_CODE
|
||||
}
|
||||
|
||||
return errorResponse{Code: code, Description: ccResp.Description}
|
||||
}
|
||||
|
||||
gateway := newGateway(errorHandler)
|
||||
return gateway
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
/*
|
||||
Package net includes helper libraries for performing HTTP requests & processing the responses
|
||||
*/
|
||||
package net
|
|
@ -1,207 +0,0 @@
|
|||
package net
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cloudfoundry-community/gogobosh/constants"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
INVALID_TOKEN_CODE = "GATEWAY INVALID TOKEN CODE"
|
||||
JOB_FINISHED = "finished"
|
||||
JOB_FAILED = "failed"
|
||||
DEFAULT_POLLING_THROTTLE = 5 * time.Second
|
||||
)
|
||||
|
||||
type JobEntity struct {
|
||||
Status string
|
||||
}
|
||||
|
||||
type JobResponse struct {
|
||||
Entity JobEntity
|
||||
}
|
||||
|
||||
type AsyncMetadata struct {
|
||||
Url string
|
||||
}
|
||||
|
||||
type AsyncResponse struct {
|
||||
Metadata AsyncMetadata
|
||||
}
|
||||
|
||||
type errorResponse struct {
|
||||
Code string
|
||||
Description string
|
||||
}
|
||||
|
||||
type errorHandler func(*http.Response) errorResponse
|
||||
|
||||
type Request struct {
|
||||
HttpReq *http.Request
|
||||
SeekableBody io.ReadSeeker
|
||||
}
|
||||
|
||||
type Gateway struct {
|
||||
errHandler errorHandler
|
||||
}
|
||||
|
||||
func newGateway(errHandler errorHandler) (gateway Gateway) {
|
||||
gateway.errHandler = errHandler
|
||||
return
|
||||
}
|
||||
|
||||
func (gateway Gateway) GetResource(url, username string, password string, resource interface{}) (apiResponse ApiResponse) {
|
||||
request, apiResponse := gateway.NewRequest("GET", url, username, password, nil)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
_, apiResponse = gateway.PerformRequestForJSONResponse(request, resource)
|
||||
return
|
||||
}
|
||||
|
||||
func (gateway Gateway) CreateResource(url, username string, password string, body io.ReadSeeker) (apiResponse ApiResponse) {
|
||||
return gateway.createUpdateOrDeleteResource("POST", url, username, password, body, nil)
|
||||
}
|
||||
|
||||
func (gateway Gateway) CreateResourceForResponse(url, username string, password string, body io.ReadSeeker, resource interface{}) (apiResponse ApiResponse) {
|
||||
return gateway.createUpdateOrDeleteResource("POST", url, username, password, body, resource)
|
||||
}
|
||||
|
||||
func (gateway Gateway) UpdateResource(url, username string, password string, body io.ReadSeeker) (apiResponse ApiResponse) {
|
||||
return gateway.createUpdateOrDeleteResource("PUT", url, username, password, body, nil)
|
||||
}
|
||||
|
||||
func (gateway Gateway) UpdateResourceForResponse(url, username string, password string, body io.ReadSeeker, resource interface{}) (apiResponse ApiResponse) {
|
||||
return gateway.createUpdateOrDeleteResource("PUT", url, username, password, body, resource)
|
||||
}
|
||||
|
||||
func (gateway Gateway) DeleteResource(url, username string, password string) (apiResponse ApiResponse) {
|
||||
return gateway.createUpdateOrDeleteResource("DELETE", url, username, password, nil, &AsyncResponse{})
|
||||
}
|
||||
|
||||
func (gateway Gateway) createUpdateOrDeleteResource(verb, url, username string, password string, body io.ReadSeeker, resource interface{}) (apiResponse ApiResponse) {
|
||||
request, apiResponse := gateway.NewRequest(verb, url, username, password, body)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
if resource == nil {
|
||||
return gateway.PerformRequest(request)
|
||||
}
|
||||
|
||||
_, apiResponse = gateway.PerformRequestForJSONResponse(request, resource)
|
||||
return
|
||||
}
|
||||
|
||||
func (gateway Gateway) NewRequest(method, path, username string, password string, body io.ReadSeeker) (req *Request, apiResponse ApiResponse) {
|
||||
if body != nil {
|
||||
body.Seek(0, 0)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(method, path, body)
|
||||
if err != nil {
|
||||
apiResponse = NewApiResponseWithError("Error building request", err)
|
||||
return
|
||||
}
|
||||
|
||||
if password != "" {
|
||||
data := []byte(username + ":" + password)
|
||||
auth := base64.StdEncoding.EncodeToString(data)
|
||||
request.Header.Set("Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
request.Header.Set("accept", "application/json")
|
||||
request.Header.Set("content-type", "application/json")
|
||||
request.Header.Set("User-Agent", "gogobosh "+constants.Version+" / "+runtime.GOOS)
|
||||
|
||||
if body != nil {
|
||||
switch v := body.(type) {
|
||||
case *os.File:
|
||||
fileStats, err := v.Stat()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
request.ContentLength = fileStats.Size()
|
||||
}
|
||||
}
|
||||
|
||||
req = &Request{HttpReq: request, SeekableBody: body}
|
||||
return
|
||||
}
|
||||
|
||||
func (gateway Gateway) PerformRequest(request *Request) (apiResponse ApiResponse) {
|
||||
_, apiResponse = gateway.doRequestAndHandlerError(request)
|
||||
return
|
||||
}
|
||||
|
||||
func (gateway Gateway) PerformRequestForResponseBytes(request *Request) (bytes []byte, headers http.Header, apiResponse ApiResponse) {
|
||||
rawResponse, apiResponse := gateway.doRequestAndHandlerError(request)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
bytes, err := ioutil.ReadAll(rawResponse.Body)
|
||||
if err != nil {
|
||||
apiResponse = NewApiResponseWithError("Error reading response", err)
|
||||
}
|
||||
|
||||
headers = rawResponse.Header
|
||||
return
|
||||
}
|
||||
|
||||
func (gateway Gateway) PerformRequestForTextResponse(request *Request) (response string, headers http.Header, apiResponse ApiResponse) {
|
||||
bytes, headers, apiResponse := gateway.PerformRequestForResponseBytes(request)
|
||||
response = string(bytes)
|
||||
return
|
||||
}
|
||||
|
||||
func (gateway Gateway) PerformRequestForJSONResponse(request *Request, response interface{}) (headers http.Header, apiResponse ApiResponse) {
|
||||
bytes, headers, apiResponse := gateway.PerformRequestForResponseBytes(request)
|
||||
if apiResponse.IsNotSuccessful() {
|
||||
return
|
||||
}
|
||||
|
||||
if apiResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err := json.Unmarshal(bytes, &response)
|
||||
if err != nil {
|
||||
apiResponse = NewApiResponseWithError("Invalid JSON response from server", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (gateway Gateway) doRequestAndHandlerError(request *Request) (rawResponse *http.Response, apiResponse ApiResponse) {
|
||||
rawResponse, err := doRequest(request.HttpReq)
|
||||
if err != nil {
|
||||
apiResponse = NewApiResponseWithError("Error performing request", err)
|
||||
return
|
||||
}
|
||||
|
||||
if rawResponse.StatusCode == 302 {
|
||||
/* DELETE requests do not automatically redirect; all others should not return 302 */
|
||||
apiResponse = NewApiResponseWithRedirect(rawResponse.Header.Get("location"))
|
||||
} else if rawResponse.StatusCode > 299 {
|
||||
errorResponse := gateway.errHandler(rawResponse)
|
||||
message := fmt.Sprintf(
|
||||
"Server error, status code: %d, error code: %s, message: %s",
|
||||
rawResponse.StatusCode,
|
||||
errorResponse.Code,
|
||||
errorResponse.Description,
|
||||
)
|
||||
apiResponse = NewApiResponse(message, errorResponse.Code, rawResponse.StatusCode)
|
||||
} else {
|
||||
apiResponse = NewApiResponseWithStatusCode(rawResponse.StatusCode)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package net
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry-community/gogobosh/constants"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var _ = Describe("Gateway", func() {
|
||||
It("NewRequest successfully", func() {
|
||||
gateway := NewDirectorGateway()
|
||||
|
||||
request, apiResponse := gateway.NewRequest("GET", "https://example.com/v2/apps", "admin", "admin", nil)
|
||||
|
||||
Expect(apiResponse.IsSuccessful()).To(Equal(true))
|
||||
Expect(request.HttpReq.Header.Get("Authorization")).To(Equal("Basic YWRtaW46YWRtaW4="))
|
||||
Expect(request.HttpReq.Header.Get("accept")).To(Equal("application/json"))
|
||||
Expect(request.HttpReq.Header.Get("User-Agent")).To(Equal("gogobosh " + constants.Version + " / " + runtime.GOOS))
|
||||
})
|
||||
})
|
|
@ -1,101 +0,0 @@
|
|||
package net
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudfoundry-community/gogobosh/utils"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
PRIVATE_DATA_PLACEHOLDER = "[PRIVATE DATA HIDDEN]"
|
||||
)
|
||||
|
||||
func newHttpClient() *http.Client {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: tr,
|
||||
CheckRedirect: PrepareRedirect,
|
||||
}
|
||||
}
|
||||
|
||||
func PrepareRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) > 1 {
|
||||
return errors.New("stopped after 1 redirect")
|
||||
}
|
||||
|
||||
prevReq := via[len(via)-1]
|
||||
|
||||
/* Ensure redirect includes original IP:PORT & Authorization */
|
||||
req.URL.Host = prevReq.URL.Host
|
||||
req.Host = prevReq.Host
|
||||
req.Header.Set("Authorization", prevReq.Header.Get("Authorization"))
|
||||
req.Header.Set("Accept", prevReq.Header.Get("Accept"))
|
||||
req.Header.Set("Content-Type", prevReq.Header.Get("Content-Type"))
|
||||
req.Header.Set("User-Agent", prevReq.Header.Get("User-Agent"))
|
||||
|
||||
dumpRequest(req)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Sanitize(input string) (sanitized string) {
|
||||
var sanitizeJson = func(propertyName string, json string) string {
|
||||
re := regexp.MustCompile(fmt.Sprintf(`"%s":"[^"]*"`, propertyName))
|
||||
return re.ReplaceAllString(json, fmt.Sprintf(`"%s":"`+PRIVATE_DATA_PLACEHOLDER+`"`, propertyName))
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`(?m)^Authorization: .*`)
|
||||
sanitized = re.ReplaceAllString(input, "Authorization: "+PRIVATE_DATA_PLACEHOLDER)
|
||||
re = regexp.MustCompile(`password=[^&]*&`)
|
||||
sanitized = re.ReplaceAllString(sanitized, "password="+PRIVATE_DATA_PLACEHOLDER+"&")
|
||||
|
||||
sanitized = sanitizeJson("access_token", sanitized)
|
||||
sanitized = sanitizeJson("refresh_token", sanitized)
|
||||
sanitized = sanitizeJson("token", sanitized)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func doRequest(request *http.Request) (response *http.Response, err error) {
|
||||
httpClient := newHttpClient()
|
||||
|
||||
dumpRequest(request)
|
||||
|
||||
response, err = httpClient.Do(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dumpResponse(response)
|
||||
return
|
||||
}
|
||||
|
||||
func dumpRequest(req *http.Request) {
|
||||
shouldDisplayBody := !strings.Contains(req.Header.Get("Content-Type"), "multipart/form-data")
|
||||
dumpedRequest, err := httputil.DumpRequest(req, shouldDisplayBody)
|
||||
if err != nil {
|
||||
utils.Logger.Printf("Error dumping request\n%s\n", err)
|
||||
} else {
|
||||
utils.Logger.Printf("\n%s\n%s\n", "REQUEST:", Sanitize(string(dumpedRequest)))
|
||||
if !shouldDisplayBody {
|
||||
utils.Logger.Println("[MULTIPART/FORM-DATA CONTENT HIDDEN]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dumpResponse(res *http.Response) {
|
||||
dumpedResponse, err := httputil.DumpResponse(res, true)
|
||||
if err != nil {
|
||||
utils.Logger.Printf("Error dumping response\n%s\n", err)
|
||||
} else {
|
||||
utils.Logger.Printf("\n%s\n%s\n", "RESPONSE:", Sanitize(string(dumpedResponse)))
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package net_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNet(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "GoGoBOSH Net suite")
|
||||
}
|
15
vendor/github.com/cloudfoundry-community/gogobosh/testhelpers/fixtures/bosh_config.yml
generated
vendored
15
vendor/github.com/cloudfoundry-community/gogobosh/testhelpers/fixtures/bosh_config.yml
generated
vendored
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
target: https://192.168.50.4:25555
|
||||
target_name: Bosh Lite Director
|
||||
target_version: 1.2732.0 (00000000)
|
||||
target_uuid: 4e46066f-e59b-493e-b775-accc1a7bc6e2
|
||||
aliases:
|
||||
target:
|
||||
lite: https://192.168.50.4:25555
|
||||
4e46066f-e59b-493e-b775-accc1a7bc6e2: https://192.168.50.4:25555
|
||||
auth:
|
||||
https://192.168.50.4:25555:
|
||||
username: admin
|
||||
password: password
|
||||
deployment:
|
||||
https://192.168.50.4:25555: "path/to/manifest.yml"
|
File diff suppressed because it is too large
Load Diff
|
@ -1,122 +0,0 @@
|
|||
package testhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/onsi/ginkgo"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewDirectorTestRequest(request TestRequest) TestRequest {
|
||||
request.Header = http.Header{
|
||||
"accept": {"application/json"},
|
||||
"authorization": {"Basic YWRtaW46YWRtaW4="},
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
type TestRequest struct {
|
||||
Method string
|
||||
Path string
|
||||
Header http.Header
|
||||
Matcher RequestMatcher
|
||||
Response TestResponse
|
||||
}
|
||||
|
||||
type RequestMatcher func(*http.Request)
|
||||
|
||||
type TestResponse struct {
|
||||
Body string
|
||||
Status int
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
type TestHandler struct {
|
||||
Requests []TestRequest
|
||||
CallCount int
|
||||
}
|
||||
|
||||
func (h *TestHandler) AllRequestsCalled() bool {
|
||||
if h.CallCount == len(h.Requests) {
|
||||
return true
|
||||
}
|
||||
fmt.Print("Failed to call requests:\n")
|
||||
for i := h.CallCount; i < len(h.Requests); i++ {
|
||||
fmt.Printf("%#v\n", h.Requests[i])
|
||||
}
|
||||
fmt.Print("\n\n")
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *TestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if len(h.Requests) <= h.CallCount {
|
||||
h.logError("Index out of range! Test server called too many times. Final Request:", r.Method, r.RequestURI)
|
||||
return
|
||||
}
|
||||
|
||||
tester := h.Requests[h.CallCount]
|
||||
h.CallCount++
|
||||
|
||||
// match method
|
||||
if tester.Method != r.Method {
|
||||
h.logError("Method does not match.\nExpected: %s\nActual: %s", tester.Method, r.Method)
|
||||
}
|
||||
|
||||
// match path
|
||||
paths := strings.Split(tester.Path, "?")
|
||||
if paths[0] != r.URL.Path {
|
||||
h.logError("Path does not match.\nExpected: %s\nActual: %s", paths[0], r.URL.Path)
|
||||
}
|
||||
// match query string
|
||||
if len(paths) > 1 {
|
||||
if !strings.Contains(r.URL.RawQuery, paths[1]) {
|
||||
h.logError("Query string does not match.\nExpected: %s\nActual: %s", paths[1], r.URL.RawQuery)
|
||||
}
|
||||
}
|
||||
|
||||
for key, values := range tester.Header {
|
||||
key = http.CanonicalHeaderKey(key)
|
||||
actualValues := strings.Join(r.Header[key], ";")
|
||||
expectedValues := strings.Join(values, ";")
|
||||
|
||||
if key == "Authorization" && !strings.Contains(actualValues, expectedValues) {
|
||||
h.logError("%s header is not contained in actual value.\nExpected: %s\nActual: %s", key, expectedValues, actualValues)
|
||||
}
|
||||
if key != "Authorization" && actualValues != expectedValues {
|
||||
h.logError("%s header did not match.\nExpected: %s\nActual: %s", key, expectedValues, actualValues)
|
||||
}
|
||||
}
|
||||
|
||||
// match custom request matcher
|
||||
if tester.Matcher != nil {
|
||||
tester.Matcher(r)
|
||||
}
|
||||
|
||||
// set response headers
|
||||
header := w.Header()
|
||||
for name, values := range tester.Response.Header {
|
||||
if len(values) < 1 {
|
||||
continue
|
||||
}
|
||||
header.Set(name, values[0])
|
||||
}
|
||||
|
||||
// write response
|
||||
w.WriteHeader(tester.Response.Status)
|
||||
fmt.Fprintln(w, tester.Response.Body)
|
||||
}
|
||||
|
||||
func NewTLSServer(requests []TestRequest) (s *httptest.Server, h *TestHandler) {
|
||||
h = &TestHandler{
|
||||
Requests: requests,
|
||||
}
|
||||
s = httptest.NewTLSServer(h)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *TestHandler) logError(msg string, args ...interface{}) {
|
||||
completeMsg := fmt.Sprintf(msg, args...)
|
||||
Fail(completeMsg)
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
/*
|
||||
Package utils is a collection of utilities used within gogobosh
|
||||
*/
|
||||
package utils
|
|
@ -1,107 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func OpenFile(path string) (file *os.File, err error) {
|
||||
err = os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModeTemporary|os.ModePerm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
}
|
||||
|
||||
func CreateFile(path string) (file *os.File, err error) {
|
||||
err = os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModeTemporary|os.ModePerm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return os.Create(path)
|
||||
}
|
||||
|
||||
func ReadFile(file *os.File) string {
|
||||
buf := &bytes.Buffer{}
|
||||
_, err := io.Copy(buf, file)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(buf.Bytes())
|
||||
}
|
||||
|
||||
func CopyFilePaths(fromPath, toPath string) (err error) {
|
||||
dst, err := CreateFile(toPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
return CopyPathToWriter(fromPath, dst)
|
||||
}
|
||||
|
||||
func IsDirEmpty(dir string) (isEmpty bool, err error) {
|
||||
dirFile, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, readErr := dirFile.Readdirnames(1)
|
||||
if readErr != nil {
|
||||
isEmpty = true
|
||||
} else {
|
||||
isEmpty = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CopyPathToWriter(originalFilePath string, targetWriter io.Writer) (err error) {
|
||||
originalFile, err := os.Open(originalFilePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer originalFile.Close()
|
||||
|
||||
_, err = io.Copy(targetWriter, originalFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func CopyReaderToPath(src io.Reader, targetPath string) (err error) {
|
||||
destFile, err := CreateFile(targetPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destFile, src)
|
||||
return
|
||||
}
|
||||
|
||||
func SetExecutableBits(dest string, fileInfoToCopy os.FileInfo) (err error) {
|
||||
destFileInfo, err := os.Stat(dest)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Chmod(dest, destFileInfo.Mode()|(fileInfoToCopy.Mode()&0111))
|
||||
return
|
||||
}
|
||||
|
||||
func SetExecutableBitsWithPaths(dest string, src string) (err error) {
|
||||
fileToCopyInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return SetExecutableBits(dest, fileToCopyInfo)
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
const GOGOBOSH_TRACE = "GOGOBOSH_TRACE"
|
||||
|
||||
type Printer interface {
|
||||
Print(v ...interface{})
|
||||
Printf(format string, v ...interface{})
|
||||
Println(v ...interface{})
|
||||
}
|
||||
|
||||
type nullLogger struct{}
|
||||
|
||||
func (*nullLogger) Print(v ...interface{}) {}
|
||||
func (*nullLogger) Printf(format string, v ...interface{}) {}
|
||||
func (*nullLogger) Println(v ...interface{}) {}
|
||||
|
||||
var stdOut io.Writer = os.Stdout
|
||||
var Logger Printer
|
||||
|
||||
func init() {
|
||||
Logger = NewLogger()
|
||||
}
|
||||
|
||||
func SetStdout(s io.Writer) {
|
||||
stdOut = s
|
||||
}
|
||||
|
||||
func NewLogger() Printer {
|
||||
cf_trace := os.Getenv(GOGOBOSH_TRACE)
|
||||
switch cf_trace {
|
||||
case "", "false":
|
||||
return new(nullLogger)
|
||||
case "true":
|
||||
return newStdoutLogger()
|
||||
default:
|
||||
return newFileLogger(cf_trace)
|
||||
}
|
||||
}
|
||||
|
||||
func newStdoutLogger() Printer {
|
||||
return log.New(stdOut, "", 0)
|
||||
}
|
||||
|
||||
func newFileLogger(path string) Printer {
|
||||
file, err := OpenFile(path)
|
||||
if err != nil {
|
||||
logger := newStdoutLogger()
|
||||
logger.Printf("CF_TRACE ERROR CREATING LOG FILE %s:\n%s", path, err)
|
||||
return logger
|
||||
}
|
||||
|
||||
return log.New(file, "", 0)
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
language: go
|
||||
go: 1.1
|
|
@ -1,21 +0,0 @@
|
|||
Copyright (C) 2013 Jeremy Saenz
|
||||
All Rights Reserved.
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
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.
|
|
@ -1,195 +0,0 @@
|
|||
[![Build Status](https://travis-ci.org/codegangsta/cli.png?branch=master)](https://travis-ci.org/codegangsta/cli)
|
||||
|
||||
# cli.go
|
||||
cli.go is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way.
|
||||
|
||||
You can view the API docs here:
|
||||
http://godoc.org/github.com/codegangsta/cli
|
||||
|
||||
## Overview
|
||||
Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app.
|
||||
|
||||
This is where cli.go comes into play. cli.go makes command line programming fun, organized, and expressive!
|
||||
|
||||
## Installation
|
||||
Make sure you have the a working Go environment (go 1.1 is *required*). [See the install instructions](http://golang.org/doc/install.html).
|
||||
|
||||
To install cli.go, simply run:
|
||||
```
|
||||
$ go get github.com/codegangsta/cli
|
||||
```
|
||||
|
||||
Make sure your PATH includes to the `$GOPATH/bin` directory so your commands can be easily used:
|
||||
```
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
One of the philosophies behind cli.go is that an API should be playful and full of discovery. So a cli.go app can be as little as one line of code in `main()`.
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.NewApp().Run(os.Args)
|
||||
}
|
||||
```
|
||||
|
||||
This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation:
|
||||
|
||||
``` go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "boom"
|
||||
app.Usage = "make an explosive entrance"
|
||||
app.Action = func(c *cli.Context) {
|
||||
println("boom! I say!")
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
```
|
||||
|
||||
Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below.
|
||||
|
||||
## Example
|
||||
|
||||
Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness!
|
||||
|
||||
``` go
|
||||
/* greet.go */
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"github.com/codegangsta/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "greet"
|
||||
app.Usage = "fight the loneliness!"
|
||||
app.Action = func(c *cli.Context) {
|
||||
println("Hello friend!")
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
```
|
||||
|
||||
Install our command to the `$GOPATH/bin` directory:
|
||||
|
||||
```
|
||||
$ go install
|
||||
```
|
||||
|
||||
Finally run our new command:
|
||||
|
||||
```
|
||||
$ greet
|
||||
Hello friend!
|
||||
```
|
||||
|
||||
cli.go also generates some bitchass help text:
|
||||
```
|
||||
$ greet help
|
||||
NAME:
|
||||
greet - fight the loneliness!
|
||||
|
||||
USAGE:
|
||||
greet [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
0.0.0
|
||||
|
||||
COMMANDS:
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS
|
||||
--version Shows version information
|
||||
```
|
||||
|
||||
### Arguments
|
||||
You can lookup arguments by calling the `Args` function on cli.Context.
|
||||
|
||||
``` go
|
||||
...
|
||||
app.Action = func(c *cli.Context) {
|
||||
println("Hello", c.Args()[0])
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### Flags
|
||||
Setting and querying flags is simple.
|
||||
``` go
|
||||
...
|
||||
app.Flags = []cli.Flag {
|
||||
cli.StringFlag{"lang", "english", "language for the greeting"},
|
||||
}
|
||||
app.Action = func(c *cli.Context) {
|
||||
name := "someone"
|
||||
if len(c.Args()) > 0 {
|
||||
name = c.Args()[0]
|
||||
}
|
||||
if c.String("lang") == "spanish" {
|
||||
println("Hola", name)
|
||||
} else {
|
||||
println("Hello", name)
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
#### Alternate Names
|
||||
|
||||
You can set alternate (or short) names for flags by providing a comma-delimited list for the Name. e.g.
|
||||
|
||||
``` go
|
||||
app.Flags = []cli.Flag {
|
||||
cli.StringFlag{"lang, l", "english", "language for the greeting"},
|
||||
}
|
||||
```
|
||||
|
||||
That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error.
|
||||
|
||||
### Subcommands
|
||||
|
||||
Subcommands can be defined for a more git-like command line app.
|
||||
```go
|
||||
...
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
ShortName: "a",
|
||||
Usage: "add a task to the list",
|
||||
Action: func(c *cli.Context) {
|
||||
println("added task: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "complete",
|
||||
ShortName: "c",
|
||||
Usage: "complete a task on the list",
|
||||
Action: func(c *cli.Context) {
|
||||
println("completed task: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
## About
|
||||
cli.go is written by none other than the [Code Gangsta](http://codegangsta.io)
|
|
@ -1,145 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// App is the main structure of a cli application. It is recomended that
|
||||
// and app be created with the cli.NewApp() function
|
||||
type App struct {
|
||||
// The name of the program. Defaults to os.Args[0]
|
||||
Name string
|
||||
// Description of the program.
|
||||
Usage string
|
||||
// Version of the program
|
||||
Version string
|
||||
// List of commands to execute
|
||||
Commands []Command
|
||||
// List of flags to parse
|
||||
Flags []Flag
|
||||
// An action to execute before any subcommands are run, but after the context is ready
|
||||
// If a non-nil error is returned, no subcommands are run
|
||||
Before func(context *Context) error
|
||||
// The action to execute when no subcommands are specified
|
||||
Action func(context *Context)
|
||||
// Compilation date
|
||||
Compiled time.Time
|
||||
// Author
|
||||
Author string
|
||||
// Author e-mail
|
||||
Email string
|
||||
}
|
||||
|
||||
// Tries to find out when this binary was compiled.
|
||||
// Returns the current time if it fails to find it.
|
||||
func compileTime() time.Time {
|
||||
info, err := os.Stat(os.Args[0])
|
||||
if err != nil {
|
||||
return time.Now()
|
||||
}
|
||||
return info.ModTime()
|
||||
}
|
||||
|
||||
// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action.
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
Name: os.Args[0],
|
||||
Usage: "A new cli application",
|
||||
Version: "0.0.0",
|
||||
Action: helpCommand.Action,
|
||||
Compiled: compileTime(),
|
||||
Author: "Author",
|
||||
Email: "unknown@email",
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination
|
||||
func (a *App) Run(arguments []string) error {
|
||||
// append help to commands
|
||||
if a.Command(helpCommand.Name) == nil {
|
||||
a.Commands = append(a.Commands, helpCommand)
|
||||
}
|
||||
|
||||
//append version/help flags
|
||||
a.appendFlag(BoolFlag{"version, v", "print the version"})
|
||||
a.appendFlag(BoolFlag{"help, h", "show help"})
|
||||
|
||||
// parse flags
|
||||
set := flagSet(a.Name, a.Flags)
|
||||
set.SetOutput(ioutil.Discard)
|
||||
err := set.Parse(arguments[1:])
|
||||
nerr := normalizeFlags(a.Flags, set)
|
||||
if nerr != nil {
|
||||
fmt.Println(nerr)
|
||||
context := NewContext(a, set, set)
|
||||
ShowAppHelp(context)
|
||||
fmt.Println("")
|
||||
return nerr
|
||||
}
|
||||
context := NewContext(a, set, set)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Incorrect Usage.\n\n")
|
||||
ShowAppHelp(context)
|
||||
fmt.Println("")
|
||||
return err
|
||||
}
|
||||
|
||||
if checkHelp(context) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkVersion(context) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.Before != nil {
|
||||
err := a.Before(context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
args := context.Args()
|
||||
if args.Present() {
|
||||
name := args.First()
|
||||
c := a.Command(name)
|
||||
if c != nil {
|
||||
return c.Run(context)
|
||||
}
|
||||
}
|
||||
|
||||
// Run default Action
|
||||
a.Action(context)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the named command on App. Returns nil if the command does not exist
|
||||
func (a *App) Command(name string) *Command {
|
||||
for _, c := range a.Commands {
|
||||
if c.HasName(name) {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) hasFlag(flag Flag) bool {
|
||||
for _, f := range a.Flags {
|
||||
if flag == f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) appendFlag(flag Flag) {
|
||||
if !a.hasFlag(flag) {
|
||||
a.Flags = append(a.Flags, flag)
|
||||
}
|
||||
}
|
|
@ -1,227 +0,0 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/codegangsta/cli"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ExampleApp() {
|
||||
// set args for examples sake
|
||||
os.Args = []string{"greet", "--name", "Jeremy"}
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "greet"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{Name: "name", Value: "bob", Usage: "a name to say"},
|
||||
}
|
||||
app.Action = func(c *cli.Context) {
|
||||
fmt.Printf("Hello %v\n", c.String("name"))
|
||||
}
|
||||
app.Run(os.Args)
|
||||
// Output:
|
||||
// Hello Jeremy
|
||||
}
|
||||
|
||||
func TestApp_Run(t *testing.T) {
|
||||
s := ""
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Action = func(c *cli.Context) {
|
||||
s = s + c.Args().First()
|
||||
}
|
||||
|
||||
err := app.Run([]string{"command", "foo"})
|
||||
expect(t, err, nil)
|
||||
err = app.Run([]string{"command", "bar"})
|
||||
expect(t, err, nil)
|
||||
expect(t, s, "foobar")
|
||||
}
|
||||
|
||||
var commandAppTests = []struct {
|
||||
name string
|
||||
expected bool
|
||||
}{
|
||||
{"foobar", true},
|
||||
{"batbaz", true},
|
||||
{"b", true},
|
||||
{"f", true},
|
||||
{"bat", false},
|
||||
{"nothing", false},
|
||||
}
|
||||
|
||||
func TestApp_Command(t *testing.T) {
|
||||
app := cli.NewApp()
|
||||
fooCommand := cli.Command{Name: "foobar", ShortName: "f"}
|
||||
batCommand := cli.Command{Name: "batbaz", ShortName: "b"}
|
||||
app.Commands = []cli.Command{
|
||||
fooCommand,
|
||||
batCommand,
|
||||
}
|
||||
|
||||
for _, test := range commandAppTests {
|
||||
expect(t, app.Command(test.name) != nil, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_CommandWithArgBeforeFlags(t *testing.T) {
|
||||
var parsedOption, firstArg string
|
||||
|
||||
app := cli.NewApp()
|
||||
command := cli.Command{
|
||||
Name: "cmd",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "option", Value: "", Usage: "some option"},
|
||||
},
|
||||
Action: func(c *cli.Context) {
|
||||
parsedOption = c.String("option")
|
||||
firstArg = c.Args().First()
|
||||
},
|
||||
}
|
||||
app.Commands = []cli.Command{command}
|
||||
|
||||
app.Run([]string{"", "cmd", "my-arg", "--option", "my-option"})
|
||||
|
||||
expect(t, parsedOption, "my-option")
|
||||
expect(t, firstArg, "my-arg")
|
||||
}
|
||||
|
||||
func TestApp_Float64Flag(t *testing.T) {
|
||||
var meters float64
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Flags = []cli.Flag{
|
||||
cli.Float64Flag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"},
|
||||
}
|
||||
app.Action = func(c *cli.Context) {
|
||||
meters = c.Float64("height")
|
||||
}
|
||||
|
||||
app.Run([]string{"", "--height", "1.93"})
|
||||
expect(t, meters, 1.93)
|
||||
}
|
||||
|
||||
func TestApp_ParseSliceFlags(t *testing.T) {
|
||||
var parsedOption, firstArg string
|
||||
var parsedIntSlice []int
|
||||
var parsedStringSlice []string
|
||||
|
||||
app := cli.NewApp()
|
||||
command := cli.Command{
|
||||
Name: "cmd",
|
||||
Flags: []cli.Flag{
|
||||
cli.IntSliceFlag{Name: "p", Value: &cli.IntSlice{}, Usage: "set one or more ip addr"},
|
||||
cli.StringSliceFlag{Name: "ip", Value: &cli.StringSlice{}, Usage: "set one or more ports to open"},
|
||||
},
|
||||
Action: func(c *cli.Context) {
|
||||
parsedIntSlice = c.IntSlice("p")
|
||||
parsedStringSlice = c.StringSlice("ip")
|
||||
parsedOption = c.String("option")
|
||||
firstArg = c.Args().First()
|
||||
},
|
||||
}
|
||||
app.Commands = []cli.Command{command}
|
||||
|
||||
app.Run([]string{"", "cmd", "my-arg", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4"})
|
||||
|
||||
IntsEquals := func(a, b []int) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
StrsEquals := func(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
var expectedIntSlice = []int{22, 80}
|
||||
var expectedStringSlice = []string{"8.8.8.8", "8.8.4.4"}
|
||||
|
||||
if !IntsEquals(parsedIntSlice, expectedIntSlice) {
|
||||
t.Errorf("%s does not match %s", parsedIntSlice, expectedIntSlice)
|
||||
}
|
||||
|
||||
if !StrsEquals(parsedStringSlice, expectedStringSlice) {
|
||||
t.Errorf("%s does not match %s", parsedStringSlice, expectedStringSlice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_BeforeFunc(t *testing.T) {
|
||||
beforeRun, subcommandRun := false, false
|
||||
beforeError := fmt.Errorf("fail")
|
||||
var err error
|
||||
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Before = func(c *cli.Context) error {
|
||||
beforeRun = true
|
||||
s := c.String("opt")
|
||||
if s == "fail" {
|
||||
return beforeError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
cli.Command{
|
||||
Name: "sub",
|
||||
Action: func(c *cli.Context) {
|
||||
subcommandRun = true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{Name: "opt"},
|
||||
}
|
||||
|
||||
// run with the Before() func succeeding
|
||||
err = app.Run([]string{"command", "--opt", "succeed", "sub"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run error: %s", err)
|
||||
}
|
||||
|
||||
if beforeRun == false {
|
||||
t.Errorf("Before() not executed when expected")
|
||||
}
|
||||
|
||||
if subcommandRun == false {
|
||||
t.Errorf("Subcommand not executed when expected")
|
||||
}
|
||||
|
||||
// reset
|
||||
beforeRun, subcommandRun = false, false
|
||||
|
||||
// run with the Before() func failing
|
||||
err = app.Run([]string{"command", "--opt", "fail", "sub"})
|
||||
|
||||
// should be the same error produced by the Before func
|
||||
if err != beforeError {
|
||||
t.Errorf("Run error expected, but not received")
|
||||
}
|
||||
|
||||
if beforeRun == false {
|
||||
t.Errorf("Before() not executed when expected")
|
||||
}
|
||||
|
||||
if subcommandRun == true {
|
||||
t.Errorf("Subcommand executed when NOT expected")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// Package cli provides a minimal framework for creating and organizing command line
|
||||
// Go applications. cli is designed to be easy to understand and write, the most simple
|
||||
// cli application can be written as follows:
|
||||
// func main() {
|
||||
// cli.NewApp().Run(os.Args)
|
||||
// }
|
||||
//
|
||||
// Of course this application does not do much, so let's make this an actual application:
|
||||
// func main() {
|
||||
// app := cli.NewApp()
|
||||
// app.Name = "greet"
|
||||
// app.Usage = "say a greeting"
|
||||
// app.Action = func(c *cli.Context) {
|
||||
// println("Greetings")
|
||||
// }
|
||||
//
|
||||
// app.Run(os.Args)
|
||||
// }
|
||||
package cli
|
|
@ -1,32 +0,0 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"github.com/codegangsta/cli"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "todo"
|
||||
app.Usage = "task list on the command line"
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
ShortName: "a",
|
||||
Usage: "add a task to the list",
|
||||
Action: func(c *cli.Context) {
|
||||
println("added task: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "complete",
|
||||
ShortName: "c",
|
||||
Usage: "complete a task on the list",
|
||||
Action: func(c *cli.Context) {
|
||||
println("completed task: ", c.Args().First())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Command is a subcommand for a cli.App.
|
||||
type Command struct {
|
||||
// The name of the command
|
||||
Name string
|
||||
// short name of the command. Typically one character
|
||||
ShortName string
|
||||
// A short description of the usage of this command
|
||||
Usage string
|
||||
// A longer explaination of how the command works
|
||||
Description string
|
||||
// The function to call when this command is invoked
|
||||
Action func(context *Context)
|
||||
// List of flags to parse
|
||||
Flags []Flag
|
||||
}
|
||||
|
||||
// Invokes the command given the context, parses ctx.Args() to generate command-specific flags
|
||||
func (c Command) Run(ctx *Context) error {
|
||||
// append help to flags
|
||||
c.Flags = append(
|
||||
c.Flags,
|
||||
BoolFlag{"help, h", "show help"},
|
||||
)
|
||||
|
||||
set := flagSet(c.Name, c.Flags)
|
||||
set.SetOutput(ioutil.Discard)
|
||||
|
||||
firstFlagIndex := -1
|
||||
for index, arg := range ctx.Args() {
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
firstFlagIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if firstFlagIndex > -1 {
|
||||
args := ctx.Args()
|
||||
regularArgs := args[1:firstFlagIndex]
|
||||
flagArgs := args[firstFlagIndex:]
|
||||
err = set.Parse(append(flagArgs, regularArgs...))
|
||||
} else {
|
||||
err = set.Parse(ctx.Args().Tail())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Incorrect Usage.\n\n")
|
||||
ShowCommandHelp(ctx, c.Name)
|
||||
fmt.Println("")
|
||||
return err
|
||||
}
|
||||
|
||||
nerr := normalizeFlags(c.Flags, set)
|
||||
if nerr != nil {
|
||||
fmt.Println(nerr)
|
||||
fmt.Println("")
|
||||
ShowCommandHelp(ctx, c.Name)
|
||||
fmt.Println("")
|
||||
return nerr
|
||||
}
|
||||
context := NewContext(ctx.App, set, ctx.globalSet)
|
||||
if checkCommandHelp(context, c.Name) {
|
||||
return nil
|
||||
}
|
||||
c.Action(context)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns true if Command.Name or Command.ShortName matches given name
|
||||
func (c Command) HasName(name string) bool {
|
||||
return c.Name == name || c.ShortName == name
|
||||
}
|
|
@ -1,224 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Context is a type that is passed through to
|
||||
// each Handler action in a cli application. Context
|
||||
// can be used to retrieve context-specific Args and
|
||||
// parsed command-line options.
|
||||
type Context struct {
|
||||
App *App
|
||||
flagSet *flag.FlagSet
|
||||
globalSet *flag.FlagSet
|
||||
setFlags map[string]bool
|
||||
}
|
||||
|
||||
// Creates a new context. For use in when invoking an App or Command action.
|
||||
func NewContext(app *App, set *flag.FlagSet, globalSet *flag.FlagSet) *Context {
|
||||
return &Context{app, set, globalSet, nil}
|
||||
}
|
||||
|
||||
// Looks up the value of a local int flag, returns 0 if no int flag exists
|
||||
func (c *Context) Int(name string) int {
|
||||
return lookupInt(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists
|
||||
func (c *Context) Float64(name string) float64 {
|
||||
return lookupFloat64(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local bool flag, returns false if no bool flag exists
|
||||
func (c *Context) Bool(name string) bool {
|
||||
return lookupBool(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local string flag, returns "" if no string flag exists
|
||||
func (c *Context) String(name string) string {
|
||||
return lookupString(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local string slice flag, returns nil if no string slice flag exists
|
||||
func (c *Context) StringSlice(name string) []string {
|
||||
return lookupStringSlice(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a local int slice flag, returns nil if no int slice flag exists
|
||||
func (c *Context) IntSlice(name string) []int {
|
||||
return lookupIntSlice(name, c.flagSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a global int flag, returns 0 if no int flag exists
|
||||
func (c *Context) GlobalInt(name string) int {
|
||||
return lookupInt(name, c.globalSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a global bool flag, returns false if no bool flag exists
|
||||
func (c *Context) GlobalBool(name string) bool {
|
||||
return lookupBool(name, c.globalSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a global string flag, returns "" if no string flag exists
|
||||
func (c *Context) GlobalString(name string) string {
|
||||
return lookupString(name, c.globalSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a global string slice flag, returns nil if no string slice flag exists
|
||||
func (c *Context) GlobalStringSlice(name string) []string {
|
||||
return lookupStringSlice(name, c.globalSet)
|
||||
}
|
||||
|
||||
// Looks up the value of a global int slice flag, returns nil if no int slice flag exists
|
||||
func (c *Context) GlobalIntSlice(name string) []int {
|
||||
return lookupIntSlice(name, c.globalSet)
|
||||
}
|
||||
|
||||
// Determines if the flag was actually set exists
|
||||
func (c *Context) IsSet(name string) bool {
|
||||
if c.setFlags == nil {
|
||||
c.setFlags = make(map[string]bool)
|
||||
c.flagSet.Visit(func(f *flag.Flag) {
|
||||
c.setFlags[f.Name] = true
|
||||
})
|
||||
}
|
||||
return c.setFlags[name] == true
|
||||
}
|
||||
|
||||
type Args []string
|
||||
|
||||
// Returns the command line arguments associated with the context.
|
||||
func (c *Context) Args() Args {
|
||||
args := Args(c.flagSet.Args())
|
||||
return args
|
||||
}
|
||||
|
||||
// Returns the nth argument, or else a blank string
|
||||
func (a Args) Get(n int) string {
|
||||
if len(a) > n {
|
||||
return a[n]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Returns the first argument, or else a blank string
|
||||
func (a Args) First() string {
|
||||
return a.Get(0)
|
||||
}
|
||||
|
||||
// Return the rest of the arguments (not the first one)
|
||||
// or else an empty string slice
|
||||
func (a Args) Tail() []string {
|
||||
if len(a) >= 2 {
|
||||
return []string(a)[1:]
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Checks if there are any arguments present
|
||||
func (a Args) Present() bool {
|
||||
return len(a) != 0
|
||||
}
|
||||
|
||||
func lookupInt(name string, set *flag.FlagSet) int {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
val, err := strconv.Atoi(f.Value.String())
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupFloat64(name string, set *flag.FlagSet) float64 {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
val, err := strconv.ParseFloat(f.Value.String(), 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupString(name string, set *flag.FlagSet) string {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
return f.Value.String()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func lookupStringSlice(name string, set *flag.FlagSet) []string {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
return (f.Value.(*StringSlice)).Value()
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupIntSlice(name string, set *flag.FlagSet) []int {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
return (f.Value.(*IntSlice)).Value()
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupBool(name string, set *flag.FlagSet) bool {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
val, err := strconv.ParseBool(f.Value.String())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeFlags(flags []Flag, set *flag.FlagSet) error {
|
||||
visited := make(map[string]bool)
|
||||
set.Visit(func(f *flag.Flag) {
|
||||
visited[f.Name] = true
|
||||
})
|
||||
for _, f := range flags {
|
||||
parts := strings.Split(f.getName(), ",")
|
||||
if len(parts) == 1 {
|
||||
continue
|
||||
}
|
||||
var ff *flag.Flag
|
||||
for _, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
if visited[name] {
|
||||
if ff != nil {
|
||||
return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name)
|
||||
}
|
||||
ff = set.Lookup(name)
|
||||
}
|
||||
}
|
||||
if ff == nil {
|
||||
continue
|
||||
}
|
||||
for _, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
set.Set(name, ff.Value.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/codegangsta/cli"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Int("myflag", 12, "doc")
|
||||
globalSet := flag.NewFlagSet("test", 0)
|
||||
globalSet.Int("myflag", 42, "doc")
|
||||
c := cli.NewContext(nil, set, globalSet)
|
||||
expect(t, c.Int("myflag"), 12)
|
||||
expect(t, c.GlobalInt("myflag"), 42)
|
||||
}
|
||||
|
||||
func TestContext_Int(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Int("myflag", 12, "doc")
|
||||
c := cli.NewContext(nil, set, set)
|
||||
expect(t, c.Int("myflag"), 12)
|
||||
}
|
||||
|
||||
func TestContext_String(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.String("myflag", "hello world", "doc")
|
||||
c := cli.NewContext(nil, set, set)
|
||||
expect(t, c.String("myflag"), "hello world")
|
||||
}
|
||||
|
||||
func TestContext_Bool(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
c := cli.NewContext(nil, set, set)
|
||||
expect(t, c.Bool("myflag"), false)
|
||||
}
|
||||
|
||||
func TestContext_Args(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
c := cli.NewContext(nil, set, set)
|
||||
set.Parse([]string{"--myflag", "bat", "baz"})
|
||||
expect(t, len(c.Args()), 2)
|
||||
expect(t, c.Bool("myflag"), true)
|
||||
}
|
||||
|
||||
func TestContext_IsSet(t *testing.T) {
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
set.Bool("myflag", false, "doc")
|
||||
set.String("otherflag", "hello world", "doc")
|
||||
c := cli.NewContext(nil, set, set)
|
||||
set.Parse([]string{"--myflag", "bat", "baz"})
|
||||
expect(t, c.IsSet("myflag"), true)
|
||||
expect(t, c.IsSet("otherflag"), false)
|
||||
expect(t, c.IsSet("bogusflag"), false)
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Flag is a common interface related to parsing flags in cli.
|
||||
// For more advanced flag parsing techniques, it is recomended that
|
||||
// this interface be implemented.
|
||||
type Flag interface {
|
||||
fmt.Stringer
|
||||
// Apply Flag settings to the given flag set
|
||||
Apply(*flag.FlagSet)
|
||||
getName() string
|
||||
}
|
||||
|
||||
func flagSet(name string, flags []Flag) *flag.FlagSet {
|
||||
set := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||
|
||||
for _, f := range flags {
|
||||
f.Apply(set)
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func eachName(longName string, fn func(string)) {
|
||||
parts := strings.Split(longName, ",")
|
||||
for _, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
fn(name)
|
||||
}
|
||||
}
|
||||
|
||||
type StringSlice []string
|
||||
|
||||
func (f *StringSlice) Set(value string) error {
|
||||
*f = append(*f, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *StringSlice) String() string {
|
||||
return fmt.Sprintf("%s", *f)
|
||||
}
|
||||
|
||||
func (f *StringSlice) Value() []string {
|
||||
return *f
|
||||
}
|
||||
|
||||
type StringSliceFlag struct {
|
||||
Name string
|
||||
Value *StringSlice
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f StringSliceFlag) String() string {
|
||||
return fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage)
|
||||
}
|
||||
|
||||
func (f StringSliceFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f StringSliceFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
type IntSlice []int
|
||||
|
||||
func (f *IntSlice) Set(value string) error {
|
||||
|
||||
tmp, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
*f = append(*f, tmp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *IntSlice) String() string {
|
||||
return fmt.Sprintf("%d", *f)
|
||||
}
|
||||
|
||||
func (f *IntSlice) Value() []int {
|
||||
return *f
|
||||
}
|
||||
|
||||
type IntSliceFlag struct {
|
||||
Name string
|
||||
Value *IntSlice
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f IntSliceFlag) String() string {
|
||||
firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ")
|
||||
pref := prefixFor(firstName)
|
||||
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)
|
||||
}
|
||||
|
||||
func (f IntSliceFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Var(f.Value, name, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f IntSliceFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
type BoolFlag struct {
|
||||
Name string
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f BoolFlag) String() string {
|
||||
return fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)
|
||||
}
|
||||
|
||||
func (f BoolFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Bool(name, false, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f BoolFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
type StringFlag struct {
|
||||
Name string
|
||||
Value string
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f StringFlag) String() string {
|
||||
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)
|
||||
}
|
||||
|
||||
func (f StringFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.String(name, f.Value, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f StringFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
type IntFlag struct {
|
||||
Name string
|
||||
Value int
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f IntFlag) String() string {
|
||||
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)
|
||||
}
|
||||
|
||||
func (f IntFlag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Int(name, f.Value, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f IntFlag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
type Float64Flag struct {
|
||||
Name string
|
||||
Value float64
|
||||
Usage string
|
||||
}
|
||||
|
||||
func (f Float64Flag) String() string {
|
||||
return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)
|
||||
}
|
||||
|
||||
func (f Float64Flag) Apply(set *flag.FlagSet) {
|
||||
eachName(f.Name, func(name string) {
|
||||
set.Float64(name, f.Value, f.Usage)
|
||||
})
|
||||
}
|
||||
|
||||
func (f Float64Flag) getName() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func prefixFor(name string) (prefix string) {
|
||||
if len(name) == 1 {
|
||||
prefix = "-"
|
||||
} else {
|
||||
prefix = "--"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func prefixedNames(fullName string) (prefixed string) {
|
||||
parts := strings.Split(fullName, ",")
|
||||
for i, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
prefixed += prefixFor(name) + name
|
||||
if i < len(parts)-1 {
|
||||
prefixed += ", "
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"github.com/codegangsta/cli"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var boolFlagTests = []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"help", "--help\t"},
|
||||
{"h", "-h\t"},
|
||||
}
|
||||
|
||||
func TestBoolFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range boolFlagTests {
|
||||
flag := cli.BoolFlag{Name: test.name}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stringFlagTests = []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"help", "--help ''\t"},
|
||||
{"h", "-h ''\t"},
|
||||
}
|
||||
|
||||
func TestStringFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range stringFlagTests {
|
||||
flag := cli.StringFlag{Name: test.name}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var intFlagTests = []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"help", "--help '0'\t"},
|
||||
{"h", "-h '0'\t"},
|
||||
}
|
||||
|
||||
func TestIntFlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range intFlagTests {
|
||||
flag := cli.IntFlag{Name: test.name}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var float64FlagTests = []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"help", "--help '0'\t"},
|
||||
{"h", "-h '0'\t"},
|
||||
}
|
||||
|
||||
func TestFloat64FlagHelpOutput(t *testing.T) {
|
||||
|
||||
for _, test := range float64FlagTests {
|
||||
flag := cli.Float64Flag{Name: test.name}
|
||||
output := flag.String()
|
||||
|
||||
if output != test.expected {
|
||||
t.Errorf("%s does not match %s", output, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMultiString(t *testing.T) {
|
||||
(&cli.App{
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "serve, s"},
|
||||
},
|
||||
Action: func(ctx *cli.Context) {
|
||||
if ctx.String("serve") != "10" {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.String("s") != "10" {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}).Run([]string{"run", "-s", "10"})
|
||||
}
|
||||
|
||||
func TestParseMultiInt(t *testing.T) {
|
||||
a := cli.App{
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{Name: "serve, s"},
|
||||
},
|
||||
Action: func(ctx *cli.Context) {
|
||||
if ctx.Int("serve") != 10 {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Int("s") != 10 {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run", "-s", "10"})
|
||||
}
|
||||
|
||||
func TestParseMultiBool(t *testing.T) {
|
||||
a := cli.App{
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "serve, s"},
|
||||
},
|
||||
Action: func(ctx *cli.Context) {
|
||||
if ctx.Bool("serve") != true {
|
||||
t.Errorf("main name not set")
|
||||
}
|
||||
if ctx.Bool("s") != true {
|
||||
t.Errorf("short name not set")
|
||||
}
|
||||
},
|
||||
}
|
||||
a.Run([]string{"run", "--serve"})
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// The text template for the Default help topic.
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
var AppHelpTemplate = `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
{{.Version}}
|
||||
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}
|
||||
GLOBAL OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
// The text template for the command help topic.
|
||||
// cli.go uses text/template to render templates. You can
|
||||
// render custom help text by setting this variable.
|
||||
var CommandHelpTemplate = `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
command {{.Name}} [command options] [arguments...]
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}
|
||||
|
||||
OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
var helpCommand = Command{
|
||||
Name: "help",
|
||||
ShortName: "h",
|
||||
Usage: "Shows a list of commands or help for one command",
|
||||
Action: func(c *Context) {
|
||||
args := c.Args()
|
||||
if args.Present() {
|
||||
ShowCommandHelp(c, args.First())
|
||||
} else {
|
||||
ShowAppHelp(c)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Prints help for the App
|
||||
func ShowAppHelp(c *Context) {
|
||||
printHelp(AppHelpTemplate, c.App)
|
||||
}
|
||||
|
||||
// Prints help for the given command
|
||||
func ShowCommandHelp(c *Context, command string) {
|
||||
for _, c := range c.App.Commands {
|
||||
if c.HasName(command) {
|
||||
printHelp(CommandHelpTemplate, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("No help topic for '%v'\n", command)
|
||||
}
|
||||
|
||||
// Prints the version number of the App
|
||||
func ShowVersion(c *Context) {
|
||||
fmt.Printf("%v version %v\n", c.App.Name, c.App.Version)
|
||||
}
|
||||
|
||||
func printHelp(templ string, data interface{}) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
|
||||
t := template.Must(template.New("help").Parse(templ))
|
||||
err := t.Execute(w, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func checkVersion(c *Context) bool {
|
||||
if c.GlobalBool("version") {
|
||||
ShowVersion(c)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkHelp(c *Context) bool {
|
||||
if c.GlobalBool("h") || c.GlobalBool("help") {
|
||||
ShowAppHelp(c)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkCommandHelp(c *Context, name string) bool {
|
||||
if c.Bool("h") || c.Bool("help") {
|
||||
ShowCommandHelp(c, name)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package cli_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/* Test Helpers */
|
||||
func expect(t *testing.T, a interface{}, b interface{}) {
|
||||
if a != b {
|
||||
t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
|
||||
func refute(t *testing.T, a interface{}, b interface{}) {
|
||||
if a == b {
|
||||
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
inject
|
||||
inject.test
|
|
@ -1,20 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Jeremy Saenz
|
||||
|
||||
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.
|
|
@ -1,4 +0,0 @@
|
|||
inject
|
||||
======
|
||||
|
||||
Dependency injection for go
|
|
@ -1,187 +0,0 @@
|
|||
// Package inject provides utilities for mapping and injecting dependencies in various ways.
|
||||
package inject
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Injector represents an interface for mapping and injecting dependencies into structs
|
||||
// and function arguments.
|
||||
type Injector interface {
|
||||
Applicator
|
||||
Invoker
|
||||
TypeMapper
|
||||
// SetParent sets the parent of the injector. If the injector cannot find a
|
||||
// dependency in its Type map it will check its parent before returning an
|
||||
// error.
|
||||
SetParent(Injector)
|
||||
}
|
||||
|
||||
// Applicator represents an interface for mapping dependencies to a struct.
|
||||
type Applicator interface {
|
||||
// Maps dependencies in the Type map to each field in the struct
|
||||
// that is tagged with 'inject'. Returns an error if the injection
|
||||
// fails.
|
||||
Apply(interface{}) error
|
||||
}
|
||||
|
||||
// Invoker represents an interface for calling functions via reflection.
|
||||
type Invoker interface {
|
||||
// Invoke attempts to call the interface{} provided as a function,
|
||||
// providing dependencies for function arguments based on Type. Returns
|
||||
// a slice of reflect.Value representing the returned values of the function.
|
||||
// Returns an error if the injection fails.
|
||||
Invoke(interface{}) ([]reflect.Value, error)
|
||||
}
|
||||
|
||||
// TypeMapper represents an interface for mapping interface{} values based on type.
|
||||
type TypeMapper interface {
|
||||
// Maps the interface{} value based on its immediate type from reflect.TypeOf.
|
||||
Map(interface{}) TypeMapper
|
||||
// Maps the interface{} value based on the pointer of an Interface provided.
|
||||
// This is really only useful for mapping a value as an interface, as interfaces
|
||||
// cannot at this time be referenced directly without a pointer.
|
||||
MapTo(interface{}, interface{}) TypeMapper
|
||||
// Provides a possibility to directly insert a mapping based on type and value.
|
||||
// This makes it possible to directly map type arguments not possible to instantiate
|
||||
// with reflect like unidirectional channels.
|
||||
Set(reflect.Type, reflect.Value) TypeMapper
|
||||
// Returns the Value that is mapped to the current type. Returns a zeroed Value if
|
||||
// the Type has not been mapped.
|
||||
Get(reflect.Type) reflect.Value
|
||||
}
|
||||
|
||||
type injector struct {
|
||||
values map[reflect.Type]reflect.Value
|
||||
parent Injector
|
||||
}
|
||||
|
||||
// InterfaceOf dereferences a pointer to an Interface type.
|
||||
// It panics if value is not an pointer to an interface.
|
||||
func InterfaceOf(value interface{}) reflect.Type {
|
||||
t := reflect.TypeOf(value)
|
||||
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Interface {
|
||||
panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// New returns a new Injector.
|
||||
func New() Injector {
|
||||
return &injector{
|
||||
values: make(map[reflect.Type]reflect.Value),
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke attempts to call the interface{} provided as a function,
|
||||
// providing dependencies for function arguments based on Type.
|
||||
// Returns a slice of reflect.Value representing the returned values of the function.
|
||||
// Returns an error if the injection fails.
|
||||
// It panics if f is not a function
|
||||
func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
|
||||
t := reflect.TypeOf(f)
|
||||
|
||||
var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func
|
||||
for i := 0; i < t.NumIn(); i++ {
|
||||
argType := t.In(i)
|
||||
val := inj.Get(argType)
|
||||
if !val.IsValid() {
|
||||
return nil, fmt.Errorf("Value not found for type %v", argType)
|
||||
}
|
||||
|
||||
in[i] = val
|
||||
}
|
||||
|
||||
return reflect.ValueOf(f).Call(in), nil
|
||||
}
|
||||
|
||||
// Maps dependencies in the Type map to each field in the struct
|
||||
// that is tagged with 'inject'.
|
||||
// Returns an error if the injection fails.
|
||||
func (inj *injector) Apply(val interface{}) error {
|
||||
v := reflect.ValueOf(val)
|
||||
|
||||
for v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil // Should not panic here ?
|
||||
}
|
||||
|
||||
t := v.Type()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
structField := t.Field(i)
|
||||
if f.CanSet() && (structField.Tag == "inject" || structField.Tag.Get("inject") != "") {
|
||||
ft := f.Type()
|
||||
v := inj.Get(ft)
|
||||
if !v.IsValid() {
|
||||
return fmt.Errorf("Value not found for type %v", ft)
|
||||
}
|
||||
|
||||
f.Set(v)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Maps the concrete value of val to its dynamic type using reflect.TypeOf,
|
||||
// It returns the TypeMapper registered in.
|
||||
func (i *injector) Map(val interface{}) TypeMapper {
|
||||
i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {
|
||||
i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
|
||||
return i
|
||||
}
|
||||
|
||||
// Maps the given reflect.Type to the given reflect.Value and returns
|
||||
// the Typemapper the mapping has been registered in.
|
||||
func (i *injector) Set(typ reflect.Type, val reflect.Value) TypeMapper {
|
||||
i.values[typ] = val
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *injector) Get(t reflect.Type) reflect.Value {
|
||||
val := i.values[t]
|
||||
|
||||
if val.IsValid() {
|
||||
return val
|
||||
}
|
||||
|
||||
// no concrete types found, try to find implementors
|
||||
// if t is an interface
|
||||
if t.Kind() == reflect.Interface {
|
||||
for k, v := range i.values {
|
||||
if k.Implements(t) {
|
||||
val = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Still no type found, try to look it up on the parent
|
||||
if !val.IsValid() && i.parent != nil {
|
||||
val = i.parent.Get(t)
|
||||
}
|
||||
|
||||
return val
|
||||
|
||||
}
|
||||
|
||||
func (i *injector) SetParent(parent Injector) {
|
||||
i.parent = parent
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
package inject_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/codegangsta/inject"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type SpecialString interface {
|
||||
}
|
||||
|
||||
type TestStruct struct {
|
||||
Dep1 string `inject:"t" json:"-"`
|
||||
Dep2 SpecialString `inject`
|
||||
Dep3 string
|
||||
}
|
||||
|
||||
type Greeter struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (g *Greeter) String() string {
|
||||
return "Hello, My name is" + g.Name
|
||||
}
|
||||
|
||||
/* Test Helpers */
|
||||
func expect(t *testing.T, a interface{}, b interface{}) {
|
||||
if a != b {
|
||||
t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
|
||||
func refute(t *testing.T, a interface{}, b interface{}) {
|
||||
if a == b {
|
||||
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_InjectorInvoke(t *testing.T) {
|
||||
injector := inject.New()
|
||||
expect(t, injector == nil, false)
|
||||
|
||||
dep := "some dependency"
|
||||
injector.Map(dep)
|
||||
dep2 := "another dep"
|
||||
injector.MapTo(dep2, (*SpecialString)(nil))
|
||||
dep3 := make(chan *SpecialString)
|
||||
dep4 := make(chan *SpecialString)
|
||||
typRecv := reflect.ChanOf(reflect.RecvDir, reflect.TypeOf(dep3).Elem())
|
||||
typSend := reflect.ChanOf(reflect.SendDir, reflect.TypeOf(dep4).Elem())
|
||||
injector.Set(typRecv, reflect.ValueOf(dep3))
|
||||
injector.Set(typSend, reflect.ValueOf(dep4))
|
||||
|
||||
_, err := injector.Invoke(func(d1 string, d2 SpecialString, d3 <-chan *SpecialString, d4 chan<- *SpecialString) {
|
||||
expect(t, d1, dep)
|
||||
expect(t, d2, dep2)
|
||||
expect(t, reflect.TypeOf(d3).Elem(), reflect.TypeOf(dep3).Elem())
|
||||
expect(t, reflect.TypeOf(d4).Elem(), reflect.TypeOf(dep4).Elem())
|
||||
expect(t, reflect.TypeOf(d3).ChanDir(), reflect.RecvDir)
|
||||
expect(t, reflect.TypeOf(d4).ChanDir(), reflect.SendDir)
|
||||
})
|
||||
|
||||
expect(t, err, nil)
|
||||
}
|
||||
|
||||
func Test_InjectorInvokeReturnValues(t *testing.T) {
|
||||
injector := inject.New()
|
||||
expect(t, injector == nil, false)
|
||||
|
||||
dep := "some dependency"
|
||||
injector.Map(dep)
|
||||
dep2 := "another dep"
|
||||
injector.MapTo(dep2, (*SpecialString)(nil))
|
||||
|
||||
result, err := injector.Invoke(func(d1 string, d2 SpecialString) string {
|
||||
expect(t, d1, dep)
|
||||
expect(t, d2, dep2)
|
||||
return "Hello world"
|
||||
})
|
||||
|
||||
expect(t, result[0].String(), "Hello world")
|
||||
expect(t, err, nil)
|
||||
}
|
||||
|
||||
func Test_InjectorApply(t *testing.T) {
|
||||
injector := inject.New()
|
||||
|
||||
injector.Map("a dep").MapTo("another dep", (*SpecialString)(nil))
|
||||
|
||||
s := TestStruct{}
|
||||
err := injector.Apply(&s)
|
||||
expect(t, err, nil)
|
||||
|
||||
expect(t, s.Dep1, "a dep")
|
||||
expect(t, s.Dep2, "another dep")
|
||||
}
|
||||
|
||||
func Test_InterfaceOf(t *testing.T) {
|
||||
iType := inject.InterfaceOf((*SpecialString)(nil))
|
||||
expect(t, iType.Kind(), reflect.Interface)
|
||||
|
||||
iType = inject.InterfaceOf((**SpecialString)(nil))
|
||||
expect(t, iType.Kind(), reflect.Interface)
|
||||
|
||||
// Expecting nil
|
||||
defer func() {
|
||||
rec := recover()
|
||||
refute(t, rec, nil)
|
||||
}()
|
||||
iType = inject.InterfaceOf((*testing.T)(nil))
|
||||
}
|
||||
|
||||
func Test_InjectorSet(t *testing.T) {
|
||||
injector := inject.New()
|
||||
typ := reflect.TypeOf("string")
|
||||
typSend := reflect.ChanOf(reflect.SendDir, typ)
|
||||
typRecv := reflect.ChanOf(reflect.RecvDir, typ)
|
||||
|
||||
// instantiating unidirectional channels is not possible using reflect
|
||||
// http://golang.org/src/pkg/reflect/value.go?s=60463:60504#L2064
|
||||
chanRecv := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, typ), 0)
|
||||
chanSend := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, typ), 0)
|
||||
|
||||
injector.Set(typSend, chanSend)
|
||||
injector.Set(typRecv, chanRecv)
|
||||
|
||||
expect(t, injector.Get(typSend).IsValid(), true)
|
||||
expect(t, injector.Get(typRecv).IsValid(), true)
|
||||
expect(t, injector.Get(chanSend.Type()).IsValid(), false)
|
||||
}
|
||||
|
||||
|
||||
func Test_InjectorGet(t *testing.T) {
|
||||
injector := inject.New()
|
||||
|
||||
injector.Map("some dependency")
|
||||
|
||||
expect(t, injector.Get(reflect.TypeOf("string")).IsValid(), true)
|
||||
expect(t, injector.Get(reflect.TypeOf(11)).IsValid(), false)
|
||||
}
|
||||
|
||||
func Test_InjectorSetParent(t *testing.T) {
|
||||
injector := inject.New()
|
||||
injector.MapTo("another dep", (*SpecialString)(nil))
|
||||
|
||||
injector2 := inject.New()
|
||||
injector2.SetParent(injector)
|
||||
|
||||
expect(t, injector2.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid(), true)
|
||||
}
|
||||
|
||||
func TestInjectImplementors(t *testing.T) {
|
||||
injector := inject.New()
|
||||
g := &Greeter{"Jeremy"}
|
||||
injector.Map(g)
|
||||
|
||||
expect(t, injector.Get(inject.InterfaceOf((*fmt.Stringer)(nil))).IsValid(), true)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
# auth
|
||||
Martini middleware/handler for http basic authentication.
|
||||
|
||||
[API Reference](http://godoc.org/github.com/codegangsta/martini-contrib/auth)
|
||||
|
||||
## Usage
|
||||
|
||||
~~~ go
|
||||
import (
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/codegangsta/martini-contrib/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
m := martini.Classic()
|
||||
// authenticate every request
|
||||
m.Use(auth.Basic("username", "secretpassword"))
|
||||
m.Run()
|
||||
}
|
||||
|
||||
~~~
|
||||
|
||||
## Authors
|
||||
* [Jeremy Saenz](http://github.com/codegangsta)
|
||||
* [Brendon Murphy](http://github.com/bemurphy)
|
|
@ -1,19 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Basic returns a Handler that authenticates via Basic Auth. Writes a http.StatusUnauthorized
|
||||
// if authentication fails
|
||||
func Basic(username string, password string) http.HandlerFunc {
|
||||
var siteAuth = base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
return func(res http.ResponseWriter, req *http.Request) {
|
||||
auth := req.Header.Get("Authorization")
|
||||
if !SecureCompare(auth, "Basic "+siteAuth) {
|
||||
res.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
|
||||
http.Error(res, "Not Authorized", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/codegangsta/martini"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_BasicAuth(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar"))
|
||||
|
||||
m := martini.New()
|
||||
m.Use(Basic("foo", "bar"))
|
||||
m.Use(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Write([]byte("hello"))
|
||||
})
|
||||
|
||||
r, _ := http.NewRequest("GET", "foo", nil)
|
||||
|
||||
m.ServeHTTP(recorder, r)
|
||||
|
||||
if recorder.Code != 401 {
|
||||
t.Error("Response not 401")
|
||||
}
|
||||
|
||||
if recorder.Body.String() == "hello" {
|
||||
t.Error("Auth block failed")
|
||||
}
|
||||
|
||||
recorder = httptest.NewRecorder()
|
||||
r.Header.Set("Authorization", auth)
|
||||
m.ServeHTTP(recorder, r)
|
||||
|
||||
if recorder.Code == 401 {
|
||||
t.Error("Response is 401")
|
||||
}
|
||||
|
||||
if recorder.Body.String() != "hello" {
|
||||
t.Error("Auth failed, got: ", recorder.Body.String())
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
)
|
||||
|
||||
// SecureCompare performs a constant time compare of two strings to limit timing attacks.
|
||||
func SecureCompare(given string, actual string) bool {
|
||||
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
|
||||
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
|
||||
} else {
|
||||
/* Securely compare actual to itself to keep constant time, but always return false */
|
||||
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var comparetests = []struct {
|
||||
a string
|
||||
b string
|
||||
val bool
|
||||
}{
|
||||
{"foo", "foo", true},
|
||||
{"bar", "bar", true},
|
||||
{"password", "password", true},
|
||||
{"Foo", "foo", false},
|
||||
{"foo", "foobar", false},
|
||||
{"password", "pass", false},
|
||||
}
|
||||
|
||||
func Test_SecureCompare(t *testing.T) {
|
||||
for _, tt := range comparetests {
|
||||
if SecureCompare(tt.a, tt.b) != tt.val {
|
||||
t.Errorf("Expected SecureCompare(%v, %v) to return %v but did not", tt.a, tt.b, tt.val)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
# binding
|
||||
|
||||
Request data binding for Martini.
|
||||
|
||||
[API Reference](http://godoc.org/github.com/codegangsta/martini-contrib/binding)
|
||||
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
Package `binding` provides several middleware for transforming raw request data into populated structs, validating the input, and handling the errors. Each handler is independent and optional.
|
||||
|
||||
#### Bind
|
||||
|
||||
`binding.Bind` is a convenient wrapper over the other handlers in this package. It does the following boilerplate for you:
|
||||
|
||||
1. Deserializes the request data into a struct you supply
|
||||
2. Performs validation with `binding.Validate`
|
||||
3. Bails out with `binding.ErrorHandler` if there are any errors
|
||||
|
||||
Your application (the final handler) will not even see the request if there are any errors.
|
||||
|
||||
It reads the Content-Type of the request to know how to deserialize it, or if the Content-Type is not specified, it tries different deserializers until one returns without errors.
|
||||
|
||||
**Important safety tip:** Don't attempt to bind a pointer to a struct. This will cause a panic [to prevent a race condition](https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659) where every request would be pointing to the same struct.
|
||||
|
||||
#### Form
|
||||
|
||||
`binding.Form` deserializes form data from the request, whether in the query string or as a form-urlencoded payload, and puts the data into a struct you pass in. It then invokes the `binding.Validate` middleware to perform validation. No error handling is performed, but you can get the errors in your handler by receiving a `binding.Errors` type.
|
||||
|
||||
|
||||
#### Json
|
||||
|
||||
`binding.Json` deserializes JSON data in the payload of the request and uses `binding.Validate` to perform validation. Similar to `binding.Form`, no error handling is performed, but you can get the errors and handle them yourself.
|
||||
|
||||
|
||||
#### Validate
|
||||
|
||||
`binding.Validate` receives a populated struct and checks it for errors, first by enforcing the `binding:"required"` value on struct field tags, then by executing the `Validate()` method on the struct, if it is a `binding.Validator`. (See usage below for an example.)
|
||||
|
||||
*Note:* Marking a field as "required" means that you do not allow the zero value for that type (i.e. if you want to allow 0 in an int field, do not make it required).
|
||||
|
||||
|
||||
#### ErrorHandler
|
||||
|
||||
`binding.ErrorHandler` is a small middleware that simply writes a `400` code to the response and also a JSON payload describing the errors, *if* any errors have been mapped to the context. It does nothing if there are no errors.
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
This is a contrived example to show a few different ways to use the `binding` package.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/codegangsta/martini-contrib/binding"
|
||||
)
|
||||
|
||||
type BlogPost struct {
|
||||
Title string `form:"title" json:"title" binding:"required"`
|
||||
Content string `form:"content" json:"content"`
|
||||
Views int `form:"views" json:"views"`
|
||||
unexported string `form:"-"` // skip binding of unexported fields
|
||||
}
|
||||
|
||||
// This method implements binding.Validator and is executed by the binding.Validate middleware
|
||||
func (bp BlogPost) Validate(errors *binding.Errors, req *http.Request) {
|
||||
if req.Header.Get("X-Custom-Thing") == "" {
|
||||
errors.Overall["x-custom-thing"] = "The X-Custom-Thing header is required"
|
||||
}
|
||||
if len(bp.Title) < 4 {
|
||||
errors.Fields["title"] = "Too short; minimum 4 characters"
|
||||
} else if len(bp.Title) > 120 {
|
||||
errors.Fields["title"] = "Too long; maximum 120 characters"
|
||||
}
|
||||
if bp.Views < 0 {
|
||||
errors.Fields["views"] = "Views must be at least 0"
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
m := martini.Classic()
|
||||
|
||||
m.Post("/blog", binding.Bind(BlogPost{}), func(blogpost BlogPost) string {
|
||||
// This function won't execute if there were errors
|
||||
return blogpost.Title
|
||||
})
|
||||
|
||||
m.Get("/blog", binding.Form(BlogPost{}), binding.ErrorHandler, func(blogpost BlogPost) string {
|
||||
// This function won't execute if there were errors
|
||||
return blogpost.Title
|
||||
})
|
||||
|
||||
m.Get("/blog", binding.Form(BlogPost{}), func(blogpost BlogPost, err binding.Errors, resp http.ResponseWriter) string {
|
||||
// This function WILL execute if there are errors because binding.Form doesn't handle errors
|
||||
if err.Count() > 0 {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
return blogpost.Title
|
||||
})
|
||||
|
||||
m.Post("/blog", binding.Json(BlogPost{}), myOwnErrorHandler, func(blogpost BlogPost) string {
|
||||
// By this point, I assume that my own middleware took care of any errors
|
||||
return blogpost.Title
|
||||
})
|
||||
|
||||
m.Run()
|
||||
}
|
||||
```
|
||||
|
||||
## Authors
|
||||
* [Matthew Holt](https://github.com/mholt)
|
||||
* [Michael Whatcott](https://github.com/mdwhatcott)
|
||||
* [Jeremy Saenz](https://github.com/codegangsta)
|
|
@ -1,364 +0,0 @@
|
|||
// Package binding transforms, with validation, a raw request into
|
||||
// a populated structure used by your application logic.
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/codegangsta/martini"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
To the land of Middle-ware Earth:
|
||||
|
||||
One func to rule them all,
|
||||
One func to find them,
|
||||
One func to bring them all,
|
||||
And in this package BIND them.
|
||||
*/
|
||||
|
||||
// Bind accepts a copy of an empty struct and populates it with
|
||||
// values from the request (if deserialization is successful). It
|
||||
// wraps up the functionality of the Form and Json middleware
|
||||
// according to the Content-Type of the request, and it guesses
|
||||
// if no Content-Type is specified. Bind invokes the ErrorHandler
|
||||
// middleware to bail out if errors occurred. If you want to perform
|
||||
// your own error handling, use Form or Json middleware directly.
|
||||
// An interface pointer can be added as a second argument in order
|
||||
// to map the struct to a specific interface.
|
||||
func Bind(obj interface{}, ifacePtr ...interface{}) martini.Handler {
|
||||
return func(context martini.Context, req *http.Request) {
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
|
||||
if strings.Contains(contentType, "form-urlencoded") {
|
||||
context.Invoke(Form(obj, ifacePtr...))
|
||||
} else if strings.Contains(contentType, "multipart/form-data") {
|
||||
context.Invoke(MultipartForm(obj, ifacePtr...))
|
||||
} else if strings.Contains(contentType, "json") {
|
||||
context.Invoke(Json(obj, ifacePtr...))
|
||||
} else {
|
||||
context.Invoke(Json(obj, ifacePtr...))
|
||||
if getErrors(context).Count() > 0 {
|
||||
context.Invoke(Form(obj, ifacePtr...))
|
||||
}
|
||||
}
|
||||
|
||||
context.Invoke(ErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
// Form is middleware to deserialize form-urlencoded data from the request.
|
||||
// It gets data from the form-urlencoded body, if present, or from the
|
||||
// query string. It uses the http.Request.ParseForm() method
|
||||
// to perform deserialization, then reflection is used to map each field
|
||||
// into the struct with the proper type. Structs with primitive slice types
|
||||
// (bool, float, int, string) can support deserialization of repeated form
|
||||
// keys, for example: key=val1&key=val2&key=val3
|
||||
// An interface pointer can be added as a second argument in order
|
||||
// to map the struct to a specific interface.
|
||||
func Form(formStruct interface{}, ifacePtr ...interface{}) martini.Handler {
|
||||
return func(context martini.Context, req *http.Request) {
|
||||
ensureNotPointer(formStruct)
|
||||
formStruct := reflect.New(reflect.TypeOf(formStruct))
|
||||
errors := newErrors()
|
||||
parseErr := req.ParseForm()
|
||||
|
||||
// Format validation of the request body or the URL would add considerable overhead,
|
||||
// and ParseForm does not complain when URL encoding is off.
|
||||
// Because an empty request body or url can also mean absence of all needed values,
|
||||
// it is not in all cases a bad request, so let's return 422.
|
||||
if parseErr != nil {
|
||||
errors.Overall[DeserializationError] = parseErr.Error()
|
||||
}
|
||||
|
||||
mapForm(formStruct, req.Form, errors)
|
||||
|
||||
validateAndMap(formStruct, context, errors, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
func MultipartForm(formStruct interface{}, ifacePtr ...interface{}) martini.Handler {
|
||||
return func(context martini.Context, req *http.Request) {
|
||||
ensureNotPointer(formStruct)
|
||||
formStruct := reflect.New(reflect.TypeOf(formStruct))
|
||||
errors := newErrors()
|
||||
|
||||
// Workaround for multipart forms returning nil instead of an error
|
||||
// when content is not multipart
|
||||
// https://code.google.com/p/go/issues/detail?id=6334
|
||||
multipartReader, err := req.MultipartReader()
|
||||
if err != nil {
|
||||
errors.Overall[DeserializationError] = err.Error()
|
||||
} else {
|
||||
form, parseErr := multipartReader.ReadForm(MaxMemory)
|
||||
|
||||
if parseErr != nil {
|
||||
errors.Overall[DeserializationError] = parseErr.Error()
|
||||
}
|
||||
|
||||
req.MultipartForm = form
|
||||
}
|
||||
|
||||
mapForm(formStruct, req.MultipartForm.Value, errors)
|
||||
|
||||
validateAndMap(formStruct, context, errors, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
// Json is middleware to deserialize a JSON payload from the request
|
||||
// into the struct that is passed in. The resulting struct is then
|
||||
// validated, but no error handling is actually performed here.
|
||||
// An interface pointer can be added as a second argument in order
|
||||
// to map the struct to a specific interface.
|
||||
func Json(jsonStruct interface{}, ifacePtr ...interface{}) martini.Handler {
|
||||
return func(context martini.Context, req *http.Request) {
|
||||
ensureNotPointer(jsonStruct)
|
||||
jsonStruct := reflect.New(reflect.TypeOf(jsonStruct))
|
||||
errors := newErrors()
|
||||
|
||||
if req.Body != nil {
|
||||
defer req.Body.Close()
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(req.Body).Decode(jsonStruct.Interface()); err != nil {
|
||||
errors.Overall[DeserializationError] = err.Error()
|
||||
}
|
||||
|
||||
validateAndMap(jsonStruct, context, errors, ifacePtr...)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate is middleware to enforce required fields. If the struct
|
||||
// passed in is a Validator, then the user-defined Validate method
|
||||
// is executed, and its errors are mapped to the context. This middleware
|
||||
// performs no error handling: it merely detects them and maps them.
|
||||
func Validate(obj interface{}) martini.Handler {
|
||||
return func(context martini.Context, req *http.Request) {
|
||||
errors := newErrors()
|
||||
validateStruct(errors, obj)
|
||||
|
||||
if validator, ok := obj.(Validator); ok {
|
||||
validator.Validate(errors, req)
|
||||
}
|
||||
context.Map(*errors)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func validateStruct(errors *Errors, obj interface{}) {
|
||||
typ := reflect.TypeOf(obj)
|
||||
val := reflect.ValueOf(obj)
|
||||
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
typ = typ.Elem()
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
|
||||
// Allow ignored fields in the struct
|
||||
if field.Tag.Get("form") == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldValue := val.Field(i).Interface()
|
||||
zero := reflect.Zero(field.Type).Interface()
|
||||
|
||||
if strings.Index(field.Tag.Get("binding"), "required") > -1 {
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
validateStruct(errors, fieldValue)
|
||||
} else if reflect.DeepEqual(zero, fieldValue) {
|
||||
errors.Fields[field.Name] = RequireError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mapForm(formStruct reflect.Value, form map[string][]string, errors *Errors) {
|
||||
typ := formStruct.Elem().Type()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
typeField := typ.Field(i)
|
||||
if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" {
|
||||
structField := formStruct.Elem().Field(i)
|
||||
if !structField.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
inputValue, exists := form[inputFieldName]
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
numElems := len(inputValue)
|
||||
if structField.Kind() == reflect.Slice && numElems > 0 {
|
||||
sliceOf := structField.Type().Elem().Kind()
|
||||
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
|
||||
for i := 0; i < numElems; i++ {
|
||||
setWithProperType(sliceOf, inputValue[i], slice.Index(i), inputFieldName, errors)
|
||||
}
|
||||
formStruct.Elem().Field(i).Set(slice)
|
||||
} else {
|
||||
setWithProperType(typeField.Type.Kind(), inputValue[0], structField, inputFieldName, errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandler simply counts the number of errors in the
|
||||
// context and, if more than 0, writes a 400 Bad Request
|
||||
// response and a JSON payload describing the errors with
|
||||
// the "Content-Type" set to "application/json".
|
||||
// Middleware remaining on the stack will not even see the request
|
||||
// if, by this point, there are any errors.
|
||||
// This is a "default" handler, of sorts, and you are
|
||||
// welcome to use your own instead. The Bind middleware
|
||||
// invokes this automatically for convenience.
|
||||
func ErrorHandler(errs Errors, resp http.ResponseWriter) {
|
||||
if errs.Count() > 0 {
|
||||
resp.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, ok := errs.Overall[DeserializationError]; ok {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
} else {
|
||||
resp.WriteHeader(422)
|
||||
}
|
||||
errOutput, _ := json.Marshal(errs)
|
||||
resp.Write(errOutput)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// This sets the value in a struct of an indeterminate type to the
|
||||
// matching value from the request (via Form middleware) in the
|
||||
// same type, so that not all deserialized values have to be strings.
|
||||
// Supported types are string, int, float, and bool.
|
||||
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value, nameInTag string, errors *Errors) {
|
||||
switch valueKind {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if val == "" {
|
||||
val = "0"
|
||||
}
|
||||
intVal, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
errors.Fields[nameInTag] = IntegerTypeError
|
||||
} else {
|
||||
structField.SetInt(int64(intVal))
|
||||
}
|
||||
case reflect.Bool:
|
||||
if val == "" {
|
||||
val = "false"
|
||||
}
|
||||
boolVal, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
errors.Fields[nameInTag] = BooleanTypeError
|
||||
} else {
|
||||
structField.SetBool(boolVal)
|
||||
}
|
||||
case reflect.Float32:
|
||||
if val == "" {
|
||||
val = "0.0"
|
||||
}
|
||||
floatVal, err := strconv.ParseFloat(val, 32)
|
||||
if err != nil {
|
||||
errors.Fields[nameInTag] = FloatTypeError
|
||||
} else {
|
||||
structField.SetFloat(floatVal)
|
||||
}
|
||||
case reflect.Float64:
|
||||
if val == "" {
|
||||
val = "0.0"
|
||||
}
|
||||
floatVal, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
errors.Fields[nameInTag] = FloatTypeError
|
||||
} else {
|
||||
structField.SetFloat(floatVal)
|
||||
}
|
||||
case reflect.String:
|
||||
structField.SetString(val)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't pass in pointers to bind to. Can lead to bugs. See:
|
||||
// https://github.com/codegangsta/martini-contrib/issues/40
|
||||
// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
|
||||
func ensureNotPointer(obj interface{}) {
|
||||
if reflect.TypeOf(obj).Kind() == reflect.Ptr {
|
||||
panic("Pointers are not accepted as binding models")
|
||||
}
|
||||
}
|
||||
|
||||
// Performs validation and combines errors from validation
|
||||
// with errors from deserialization, then maps both the
|
||||
// resulting struct and the errors to the context.
|
||||
func validateAndMap(obj reflect.Value, context martini.Context, errors *Errors, ifacePtr ...interface{}) {
|
||||
context.Invoke(Validate(obj.Interface()))
|
||||
errors.combine(getErrors(context))
|
||||
context.Map(*errors)
|
||||
context.Map(obj.Elem().Interface())
|
||||
if len(ifacePtr) > 0 {
|
||||
context.MapTo(obj.Elem().Interface(), ifacePtr[0])
|
||||
}
|
||||
}
|
||||
|
||||
func newErrors() *Errors {
|
||||
return &Errors{make(map[string]string), make(map[string]string)}
|
||||
}
|
||||
|
||||
func getErrors(context martini.Context) Errors {
|
||||
return context.Get(reflect.TypeOf(Errors{})).Interface().(Errors)
|
||||
}
|
||||
|
||||
func (this *Errors) combine(other Errors) {
|
||||
for key, val := range other.Fields {
|
||||
if _, exists := this.Fields[key]; !exists {
|
||||
this.Fields[key] = val
|
||||
}
|
||||
}
|
||||
for key, val := range other.Overall {
|
||||
if _, exists := this.Overall[key]; !exists {
|
||||
this.Overall[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Total errors is the sum of errors with the request overall
|
||||
// and errors on individual fields.
|
||||
func (self Errors) Count() int {
|
||||
return len(self.Overall) + len(self.Fields)
|
||||
}
|
||||
|
||||
type (
|
||||
// Errors represents the contract of the response body when the
|
||||
// binding step fails before getting to the application.
|
||||
Errors struct {
|
||||
Overall map[string]string `json:"overall"`
|
||||
Fields map[string]string `json:"fields"`
|
||||
}
|
||||
|
||||
// Implement the Validator interface to define your own input
|
||||
// validation before the request even gets to your application.
|
||||
// The Validate method will be executed during the validation phase.
|
||||
Validator interface {
|
||||
Validate(*Errors, *http.Request)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// Maximum amount of memory to use when parsing a multipart form.
|
||||
// Set this to whatever value you prefer; default is 10 MB.
|
||||
MaxMemory = int64(1024 * 1024 * 10)
|
||||
)
|
||||
|
||||
const (
|
||||
RequireError string = "Required"
|
||||
DeserializationError string = "DeserializationError"
|
||||
IntegerTypeError string = "IntegerTypeError"
|
||||
BooleanTypeError string = "BooleanTypeError"
|
||||
FloatTypeError string = "FloatTypeError"
|
||||
)
|
|
@ -1,585 +0,0 @@
|
|||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/codegangsta/martini"
|
||||
)
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
testBind(t, false)
|
||||
}
|
||||
|
||||
func TestBindWithInterface(t *testing.T) {
|
||||
testBind(t, true)
|
||||
}
|
||||
|
||||
func TestMultipartBind(t *testing.T) {
|
||||
index := 0
|
||||
for test, expectStatus := range bindMultipartTests {
|
||||
handler := func(post BlogPost, errors Errors) {
|
||||
handle(test, t, index, post, errors)
|
||||
}
|
||||
recorder := testMultipart(t, test, Bind(BlogPost{}), handler, index)
|
||||
|
||||
if recorder.Code != expectStatus {
|
||||
t.Errorf("On test case %v, got status code %d but expected %d", test, recorder.Code, expectStatus)
|
||||
}
|
||||
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
func TestForm(t *testing.T) {
|
||||
testForm(t, false)
|
||||
}
|
||||
|
||||
func TestFormWithInterface(t *testing.T) {
|
||||
testForm(t, true)
|
||||
}
|
||||
|
||||
func TestMultipartForm(t *testing.T) {
|
||||
for index, test := range multipartformTests {
|
||||
handler := func(post BlogPost, errors Errors) {
|
||||
handle(test, t, index, post, errors)
|
||||
}
|
||||
testMultipart(t, test, MultipartForm(BlogPost{}), handler, index)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipartFormWithInterface(t *testing.T) {
|
||||
for index, test := range multipartformTests {
|
||||
handler := func(post Modeler, errors Errors) {
|
||||
post.Create(test, t, index)
|
||||
}
|
||||
testMultipart(t, test, MultipartForm(BlogPost{}, (*Modeler)(nil)), handler, index)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJson(t *testing.T) {
|
||||
testJson(t, false)
|
||||
}
|
||||
|
||||
func TestJsonWithInterface(t *testing.T) {
|
||||
testJson(t, true)
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
handlerMustErr := func(errors Errors) {
|
||||
if errors.Count() == 0 {
|
||||
t.Error("Expected at least one error, got 0")
|
||||
}
|
||||
}
|
||||
handlerNoErr := func(errors Errors) {
|
||||
if errors.Count() > 0 {
|
||||
t.Error("Expected no errors, got", errors.Count())
|
||||
}
|
||||
}
|
||||
|
||||
performValidationTest(&BlogPost{"", "...", 0, 0, []int{}}, handlerMustErr, t)
|
||||
performValidationTest(&BlogPost{"Good Title", "Good content", 0, 0, []int{}}, handlerNoErr, t)
|
||||
|
||||
performValidationTest(&User{Name: "Jim", Home: Address{"", ""}}, handlerMustErr, t)
|
||||
performValidationTest(&User{Name: "Jim", Home: Address{"required", ""}}, handlerNoErr, t)
|
||||
}
|
||||
|
||||
func handle(test testCase, t *testing.T, index int, post BlogPost, errors Errors) {
|
||||
assertEqualField(t, "Title", index, test.ref.Title, post.Title)
|
||||
assertEqualField(t, "Content", index, test.ref.Content, post.Content)
|
||||
assertEqualField(t, "Views", index, test.ref.Views, post.Views)
|
||||
|
||||
for i := range test.ref.Multiple {
|
||||
if i >= len(post.Multiple) {
|
||||
t.Errorf("Expected: %v (size %d) to have same size as: %v (size %d)", post.Multiple, len(post.Multiple), test.ref.Multiple, len(test.ref.Multiple))
|
||||
break
|
||||
}
|
||||
if test.ref.Multiple[i] != post.Multiple[i] {
|
||||
t.Errorf("Expected: %v to deep equal: %v", post.Multiple, test.ref.Multiple)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if test.ok && errors.Count() > 0 {
|
||||
t.Errorf("%+v should be OK (0 errors), but had errors: %+v", test, errors)
|
||||
} else if !test.ok && errors.Count() == 0 {
|
||||
t.Errorf("%+v should have errors, but was OK (0 errors): %+v", test)
|
||||
}
|
||||
}
|
||||
|
||||
func testBind(t *testing.T, withInterface bool) {
|
||||
index := 0
|
||||
for test, expectStatus := range bindTests {
|
||||
m := martini.Classic()
|
||||
recorder := httptest.NewRecorder()
|
||||
handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
|
||||
binding := Bind(BlogPost{})
|
||||
|
||||
if withInterface {
|
||||
handler = func(post BlogPost, errors Errors) {
|
||||
post.Create(test, t, index)
|
||||
}
|
||||
binding = Bind(BlogPost{}, (*Modeler)(nil))
|
||||
}
|
||||
|
||||
switch test.method {
|
||||
case "GET":
|
||||
m.Get(route, binding, handler)
|
||||
case "POST":
|
||||
m.Post(route, binding, handler)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(test.method, test.path, strings.NewReader(test.payload))
|
||||
req.Header.Add("Content-Type", test.contentType)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
m.ServeHTTP(recorder, req)
|
||||
|
||||
if recorder.Code != expectStatus {
|
||||
t.Errorf("On test case %v, got status code %d but expected %d", test, recorder.Code, expectStatus)
|
||||
}
|
||||
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
func testJson(t *testing.T, withInterface bool) {
|
||||
for index, test := range jsonTests {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
|
||||
binding := Json(BlogPost{})
|
||||
|
||||
if withInterface {
|
||||
handler = func(post BlogPost, errors Errors) {
|
||||
post.Create(test, t, index)
|
||||
}
|
||||
binding = Bind(BlogPost{}, (*Modeler)(nil))
|
||||
}
|
||||
|
||||
m := martini.Classic()
|
||||
switch test.method {
|
||||
case "GET":
|
||||
m.Get(route, binding, handler)
|
||||
case "POST":
|
||||
m.Post(route, binding, handler)
|
||||
case "PUT":
|
||||
m.Put(route, binding, handler)
|
||||
case "DELETE":
|
||||
m.Delete(route, binding, handler)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(test.method, route, strings.NewReader(test.payload))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
m.ServeHTTP(recorder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func testForm(t *testing.T, withInterface bool) {
|
||||
for index, test := range formTests {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
|
||||
binding := Form(BlogPost{})
|
||||
|
||||
if withInterface {
|
||||
handler = func(post BlogPost, errors Errors) {
|
||||
post.Create(test, t, index)
|
||||
}
|
||||
binding = Form(BlogPost{}, (*Modeler)(nil))
|
||||
}
|
||||
|
||||
m := martini.Classic()
|
||||
switch test.method {
|
||||
case "GET":
|
||||
m.Get(route, binding, handler)
|
||||
case "POST":
|
||||
m.Post(route, binding, handler)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(test.method, test.path, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
m.ServeHTTP(recorder, req)
|
||||
}
|
||||
}
|
||||
|
||||
func testMultipart(t *testing.T, test testCase, middleware martini.Handler, handler martini.Handler, index int) *httptest.ResponseRecorder {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
m := martini.Classic()
|
||||
m.Post(route, middleware, handler)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
writer.WriteField("title", test.ref.Title)
|
||||
writer.WriteField("content", test.ref.Content)
|
||||
writer.WriteField("views", strconv.Itoa(test.ref.Views))
|
||||
if len(test.ref.Multiple) != 0 {
|
||||
for _, value := range test.ref.Multiple {
|
||||
writer.WriteField("multiple", strconv.Itoa(value))
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(test.method, test.path, body)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
m.ServeHTTP(recorder, req)
|
||||
|
||||
return recorder
|
||||
}
|
||||
|
||||
func assertEqualField(t *testing.T, fieldname string, testcasenumber int, expected interface{}, got interface{}) {
|
||||
if expected != got {
|
||||
t.Errorf("%s: expected=%s, got=%s in test case %d\n", fieldname, expected, got, testcasenumber)
|
||||
}
|
||||
}
|
||||
|
||||
func performValidationTest(data interface{}, handler func(Errors), t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
m := martini.Classic()
|
||||
m.Get(route, Validate(data), handler)
|
||||
|
||||
req, err := http.NewRequest("GET", route, nil)
|
||||
if err != nil {
|
||||
t.Error("HTTP error:", err)
|
||||
}
|
||||
|
||||
m.ServeHTTP(recorder, req)
|
||||
}
|
||||
|
||||
func (self BlogPost) Validate(errors *Errors, req *http.Request) {
|
||||
if len(self.Title) < 4 {
|
||||
errors.Fields["Title"] = "Too short; minimum 4 characters"
|
||||
}
|
||||
if len(self.Content) > 1024 {
|
||||
errors.Fields["Content"] = "Too long; maximum 1024 characters"
|
||||
}
|
||||
if len(self.Content) < 5 {
|
||||
errors.Fields["Content"] = "Too short; minimum 5 characters"
|
||||
}
|
||||
}
|
||||
|
||||
func (self BlogPost) Create(test testCase, t *testing.T, index int) {
|
||||
assertEqualField(t, "Title", index, test.ref.Title, self.Title)
|
||||
assertEqualField(t, "Content", index, test.ref.Content, self.Content)
|
||||
assertEqualField(t, "Views", index, test.ref.Views, self.Views)
|
||||
|
||||
for i := range test.ref.Multiple {
|
||||
if i >= len(self.Multiple) {
|
||||
t.Errorf("Expected: %v (size %d) to have same size as: %v (size %d)", self.Multiple, len(self.Multiple), test.ref.Multiple, len(test.ref.Multiple))
|
||||
break
|
||||
}
|
||||
if test.ref.Multiple[i] != self.Multiple[i] {
|
||||
t.Errorf("Expected: %v to deep equal: %v", self.Multiple, test.ref.Multiple)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self BlogSection) Create(test testCase, t *testing.T, index int) {
|
||||
// intentionally left empty
|
||||
}
|
||||
|
||||
type (
|
||||
testCase struct {
|
||||
method string
|
||||
path string
|
||||
payload string
|
||||
contentType string
|
||||
ok bool
|
||||
ref *BlogPost
|
||||
}
|
||||
|
||||
Modeler interface {
|
||||
Create(test testCase, t *testing.T, index int)
|
||||
}
|
||||
|
||||
BlogPost struct {
|
||||
Title string `form:"title" json:"title" binding:"required"`
|
||||
Content string `form:"content" json:"content"`
|
||||
Views int `form:"views" json:"views"`
|
||||
internal int `form:"-"`
|
||||
Multiple []int `form:"multiple"`
|
||||
}
|
||||
|
||||
BlogSection struct {
|
||||
Title string `form:"title" json:"title" binding:"required"`
|
||||
Content string `form:"content" json:"content"`
|
||||
}
|
||||
|
||||
User struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Home Address `json:"address" binding:"required"`
|
||||
}
|
||||
|
||||
Address struct {
|
||||
Street1 string `json:"street1" binding:"required"`
|
||||
Street2 string `json:"street2"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
bindTests = map[testCase]int{
|
||||
// These should bail at the deserialization/binding phase
|
||||
testCase{
|
||||
"POST",
|
||||
path,
|
||||
`{ bad JSON `,
|
||||
"application/json",
|
||||
false,
|
||||
new(BlogPost),
|
||||
}: http.StatusBadRequest,
|
||||
testCase{
|
||||
"POST",
|
||||
path,
|
||||
`not multipart but has content-type`,
|
||||
"multipart/form-data",
|
||||
false,
|
||||
new(BlogPost),
|
||||
}: http.StatusBadRequest,
|
||||
testCase{
|
||||
"POST",
|
||||
path,
|
||||
`no content-type and not URL-encoded or JSON"`,
|
||||
"",
|
||||
false,
|
||||
new(BlogPost),
|
||||
}: http.StatusBadRequest,
|
||||
|
||||
// These should deserialize, then bail at the validation phase
|
||||
testCase{
|
||||
"POST",
|
||||
path + "?title= This is wrong ",
|
||||
`not URL-encoded but has content-type`,
|
||||
"x-www-form-urlencoded",
|
||||
false,
|
||||
new(BlogPost),
|
||||
}: 422, // according to comments in Form() -> although the request is not url encoded, ParseForm does not complain
|
||||
testCase{
|
||||
"GET",
|
||||
path + "?content=This+is+the+content",
|
||||
``,
|
||||
"x-www-form-urlencoded",
|
||||
false,
|
||||
&BlogPost{Title: "", Content: "This is the content"},
|
||||
}: 422,
|
||||
testCase{
|
||||
"GET",
|
||||
path + "",
|
||||
`{"content":"", "title":"Blog Post Title"}`,
|
||||
"application/json",
|
||||
false,
|
||||
&BlogPost{Title: "Blog Post Title", Content: ""},
|
||||
}: 422,
|
||||
|
||||
// These should succeed
|
||||
testCase{
|
||||
"GET",
|
||||
path + "",
|
||||
`{"content":"This is the content", "title":"Blog Post Title"}`,
|
||||
"application/json",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
|
||||
}: http.StatusOK,
|
||||
testCase{
|
||||
"GET",
|
||||
path + "?content=This+is+the+content&title=Blog+Post+Title",
|
||||
``,
|
||||
"",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
|
||||
}: http.StatusOK,
|
||||
testCase{
|
||||
"GET",
|
||||
path + "?content=This is the content&title=Blog+Post+Title",
|
||||
`{"content":"This is the content", "title":"Blog Post Title"}`,
|
||||
"",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
|
||||
}: http.StatusOK,
|
||||
testCase{
|
||||
"GET",
|
||||
path + "",
|
||||
`{"content":"This is the content", "title":"Blog Post Title"}`,
|
||||
"",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
|
||||
}: http.StatusOK,
|
||||
}
|
||||
|
||||
bindMultipartTests = map[testCase]int{
|
||||
// This should deserialize, then bail at the validation phase
|
||||
testCase{
|
||||
"POST",
|
||||
path,
|
||||
"",
|
||||
"multipart/form-data",
|
||||
false,
|
||||
&BlogPost{Title: "", Content: "This is the content"},
|
||||
}: 422,
|
||||
// This should succeed
|
||||
testCase{
|
||||
"POST",
|
||||
path,
|
||||
"",
|
||||
"multipart/form-data",
|
||||
true,
|
||||
&BlogPost{Title: "This is the Title", Content: "This is the content"},
|
||||
}: http.StatusOK,
|
||||
}
|
||||
|
||||
formTests = []testCase{
|
||||
{
|
||||
"GET",
|
||||
path + "?content=This is the content",
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
&BlogPost{Title: "", Content: "This is the content"},
|
||||
},
|
||||
{
|
||||
"POST",
|
||||
path + "?content=This+is+the+content&title=Blog+Post+Title&views=3",
|
||||
"",
|
||||
"",
|
||||
false, // false because POST requests should have a body, not just a query string
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3},
|
||||
},
|
||||
{
|
||||
"GET",
|
||||
path + "?content=This+is+the+content&title=Blog+Post+Title&views=3&multiple=5&multiple=10&multiple=15&multiple=20",
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3, Multiple: []int{5, 10, 15, 20}},
|
||||
},
|
||||
}
|
||||
|
||||
multipartformTests = []testCase{
|
||||
{
|
||||
"POST",
|
||||
path,
|
||||
"",
|
||||
"multipart/form-data",
|
||||
false,
|
||||
&BlogPost{Title: "", Content: "This is the content"},
|
||||
},
|
||||
{
|
||||
"POST",
|
||||
path,
|
||||
"",
|
||||
"multipart/form-data",
|
||||
false,
|
||||
&BlogPost{Title: "Blog Post Title", Views: 3},
|
||||
},
|
||||
{
|
||||
"POST",
|
||||
path,
|
||||
"",
|
||||
"multipart/form-data",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3, Multiple: []int{5, 10, 15, 20}},
|
||||
},
|
||||
}
|
||||
|
||||
jsonTests = []testCase{
|
||||
// bad requests
|
||||
{
|
||||
"GET",
|
||||
"",
|
||||
`{blah blah blah}`,
|
||||
"",
|
||||
false,
|
||||
&BlogPost{},
|
||||
},
|
||||
{
|
||||
"POST",
|
||||
"",
|
||||
`{asdf}`,
|
||||
"",
|
||||
false,
|
||||
&BlogPost{},
|
||||
},
|
||||
{
|
||||
"PUT",
|
||||
"",
|
||||
`{blah blah blah}`,
|
||||
"",
|
||||
false,
|
||||
&BlogPost{},
|
||||
},
|
||||
{
|
||||
"DELETE",
|
||||
"",
|
||||
`{;sdf _SDf- }`,
|
||||
"",
|
||||
false,
|
||||
&BlogPost{},
|
||||
},
|
||||
|
||||
// Valid-JSON requests
|
||||
{
|
||||
"GET",
|
||||
"",
|
||||
`{"content":"This is the content"}`,
|
||||
"",
|
||||
false,
|
||||
&BlogPost{Title: "", Content: "This is the content"},
|
||||
},
|
||||
{
|
||||
"POST",
|
||||
"",
|
||||
`{}`,
|
||||
"application/json",
|
||||
false,
|
||||
&BlogPost{Title: "", Content: ""},
|
||||
},
|
||||
{
|
||||
"POST",
|
||||
"",
|
||||
`{"content":"This is the content", "title":"Blog Post Title"}`,
|
||||
"",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
|
||||
},
|
||||
{
|
||||
"PUT",
|
||||
"",
|
||||
`{"content":"This is the content", "title":"Blog Post Title"}`,
|
||||
"",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
|
||||
},
|
||||
{
|
||||
"DELETE",
|
||||
"",
|
||||
`{"content":"This is the content", "title":"Blog Post Title"}`,
|
||||
"",
|
||||
true,
|
||||
&BlogPost{Title: "Blog Post Title", Content: "This is the content"},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
route = "/blogposts/create"
|
||||
path = "http://localhost:3000" + route
|
||||
)
|
|
@ -1,26 +0,0 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
|
||||
/.godeps
|
||||
/.envrc
|
|
@ -1 +0,0 @@
|
|||
github.com/codegangsta/inject master
|
|
@ -1,20 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Jeremy Saenz
|
||||
|
||||
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.
|
|
@ -1,381 +0,0 @@
|
|||
# Martini [![wercker status](https://app.wercker.com/status/9b7dbc6e2654b604cd694d191c3d5487/s/master "wercker status")](https://app.wercker.com/project/bykey/9b7dbc6e2654b604cd694d191c3d5487)[![GoDoc](https://godoc.org/github.com/go-martini/martini?status.png)](http://godoc.org/github.com/go-martini/martini)
|
||||
|
||||
Martini is a powerful package for quickly writing modular web applications/services in Golang.
|
||||
|
||||
Language Translations:
|
||||
* [简体中文](translations/README_zh_cn.md)
|
||||
* [Português Brasileiro (pt_BR)](translations/README_pt_br.md)
|
||||
* [Español](translations/README_es_ES.md)
|
||||
* [한국어 번역](translations/README_ko_kr.md)
|
||||
* [Русский](translations/README_ru_RU.md)
|
||||
* [日本語](translations/README_ja_JP.md)
|
||||
* [French](translations/README_fr_FR.md)
|
||||
* [Turkish](translations/README_tr_TR.md)
|
||||
|
||||
## Getting Started
|
||||
|
||||
After installing Go and setting up your [GOPATH](http://golang.org/doc/code.html#GOPATH), create your first `.go` file. We'll call it `server.go`.
|
||||
|
||||
~~~ go
|
||||
package main
|
||||
|
||||
import "github.com/go-martini/martini"
|
||||
|
||||
func main() {
|
||||
m := martini.Classic()
|
||||
m.Get("/", func() string {
|
||||
return "Hello world!"
|
||||
})
|
||||
m.Run()
|
||||
}
|
||||
~~~
|
||||
|
||||
Then install the Martini package (**go 1.1** or greater is required):
|
||||
~~~
|
||||
go get github.com/go-martini/martini
|
||||
~~~
|
||||
|
||||
Then run your server:
|
||||
~~~
|
||||
go run server.go
|
||||
~~~
|
||||
|
||||
You will now have a Martini webserver running on `localhost:3000`.
|
||||
|
||||
## Getting Help
|
||||
|
||||
Join the [Mailing list](https://groups.google.com/forum/#!forum/martini-go)
|
||||
|
||||
Watch the [Demo Video](http://martini.codegangsta.io/#demo)
|
||||
|
||||
Ask questions on Stackoverflow using the [martini tag](http://stackoverflow.com/questions/tagged/martini)
|
||||
|
||||
GoDoc [documentation](http://godoc.org/github.com/go-martini/martini)
|
||||
|
||||
|
||||
## Features
|
||||
* Extremely simple to use.
|
||||
* Non-intrusive design.
|
||||
* Plays nice with other Golang packages.
|
||||
* Awesome path matching and routing.
|
||||
* Modular design - Easy to add functionality, easy to rip stuff out.
|
||||
* Lots of good handlers/middlewares to use.
|
||||
* Great 'out of the box' feature set.
|
||||
* **Fully compatible with the [http.HandlerFunc](http://godoc.org/net/http#HandlerFunc) interface.**
|
||||
* Default document serving (e.g., for serving AngularJS apps in HTML5 mode).
|
||||
|
||||
## More Middleware
|
||||
For more middleware and functionality, check out the repositories in the [martini-contrib](https://github.com/martini-contrib) organization.
|
||||
|
||||
## Table of Contents
|
||||
* [Classic Martini](#classic-martini)
|
||||
* [Handlers](#handlers)
|
||||
* [Routing](#routing)
|
||||
* [Services](#services)
|
||||
* [Serving Static Files](#serving-static-files)
|
||||
* [Middleware Handlers](#middleware-handlers)
|
||||
* [Next()](#next)
|
||||
* [Martini Env](#martini-env)
|
||||
* [FAQ](#faq)
|
||||
|
||||
## Classic Martini
|
||||
To get up and running quickly, [martini.Classic()](http://godoc.org/github.com/go-martini/martini#Classic) provides some reasonable defaults that work well for most web applications:
|
||||
~~~ go
|
||||
m := martini.Classic()
|
||||
// ... middleware and routing goes here
|
||||
m.Run()
|
||||
~~~
|
||||
|
||||
Below is some of the functionality [martini.Classic()](http://godoc.org/github.com/go-martini/martini#Classic) pulls in automatically:
|
||||
* Request/Response Logging - [martini.Logger](http://godoc.org/github.com/go-martini/martini#Logger)
|
||||
* Panic Recovery - [martini.Recovery](http://godoc.org/github.com/go-martini/martini#Recovery)
|
||||
* Static File serving - [martini.Static](http://godoc.org/github.com/go-martini/martini#Static)
|
||||
* Routing - [martini.Router](http://godoc.org/github.com/go-martini/martini#Router)
|
||||
|
||||
### Handlers
|
||||
Handlers are the heart and soul of Martini. A handler is basically any kind of callable function:
|
||||
~~~ go
|
||||
m.Get("/", func() {
|
||||
println("hello world")
|
||||
})
|
||||
~~~
|
||||
|
||||
#### Return Values
|
||||
If a handler returns something, Martini will write the result to the current [http.ResponseWriter](http://godoc.org/net/http#ResponseWriter) as a string:
|
||||
~~~ go
|
||||
m.Get("/", func() string {
|
||||
return "hello world" // HTTP 200 : "hello world"
|
||||
})
|
||||
~~~
|
||||
|
||||
You can also optionally return a status code:
|
||||
~~~ go
|
||||
m.Get("/", func() (int, string) {
|
||||
return 418, "i'm a teapot" // HTTP 418 : "i'm a teapot"
|
||||
})
|
||||
~~~
|
||||
|
||||
#### Service Injection
|
||||
Handlers are invoked via reflection. Martini makes use of *Dependency Injection* to resolve dependencies in a Handlers argument list. **This makes Martini completely compatible with golang's `http.HandlerFunc` interface.**
|
||||
|
||||
If you add an argument to your Handler, Martini will search its list of services and attempt to resolve the dependency via type assertion:
|
||||
~~~ go
|
||||
m.Get("/", func(res http.ResponseWriter, req *http.Request) { // res and req are injected by Martini
|
||||
res.WriteHeader(200) // HTTP 200
|
||||
})
|
||||
~~~
|
||||
|
||||
The following services are included with [martini.Classic()](http://godoc.org/github.com/go-martini/martini#Classic):
|
||||
* [*log.Logger](http://godoc.org/log#Logger) - Global logger for Martini.
|
||||
* [martini.Context](http://godoc.org/github.com/go-martini/martini#Context) - http request context.
|
||||
* [martini.Params](http://godoc.org/github.com/go-martini/martini#Params) - `map[string]string` of named params found by route matching.
|
||||
* [martini.Routes](http://godoc.org/github.com/go-martini/martini#Routes) - Route helper service.
|
||||
* [http.ResponseWriter](http://godoc.org/net/http/#ResponseWriter) - http Response writer interface.
|
||||
* [*http.Request](http://godoc.org/net/http/#Request) - http Request.
|
||||
|
||||
### Routing
|
||||
In Martini, a route is an HTTP method paired with a URL-matching pattern.
|
||||
Each route can take one or more handler methods:
|
||||
~~~ go
|
||||
m.Get("/", func() {
|
||||
// show something
|
||||
})
|
||||
|
||||
m.Patch("/", func() {
|
||||
// update something
|
||||
})
|
||||
|
||||
m.Post("/", func() {
|
||||
// create something
|
||||
})
|
||||
|
||||
m.Put("/", func() {
|
||||
// replace something
|
||||
})
|
||||
|
||||
m.Delete("/", func() {
|
||||
// destroy something
|
||||
})
|
||||
|
||||
m.Options("/", func() {
|
||||
// http options
|
||||
})
|
||||
|
||||
m.NotFound(func() {
|
||||
// handle 404
|
||||
})
|
||||
~~~
|
||||
|
||||
Routes are matched in the order they are defined. The first route that
|
||||
matches the request is invoked.
|
||||
|
||||
Route patterns may include named parameters, accessible via the [martini.Params](http://godoc.org/github.com/go-martini/martini#Params) service:
|
||||
~~~ go
|
||||
m.Get("/hello/:name", func(params martini.Params) string {
|
||||
return "Hello " + params["name"]
|
||||
})
|
||||
~~~
|
||||
|
||||
Routes can be matched with globs:
|
||||
~~~ go
|
||||
m.Get("/hello/**", func(params martini.Params) string {
|
||||
return "Hello " + params["_1"]
|
||||
})
|
||||
~~~
|
||||
|
||||
Regular expressions can be used as well:
|
||||
~~~go
|
||||
m.Get("/hello/(?P<name>[a-zA-Z]+)", func(params martini.Params) string {
|
||||
return fmt.Sprintf ("Hello %s", params["name"])
|
||||
})
|
||||
~~~
|
||||
Take a look at the [Go documentation](http://golang.org/pkg/regexp/syntax/) for more info about regular expressions syntax .
|
||||
|
||||
Route handlers can be stacked on top of each other, which is useful for things like authentication and authorization:
|
||||
~~~ go
|
||||
m.Get("/secret", authorize, func() {
|
||||
// this will execute as long as authorize doesn't write a response
|
||||
})
|
||||
~~~
|
||||
|
||||
Route groups can be added too using the Group method.
|
||||
~~~ go
|
||||
m.Group("/books", func(r martini.Router) {
|
||||
r.Get("/:id", GetBooks)
|
||||
r.Post("/new", NewBook)
|
||||
r.Put("/update/:id", UpdateBook)
|
||||
r.Delete("/delete/:id", DeleteBook)
|
||||
})
|
||||
~~~
|
||||
|
||||
Just like you can pass middlewares to a handler you can pass middlewares to groups.
|
||||
~~~ go
|
||||
m.Group("/books", func(r martini.Router) {
|
||||
r.Get("/:id", GetBooks)
|
||||
r.Post("/new", NewBook)
|
||||
r.Put("/update/:id", UpdateBook)
|
||||
r.Delete("/delete/:id", DeleteBook)
|
||||
}, MyMiddleware1, MyMiddleware2)
|
||||
~~~
|
||||
|
||||
### Services
|
||||
Services are objects that are available to be injected into a Handler's argument list. You can map a service on a *Global* or *Request* level.
|
||||
|
||||
#### Global Mapping
|
||||
A Martini instance implements the inject.Injector interface, so mapping a service is easy:
|
||||
~~~ go
|
||||
db := &MyDatabase{}
|
||||
m := martini.Classic()
|
||||
m.Map(db) // the service will be available to all handlers as *MyDatabase
|
||||
// ...
|
||||
m.Run()
|
||||
~~~
|
||||
|
||||
#### Request-Level Mapping
|
||||
Mapping on the request level can be done in a handler via [martini.Context](http://godoc.org/github.com/go-martini/martini#Context):
|
||||
~~~ go
|
||||
func MyCustomLoggerHandler(c martini.Context, req *http.Request) {
|
||||
logger := &MyCustomLogger{req}
|
||||
c.Map(logger) // mapped as *MyCustomLogger
|
||||
}
|
||||
~~~
|
||||
|
||||
#### Mapping values to Interfaces
|
||||
One of the most powerful parts about services is the ability to map a service to an interface. For instance, if you wanted to override the [http.ResponseWriter](http://godoc.org/net/http#ResponseWriter) with an object that wrapped it and performed extra operations, you can write the following handler:
|
||||
~~~ go
|
||||
func WrapResponseWriter(res http.ResponseWriter, c martini.Context) {
|
||||
rw := NewSpecialResponseWriter(res)
|
||||
c.MapTo(rw, (*http.ResponseWriter)(nil)) // override ResponseWriter with our wrapper ResponseWriter
|
||||
}
|
||||
~~~
|
||||
|
||||
### Serving Static Files
|
||||
A [martini.Classic()](http://godoc.org/github.com/go-martini/martini#Classic) instance automatically serves static files from the "public" directory in the root of your server.
|
||||
You can serve from more directories by adding more [martini.Static](http://godoc.org/github.com/go-martini/martini#Static) handlers.
|
||||
~~~ go
|
||||
m.Use(martini.Static("assets")) // serve from the "assets" directory as well
|
||||
~~~
|
||||
|
||||
#### Serving a Default Document
|
||||
You can specify the URL of a local file to serve when the requested URL is not
|
||||
found. You can also specify an exclusion prefix so that certain URLs are ignored.
|
||||
This is useful for servers that serve both static files and have additional
|
||||
handlers defined (e.g., REST API). When doing so, it's useful to define the
|
||||
static handler as a part of the NotFound chain.
|
||||
|
||||
The following example serves the `/index.html` file whenever any URL is
|
||||
requested that does not match any local file and does not start with `/api/v`:
|
||||
~~~ go
|
||||
static := martini.Static("assets", martini.StaticOptions{Fallback: "/index.html", Exclude: "/api/v"})
|
||||
m.NotFound(static, http.NotFound)
|
||||
~~~
|
||||
|
||||
## Middleware Handlers
|
||||
Middleware Handlers sit between the incoming http request and the router. In essence they are no different than any other Handler in Martini. You can add a middleware handler to the stack like so:
|
||||
~~~ go
|
||||
m.Use(func() {
|
||||
// do some middleware stuff
|
||||
})
|
||||
~~~
|
||||
|
||||
You can have full control over the middleware stack with the `Handlers` function. This will replace any handlers that have been previously set:
|
||||
~~~ go
|
||||
m.Handlers(
|
||||
Middleware1,
|
||||
Middleware2,
|
||||
Middleware3,
|
||||
)
|
||||
~~~
|
||||
|
||||
Middleware Handlers work really well for things like logging, authorization, authentication, sessions, gzipping, error pages and any other operations that must happen before or after an http request:
|
||||
~~~ go
|
||||
// validate an api key
|
||||
m.Use(func(res http.ResponseWriter, req *http.Request) {
|
||||
if req.Header.Get("X-API-KEY") != "secret123" {
|
||||
res.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
})
|
||||
~~~
|
||||
|
||||
### Next()
|
||||
[Context.Next()](http://godoc.org/github.com/go-martini/martini#Context) is an optional function that Middleware Handlers can call to yield the until after the other Handlers have been executed. This works really well for any operations that must happen after an http request:
|
||||
~~~ go
|
||||
// log before and after a request
|
||||
m.Use(func(c martini.Context, log *log.Logger){
|
||||
log.Println("before a request")
|
||||
|
||||
c.Next()
|
||||
|
||||
log.Println("after a request")
|
||||
})
|
||||
~~~
|
||||
|
||||
## Martini Env
|
||||
|
||||
Some Martini handlers make use of the `martini.Env` global variable to provide special functionality for development environments vs production environments. It is recommended that the `MARTINI_ENV=production` environment variable to be set when deploying a Martini server into a production environment.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Where do I find middleware X?
|
||||
|
||||
Start by looking in the [martini-contrib](https://github.com/martini-contrib) projects. If it is not there feel free to contact a martini-contrib team member about adding a new repo to the organization.
|
||||
|
||||
* [auth](https://github.com/martini-contrib/auth) - Handlers for authentication.
|
||||
* [binding](https://github.com/martini-contrib/binding) - Handler for mapping/validating a raw request into a structure.
|
||||
* [gzip](https://github.com/martini-contrib/gzip) - Handler for adding gzip compress to requests
|
||||
* [render](https://github.com/martini-contrib/render) - Handler that provides a service for easily rendering JSON and HTML templates.
|
||||
* [acceptlang](https://github.com/martini-contrib/acceptlang) - Handler for parsing the `Accept-Language` HTTP header.
|
||||
* [sessions](https://github.com/martini-contrib/sessions) - Handler that provides a Session service.
|
||||
* [strip](https://github.com/martini-contrib/strip) - URL Prefix stripping.
|
||||
* [method](https://github.com/martini-contrib/method) - HTTP method overriding via Header or form fields.
|
||||
* [secure](https://github.com/martini-contrib/secure) - Implements a few quick security wins.
|
||||
* [encoder](https://github.com/martini-contrib/encoder) - Encoder service for rendering data in several formats and content negotiation.
|
||||
* [cors](https://github.com/martini-contrib/cors) - Handler that enables CORS support.
|
||||
* [oauth2](https://github.com/martini-contrib/oauth2) - Handler that provides OAuth 2.0 login for Martini apps. Google Sign-in, Facebook Connect and Github login is supported.
|
||||
* [vauth](https://github.com/rafecolton/vauth) - Handlers for vender webhook authentication (currently GitHub and TravisCI)
|
||||
|
||||
### How do I integrate with existing servers?
|
||||
|
||||
A Martini instance implements `http.Handler`, so it can easily be used to serve subtrees
|
||||
on existing Go servers. For example this is a working Martini app for Google App Engine:
|
||||
|
||||
~~~ go
|
||||
package hello
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/go-martini/martini"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m := martini.Classic()
|
||||
m.Get("/", func() string {
|
||||
return "Hello world!"
|
||||
})
|
||||
http.Handle("/", m)
|
||||
}
|
||||
~~~
|
||||
|
||||
### How do I change the port/host?
|
||||
|
||||
Martini's `Run` function looks for the PORT and HOST environment variables and uses those. Otherwise Martini will default to localhost:3000.
|
||||
To have more flexibility over port and host, use the `martini.RunOnAddr` function instead.
|
||||
|
||||
~~~ go
|
||||
m := martini.Classic()
|
||||
// ...
|
||||
log.Fatal(m.RunOnAddr(":8080"))
|
||||
~~~
|
||||
|
||||
### Live code reload?
|
||||
|
||||
[gin](https://github.com/codegangsta/gin) and [fresh](https://github.com/pilu/fresh) both live reload martini apps.
|
||||
|
||||
## Contributing
|
||||
Martini is meant to be kept tiny and clean. Most contributions should end up in a repository in the [martini-contrib](https://github.com/martini-contrib) organization. If you do have a contribution for the core of Martini feel free to put up a Pull Request.
|
||||
|
||||
## About
|
||||
|
||||
Inspired by [express](https://github.com/visionmedia/express) and [sinatra](https://github.com/sinatra/sinatra)
|
||||
|
||||
Martini is obsessively designed by none other than the [Code Gangsta](http://codegangsta.io/)
|
|
@ -1,31 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Envs
|
||||
const (
|
||||
Dev string = "development"
|
||||
Prod string = "production"
|
||||
Test string = "test"
|
||||
)
|
||||
|
||||
// Env is the environment that Martini is executing in. The MARTINI_ENV is read on initialization to set this variable.
|
||||
var Env = Dev
|
||||
var Root string
|
||||
|
||||
func setENV(e string) {
|
||||
if len(e) > 0 {
|
||||
Env = e
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
setENV(os.Getenv("MARTINI_ENV"))
|
||||
var err error
|
||||
Root, err = os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_SetENV(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"", "development"},
|
||||
{"not_development", "not_development"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
setENV(test.in)
|
||||
if Env != test.out {
|
||||
expect(t, Env, test.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Root(t *testing.T) {
|
||||
if len(Root) == 0 {
|
||||
t.Errorf("Expected root path will be set")
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
// +build !go1.1
|
||||
|
||||
package martini
|
||||
|
||||
func MartiniDoesNotSupportGo1Point0() {
|
||||
"Martini requires Go 1.1 or greater."
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logger returns a middleware handler that logs the request as it goes in and the response as it goes out.
|
||||
func Logger() Handler {
|
||||
return func(res http.ResponseWriter, req *http.Request, c Context, log *log.Logger) {
|
||||
start := time.Now()
|
||||
|
||||
addr := req.Header.Get("X-Real-IP")
|
||||
if addr == "" {
|
||||
addr = req.Header.Get("X-Forwarded-For")
|
||||
if addr == "" {
|
||||
addr = req.RemoteAddr
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Started %s %s for %s", req.Method, req.URL.Path, addr)
|
||||
|
||||
rw := res.(ResponseWriter)
|
||||
c.Next()
|
||||
|
||||
log.Printf("Completed %v %s in %v\n", rw.Status(), http.StatusText(rw.Status()), time.Since(start))
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_Logger(t *testing.T) {
|
||||
buff := bytes.NewBufferString("")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
m := New()
|
||||
// replace log for testing
|
||||
m.Map(log.New(buff, "[martini] ", 0))
|
||||
m.Use(Logger())
|
||||
m.Use(func(res http.ResponseWriter) {
|
||||
res.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost:3000/foobar", nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
m.ServeHTTP(recorder, req)
|
||||
expect(t, recorder.Code, http.StatusNotFound)
|
||||
refute(t, len(buff.String()), 0)
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
// Package martini is a powerful package for quickly writing modular web applications/services in Golang.
|
||||
//
|
||||
// For a full guide visit http://github.com/go-martini/martini
|
||||
//
|
||||
// package main
|
||||
//
|
||||
// import "github.com/go-martini/martini"
|
||||
//
|
||||
// func main() {
|
||||
// m := martini.Classic()
|
||||
//
|
||||
// m.Get("/", func() string {
|
||||
// return "Hello world!"
|
||||
// })
|
||||
//
|
||||
// m.Run()
|
||||
// }
|
||||
package martini
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/codegangsta/inject"
|
||||
)
|
||||
|
||||
// Martini represents the top level web application. inject.Injector methods can be invoked to map services on a global level.
|
||||
type Martini struct {
|
||||
inject.Injector
|
||||
handlers []Handler
|
||||
action Handler
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// New creates a bare bones Martini instance. Use this method if you want to have full control over the middleware that is used.
|
||||
func New() *Martini {
|
||||
m := &Martini{Injector: inject.New(), action: func() {}, logger: log.New(os.Stdout, "[martini] ", 0)}
|
||||
m.Map(m.logger)
|
||||
m.Map(defaultReturnHandler())
|
||||
return m
|
||||
}
|
||||
|
||||
// Handlers sets the entire middleware stack with the given Handlers. This will clear any current middleware handlers.
|
||||
// Will panic if any of the handlers is not a callable function
|
||||
func (m *Martini) Handlers(handlers ...Handler) {
|
||||
m.handlers = make([]Handler, 0)
|
||||
for _, handler := range handlers {
|
||||
m.Use(handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Action sets the handler that will be called after all the middleware has been invoked. This is set to martini.Router in a martini.Classic().
|
||||
func (m *Martini) Action(handler Handler) {
|
||||
validateHandler(handler)
|
||||
m.action = handler
|
||||
}
|
||||
|
||||
// Use adds a middleware Handler to the stack. Will panic if the handler is not a callable func. Middleware Handlers are invoked in the order that they are added.
|
||||
func (m *Martini) Use(handler Handler) {
|
||||
validateHandler(handler)
|
||||
|
||||
m.handlers = append(m.handlers, handler)
|
||||
}
|
||||
|
||||
// ServeHTTP is the HTTP Entry point for a Martini instance. Useful if you want to control your own HTTP server.
|
||||
func (m *Martini) ServeHTTP(res http.ResponseWriter, req *http.Request) {
|
||||
m.createContext(res, req).run()
|
||||
}
|
||||
|
||||
// Run the http server on a given host and port.
|
||||
func (m *Martini) RunOnAddr(addr string) {
|
||||
// TODO: Should probably be implemented using a new instance of http.Server in place of
|
||||
// calling http.ListenAndServer directly, so that it could be stored in the martini struct for later use.
|
||||
// This would also allow to improve testing when a custom host and port are passed.
|
||||
|
||||
logger := m.Injector.Get(reflect.TypeOf(m.logger)).Interface().(*log.Logger)
|
||||
logger.Printf("listening on %s (%s)\n", addr, Env)
|
||||
logger.Fatalln(http.ListenAndServe(addr, m))
|
||||
}
|
||||
|
||||
// Run the http server. Listening on os.GetEnv("PORT") or 3000 by default.
|
||||
func (m *Martini) Run() {
|
||||
port := os.Getenv("PORT")
|
||||
if len(port) == 0 {
|
||||
port = "3000"
|
||||
}
|
||||
|
||||
host := os.Getenv("HOST")
|
||||
|
||||
m.RunOnAddr(host + ":" + port)
|
||||
}
|
||||
|
||||
func (m *Martini) createContext(res http.ResponseWriter, req *http.Request) *context {
|
||||
c := &context{inject.New(), m.handlers, m.action, NewResponseWriter(res), 0}
|
||||
c.SetParent(m)
|
||||
c.MapTo(c, (*Context)(nil))
|
||||
c.MapTo(c.rw, (*http.ResponseWriter)(nil))
|
||||
c.Map(req)
|
||||
return c
|
||||
}
|
||||
|
||||
// ClassicMartini represents a Martini with some reasonable defaults. Embeds the router functions for convenience.
|
||||
type ClassicMartini struct {
|
||||
*Martini
|
||||
Router
|
||||
}
|
||||
|
||||
// Classic creates a classic Martini with some basic default middleware - martini.Logger, martini.Recovery and martini.Static.
|
||||
// Classic also maps martini.Routes as a service.
|
||||
func Classic() *ClassicMartini {
|
||||
r := NewRouter()
|
||||
m := New()
|
||||
m.Use(Logger())
|
||||
m.Use(Recovery())
|
||||
m.Use(Static("public"))
|
||||
m.MapTo(r, (*Routes)(nil))
|
||||
m.Action(r.Handle)
|
||||
return &ClassicMartini{m, r}
|
||||
}
|
||||
|
||||
// Handler can be any callable function. Martini attempts to inject services into the handler's argument list.
|
||||
// Martini will panic if an argument could not be fullfilled via dependency injection.
|
||||
type Handler interface{}
|
||||
|
||||
func validateHandler(handler Handler) {
|
||||
if reflect.TypeOf(handler).Kind() != reflect.Func {
|
||||
panic("martini handler must be a callable func")
|
||||
}
|
||||
}
|
||||
|
||||
// Context represents a request context. Services can be mapped on the request level from this interface.
|
||||
type Context interface {
|
||||
inject.Injector
|
||||
// Next is an optional function that Middleware Handlers can call to yield the until after
|
||||
// the other Handlers have been executed. This works really well for any operations that must
|
||||
// happen after an http request
|
||||
Next()
|
||||
// Written returns whether or not the response for this context has been written.
|
||||
Written() bool
|
||||
}
|
||||
|
||||
type context struct {
|
||||
inject.Injector
|
||||
handlers []Handler
|
||||
action Handler
|
||||
rw ResponseWriter
|
||||
index int
|
||||
}
|
||||
|
||||
func (c *context) handler() Handler {
|
||||
if c.index < len(c.handlers) {
|
||||
return c.handlers[c.index]
|
||||
}
|
||||
if c.index == len(c.handlers) {
|
||||
return c.action
|
||||
}
|
||||
panic("invalid index for context handler")
|
||||
}
|
||||
|
||||
func (c *context) Next() {
|
||||
c.index += 1
|
||||
c.run()
|
||||
}
|
||||
|
||||
func (c *context) Written() bool {
|
||||
return c.rw.Written()
|
||||
}
|
||||
|
||||
func (c *context) run() {
|
||||
for c.index <= len(c.handlers) {
|
||||
_, err := c.Invoke(c.handler())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.index += 1
|
||||
|
||||
if c.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/* Test Helpers */
|
||||
func expect(t *testing.T, a interface{}, b interface{}) {
|
||||
if a != b {
|
||||
t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
|
||||
func refute(t *testing.T, a interface{}, b interface{}) {
|
||||
if a == b {
|
||||
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
m := New()
|
||||
if m == nil {
|
||||
t.Error("martini.New() cannot return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Martini_RunOnAddr(t *testing.T) {
|
||||
// just test that Run doesn't bomb
|
||||
go New().RunOnAddr("127.0.0.1:8080")
|
||||
}
|
||||
|
||||
func Test_Martini_Run(t *testing.T) {
|
||||
go New().Run()
|
||||
}
|
||||
|
||||
func Test_Martini_ServeHTTP(t *testing.T) {
|
||||
result := ""
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
m := New()
|
||||
m.Use(func(c Context) {
|
||||
result += "foo"
|
||||
c.Next()
|
||||
result += "ban"
|
||||
})
|
||||
m.Use(func(c Context) {
|
||||
result += "bar"
|
||||
c.Next()
|
||||
result += "baz"
|
||||
})
|
||||
m.Action(func(res http.ResponseWriter, req *http.Request) {
|
||||
result += "bat"
|
||||
res.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
|
||||
m.ServeHTTP(response, (*http.Request)(nil))
|
||||
|
||||
expect(t, result, "foobarbatbazban")
|
||||
expect(t, response.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func Test_Martini_Handlers(t *testing.T) {
|
||||
result := ""
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
batman := func(c Context) {
|
||||
result += "batman!"
|
||||
}
|
||||
|
||||
m := New()
|
||||
m.Use(func(c Context) {
|
||||
result += "foo"
|
||||
c.Next()
|
||||
result += "ban"
|
||||
})
|
||||
m.Handlers(
|
||||
batman,
|
||||
batman,
|
||||
batman,
|
||||
)
|
||||
m.Action(func(res http.ResponseWriter, req *http.Request) {
|
||||
result += "bat"
|
||||
res.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
|
||||
m.ServeHTTP(response, (*http.Request)(nil))
|
||||
|
||||
expect(t, result, "batman!batman!batman!bat")
|
||||
expect(t, response.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func Test_Martini_EarlyWrite(t *testing.T) {
|
||||
result := ""
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
m := New()
|
||||
m.Use(func(res http.ResponseWriter) {
|
||||
result += "foobar"
|
||||
res.Write([]byte("Hello world"))
|
||||
})
|
||||
m.Use(func() {
|
||||
result += "bat"
|
||||
})
|
||||
m.Action(func(res http.ResponseWriter) {
|
||||
result += "baz"
|
||||
res.WriteHeader(http.StatusBadRequest)
|
||||
})
|
||||
|
||||
m.ServeHTTP(response, (*http.Request)(nil))
|
||||
|
||||
expect(t, result, "foobar")
|
||||
expect(t, response.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func Test_Martini_Written(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
m := New()
|
||||
m.Handlers(func(res http.ResponseWriter) {
|
||||
res.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
ctx := m.createContext(response, (*http.Request)(nil))
|
||||
expect(t, ctx.Written(), false)
|
||||
|
||||
ctx.run()
|
||||
expect(t, ctx.Written(), true)
|
||||
}
|
||||
|
||||
func Test_Martini_Basic_NoRace(t *testing.T) {
|
||||
m := New()
|
||||
handlers := []Handler{func() {}, func() {}}
|
||||
// Ensure append will not realloc to trigger the race condition
|
||||
m.handlers = handlers[:1]
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
for i := 0; i < 2; i++ {
|
||||
go func() {
|
||||
response := httptest.NewRecorder()
|
||||
m.ServeHTTP(response, req)
|
||||
}()
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/codegangsta/inject"
|
||||
)
|
||||
|
||||
const (
|
||||
panicHtml = `<html>
|
||||
<head><title>PANIC: %s</title>
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
font-family: "Roboto", sans-serif;
|
||||
color: #333333;
|
||||
background-color: #ea5343;
|
||||
margin: 0px;
|
||||
}
|
||||
h1 {
|
||||
color: #d04526;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-bottom: 1px dashed #2b3848;
|
||||
}
|
||||
pre {
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid #2b3848;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
</head><body>
|
||||
<h1>PANIC</h1>
|
||||
<pre style="font-weight: bold;">%s</pre>
|
||||
<pre>%s</pre>
|
||||
</body>
|
||||
</html>`
|
||||
)
|
||||
|
||||
var (
|
||||
dunno = []byte("???")
|
||||
centerDot = []byte("·")
|
||||
dot = []byte(".")
|
||||
slash = []byte("/")
|
||||
)
|
||||
|
||||
// stack returns a nicely formated stack frame, skipping skip frames
|
||||
func stack(skip int) []byte {
|
||||
buf := new(bytes.Buffer) // the returned data
|
||||
// As we loop, we open files and read them. These variables record the currently
|
||||
// loaded file.
|
||||
var lines [][]byte
|
||||
var lastFile string
|
||||
for i := skip; ; i++ { // Skip the expected number of frames
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
// Print this much at least. If we can't find the source, it won't show.
|
||||
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
|
||||
if file != lastFile {
|
||||
data, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
lastFile = file
|
||||
}
|
||||
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// source returns a space-trimmed slice of the n'th line.
|
||||
func source(lines [][]byte, n int) []byte {
|
||||
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
|
||||
if n < 0 || n >= len(lines) {
|
||||
return dunno
|
||||
}
|
||||
return bytes.TrimSpace(lines[n])
|
||||
}
|
||||
|
||||
// function returns, if possible, the name of the function containing the PC.
|
||||
func function(pc uintptr) []byte {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
return dunno
|
||||
}
|
||||
name := []byte(fn.Name())
|
||||
// The name includes the path name to the package, which is unnecessary
|
||||
// since the file name is already included. Plus, it has center dots.
|
||||
// That is, we see
|
||||
// runtime/debug.*T·ptrmethod
|
||||
// and want
|
||||
// *T.ptrmethod
|
||||
// Also the package path might contains dot (e.g. code.google.com/...),
|
||||
// so first eliminate the path prefix
|
||||
if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
|
||||
name = name[lastslash+1:]
|
||||
}
|
||||
if period := bytes.Index(name, dot); period >= 0 {
|
||||
name = name[period+1:]
|
||||
}
|
||||
name = bytes.Replace(name, centerDot, dot, -1)
|
||||
return name
|
||||
}
|
||||
|
||||
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
|
||||
// While Martini is in development mode, Recovery will also output the panic as HTML.
|
||||
func Recovery() Handler {
|
||||
return func(c Context, log *log.Logger) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
stack := stack(3)
|
||||
log.Printf("PANIC: %s\n%s", err, stack)
|
||||
|
||||
// Lookup the current responsewriter
|
||||
val := c.Get(inject.InterfaceOf((*http.ResponseWriter)(nil)))
|
||||
res := val.Interface().(http.ResponseWriter)
|
||||
|
||||
// respond with panic message while in development mode
|
||||
var body []byte
|
||||
if Env == Dev {
|
||||
res.Header().Set("Content-Type", "text/html")
|
||||
body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
|
||||
} else {
|
||||
body = []byte("500 Internal Server Error")
|
||||
}
|
||||
|
||||
res.WriteHeader(http.StatusInternalServerError)
|
||||
if nil != body {
|
||||
res.Write(body)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_Recovery(t *testing.T) {
|
||||
buff := bytes.NewBufferString("")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
setENV(Dev)
|
||||
m := New()
|
||||
// replace log for testing
|
||||
m.Map(log.New(buff, "[martini] ", 0))
|
||||
m.Use(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Set("Content-Type", "unpredictable")
|
||||
})
|
||||
m.Use(Recovery())
|
||||
m.Use(func(res http.ResponseWriter, req *http.Request) {
|
||||
panic("here is a panic!")
|
||||
})
|
||||
m.ServeHTTP(recorder, (*http.Request)(nil))
|
||||
expect(t, recorder.Code, http.StatusInternalServerError)
|
||||
expect(t, recorder.HeaderMap.Get("Content-Type"), "text/html")
|
||||
refute(t, recorder.Body.Len(), 0)
|
||||
refute(t, len(buff.String()), 0)
|
||||
}
|
||||
|
||||
func Test_Recovery_ResponseWriter(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
recorder2 := httptest.NewRecorder()
|
||||
|
||||
setENV(Dev)
|
||||
m := New()
|
||||
m.Use(Recovery())
|
||||
m.Use(func(c Context) {
|
||||
c.MapTo(recorder2, (*http.ResponseWriter)(nil))
|
||||
panic("here is a panic!")
|
||||
})
|
||||
m.ServeHTTP(recorder, (*http.Request)(nil))
|
||||
|
||||
expect(t, recorder2.Code, http.StatusInternalServerError)
|
||||
expect(t, recorder2.HeaderMap.Get("Content-Type"), "text/html")
|
||||
refute(t, recorder2.Body.Len(), 0)
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about
|
||||
// the response. It is recommended that middleware handlers use this construct to wrap a responsewriter
|
||||
// if the functionality calls for it.
|
||||
type ResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
http.Flusher
|
||||
http.Hijacker
|
||||
// Status returns the status code of the response or 0 if the response has not been written.
|
||||
Status() int
|
||||
// Written returns whether or not the ResponseWriter has been written.
|
||||
Written() bool
|
||||
// Size returns the size of the response body.
|
||||
Size() int
|
||||
// Before allows for a function to be called before the ResponseWriter has been written to. This is
|
||||
// useful for setting headers or any other operations that must happen before a response has been written.
|
||||
Before(BeforeFunc)
|
||||
}
|
||||
|
||||
// BeforeFunc is a function that is called before the ResponseWriter has been written to.
|
||||
type BeforeFunc func(ResponseWriter)
|
||||
|
||||
// NewResponseWriter creates a ResponseWriter that wraps an http.ResponseWriter
|
||||
func NewResponseWriter(rw http.ResponseWriter) ResponseWriter {
|
||||
return &responseWriter{rw, 0, 0, nil}
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
size int
|
||||
beforeFuncs []BeforeFunc
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(s int) {
|
||||
rw.callBefore()
|
||||
rw.ResponseWriter.WriteHeader(s)
|
||||
rw.status = s
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||
if !rw.Written() {
|
||||
// The status will be StatusOK if WriteHeader has not been called yet
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
size, err := rw.ResponseWriter.Write(b)
|
||||
rw.size += size
|
||||
return size, err
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Status() int {
|
||||
return rw.status
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Size() int {
|
||||
return rw.size
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Written() bool {
|
||||
return rw.status != 0
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Before(before BeforeFunc) {
|
||||
rw.beforeFuncs = append(rw.beforeFuncs, before)
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hijacker, ok := rw.ResponseWriter.(http.Hijacker)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface")
|
||||
}
|
||||
return hijacker.Hijack()
|
||||
}
|
||||
|
||||
func (rw *responseWriter) CloseNotify() <-chan bool {
|
||||
return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||
}
|
||||
|
||||
func (rw *responseWriter) callBefore() {
|
||||
for i := len(rw.beforeFuncs) - 1; i >= 0; i-- {
|
||||
rw.beforeFuncs[i](rw)
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Flush() {
|
||||
flusher, ok := rw.ResponseWriter.(http.Flusher)
|
||||
if ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
package martini
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type closeNotifyingRecorder struct {
|
||||
*httptest.ResponseRecorder
|
||||
closed chan bool
|
||||
}
|
||||
|
||||
func newCloseNotifyingRecorder() *closeNotifyingRecorder {
|
||||
return &closeNotifyingRecorder{
|
||||
httptest.NewRecorder(),
|
||||
make(chan bool, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *closeNotifyingRecorder) close() {
|
||||
c.closed <- true
|
||||
}
|
||||
|
||||
func (c *closeNotifyingRecorder) CloseNotify() <-chan bool {
|
||||
return c.closed
|
||||
}
|
||||
|
||||
type hijackableResponse struct {
|
||||
Hijacked bool
|
||||
}
|
||||
|
||||
func newHijackableResponse() *hijackableResponse {
|
||||
return &hijackableResponse{}
|
||||
}
|
||||
|
||||
func (h *hijackableResponse) Header() http.Header { return nil }
|
||||
func (h *hijackableResponse) Write(buf []byte) (int, error) { return 0, nil }
|
||||
func (h *hijackableResponse) WriteHeader(code int) {}
|
||||
func (h *hijackableResponse) Flush() {}
|
||||
func (h *hijackableResponse) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
h.Hijacked = true
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func Test_ResponseWriter_WritingString(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
rw := NewResponseWriter(rec)
|
||||
|
||||
rw.Write([]byte("Hello world"))
|
||||
|
||||
expect(t, rec.Code, rw.Status())
|
||||
expect(t, rec.Body.String(), "Hello world")
|
||||
expect(t, rw.Status(), http.StatusOK)
|
||||
expect(t, rw.Size(), 11)
|
||||
expect(t, rw.Written(), true)
|
||||
}
|
||||
|
||||
func Test_ResponseWriter_WritingStrings(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
rw := NewResponseWriter(rec)
|
||||
|
||||
rw.Write([]byte("Hello world"))
|
||||
rw.Write([]byte("foo bar bat baz"))
|
||||
|
||||
expect(t, rec.Code, rw.Status())
|
||||
expect(t, rec.Body.String(), "Hello worldfoo bar bat baz")
|
||||
expect(t, rw.Status(), http.StatusOK)
|
||||
expect(t, rw.Size(), 26)
|
||||
}
|
||||
|
||||
func Test_ResponseWriter_WritingHeader(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
rw := NewResponseWriter(rec)
|
||||
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
|
||||
expect(t, rec.Code, rw.Status())
|
||||
expect(t, rec.Body.String(), "")
|
||||
expect(t, rw.Status(), http.StatusNotFound)
|
||||
expect(t, rw.Size(), 0)
|
||||
}
|
||||
|
||||
func Test_ResponseWriter_Before(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
rw := NewResponseWriter(rec)
|
||||
result := ""
|
||||
|
||||
rw.Before(func(ResponseWriter) {
|
||||
result += "foo"
|
||||
})
|
||||
rw.Before(func(ResponseWriter) {
|
||||
result += "bar"
|
||||
})
|
||||
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
|
||||
expect(t, rec.Code, rw.Status())
|
||||
expect(t, rec.Body.String(), "")
|
||||
expect(t, rw.Status(), http.StatusNotFound)
|
||||
expect(t, rw.Size(), 0)
|
||||
expect(t, result, "barfoo")
|
||||
}
|
||||
|
||||
func Test_ResponseWriter_Hijack(t *testing.T) {
|
||||
hijackable := newHijackableResponse()
|
||||
rw := NewResponseWriter(hijackable)
|
||||
hijacker, ok := rw.(http.Hijacker)
|
||||
expect(t, ok, true)
|
||||
_, _, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
expect(t, hijackable.Hijacked, true)
|
||||
}
|
||||
|
||||
func Test_ResponseWrite_Hijack_NotOK(t *testing.T) {
|
||||
hijackable := new(http.ResponseWriter)
|
||||
rw := NewResponseWriter(*hijackable)
|
||||
hijacker, ok := rw.(http.Hijacker)
|
||||
expect(t, ok, true)
|
||||
_, _, err := hijacker.Hijack()
|
||||
|
||||
refute(t, err, nil)
|
||||
}
|
||||
|
||||
func Test_ResponseWriter_CloseNotify(t *testing.T) {
|
||||
rec := newCloseNotifyingRecorder()
|
||||
rw := NewResponseWriter(rec)
|
||||
closed := false
|
||||
notifier := rw.(http.CloseNotifier).CloseNotify()
|
||||
rec.close()
|
||||
select {
|
||||
case <-notifier:
|
||||
closed = true
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
expect(t, closed, true)
|
||||
}
|
||||
|
||||
func Test_ResponseWriter_Flusher(t *testing.T) {
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
rw := NewResponseWriter(rec)
|
||||
|
||||
_, ok := rw.(http.Flusher)
|
||||
expect(t, ok, true)
|
||||
}
|
||||
|
||||
func Test_ResponseWriter_FlusherHandler(t *testing.T) {
|
||||
|
||||
// New martini instance
|
||||
m := Classic()
|
||||
|
||||
m.Get("/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
f, ok := w.(http.Flusher)
|
||||
expect(t, ok, true)
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
io.WriteString(w, "data: Hello\n\n")
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/events", nil)
|
||||
m.ServeHTTP(recorder, r)
|
||||
|
||||
if recorder.Code != 200 {
|
||||
t.Error("Response not 200")
|
||||
}
|
||||
|
||||
if recorder.Body.String() != "data: Hello\n\ndata: Hello\n\n" {
|
||||
t.Error("Didn't receive correct body, got:", recorder.Body.String())
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue