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:
Dr Nic Williams 2018-04-15 08:20:42 +10:00
parent 7c64f011bd
commit a7a8aeffb2
435 changed files with 30 additions and 83519 deletions

View File

@ -1 +1,2 @@
web: stannis webserver
web: stannis webserver --config config/webserver.config.example.yml
agent: stannis agent --config config/agent.config.example.yml

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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{
{

2
vendor/.gitignore vendored
View File

@ -1,2 +0,0 @@
/pkg
/bin

View File

@ -1 +0,0 @@
Godeps/_workspace

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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.

View File

@ -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.

View File

@ -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
}

View File

@ -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),
},
})
}

View File

@ -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
}

View File

@ -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))
})
})

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
})
})

View File

@ -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
}

View File

@ -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))
})
})

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
})
})

View File

@ -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
}

View File

@ -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
})
})

View File

@ -1,2 +0,0 @@
// Package api implements the BOSH API client functions
package api

View File

@ -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")
}

View File

@ -1,5 +0,0 @@
package constants
const (
Version = "0.1.0"
)

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -1,10 +0,0 @@
package gogobosh_test
import (
. "github.com/onsi/ginkgo"
)
var _ = Describe("GoGoBOSH", func() {
It("parse response", func() {
})
})

View File

@ -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]
}

View File

@ -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"))
})
})

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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))
})
})

View File

@ -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))
})
})

View File

@ -1,2 +0,0 @@
// Package models contains the public repsentations of BOSH resources
package models

View File

@ -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
}

View File

@ -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")
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -1,4 +0,0 @@
/*
Package net includes helper libraries for performing HTTP requests & processing the responses
*/
package net

View File

@ -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
}

View File

@ -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))
})
})

View File

@ -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)))
}
}

View File

@ -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")
}

View File

@ -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"

View File

@ -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)
}

View File

@ -1,4 +0,0 @@
/*
Package utils is a collection of utilities used within gogobosh
*/
package utils

View File

@ -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)
}

View File

@ -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)
}

View File

@ -1,2 +0,0 @@
language: go
go: 1.1

View File

@ -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.

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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"})
}

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -1,2 +0,0 @@
inject
inject.test

View File

@ -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.

View File

@ -1,4 +0,0 @@
inject
======
Dependency injection for go

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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())
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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"
)

View File

@ -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
)

View File

@ -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

View File

@ -1 +0,0 @@
github.com/codegangsta/inject master

View File

@ -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.

View File

@ -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/)

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -1,7 +0,0 @@
// +build !go1.1
package martini
func MartiniDoesNotSupportGo1Point0() {
"Martini requires Go 1.1 or greater."
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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)
}()
}
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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