├── Godeps ├── Readme └── Godeps.json ├── types.go ├── .travis.yml ├── .gitignore ├── error.go ├── services_test.go ├── stacks_test.go ├── LICENSE ├── routes_test.go ├── README.md ├── services.go ├── stacks.go ├── routes.go ├── client_test.go ├── space_quotas_test.go ├── org_quotas_test.go ├── service_instances_test.go ├── appevents_test.go ├── domains_test.go ├── org_quotas.go ├── space_quotas.go ├── cf_test.go ├── service_instances.go ├── secgroups_test.go ├── users.go ├── users_test.go ├── domains.go ├── appevents.go ├── tasks.go ├── tasks_test.go ├── client.go ├── spaces_test.go ├── apps_test.go ├── apps.go ├── secgroups.go ├── spaces.go ├── orgs_test.go └── orgs.go /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | type Meta struct { 4 | Guid string `json:"guid"` 5 | Url string `json:"url"` 6 | CreatedAt string `json:"created_at"` 7 | UpdatedAt string `json:"updated_at"` 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.6 5 | - 1.7 6 | - 1.8 7 | 8 | before_install: 9 | - go get -u golang.org/x/tools/cmd/goimports 10 | 11 | script: 12 | - FILES=`find . -iname '*.go' -type f -not -path "./vendor/*"` 13 | # linting 14 | - gofmt -d $FILES 15 | - go tool vet $FILES 16 | - goimports -d $FILES 17 | # testing 18 | - go test -v -race 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | _workspace 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import "fmt" 4 | 5 | type CloudFoundryErrors struct { 6 | Errors []CloudFoundryError `json:"errors"` 7 | } 8 | 9 | func (cfErrs CloudFoundryErrors) Error() string { 10 | err := "" 11 | 12 | for _, cfErr := range cfErrs.Errors { 13 | err += fmt.Sprintf("%s\n", cfErr) 14 | } 15 | 16 | return err 17 | } 18 | 19 | type CloudFoundryError struct { 20 | Code int `json:"code"` 21 | Title string `json:"title"` 22 | Detail string `json:"detail"` 23 | } 24 | 25 | func (cfErr CloudFoundryError) Error() string { 26 | return fmt.Sprintf("cfclient: error (%d): %s", cfErr.Code, cfErr.Title) 27 | } 28 | -------------------------------------------------------------------------------- /services_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListServices(t *testing.T) { 10 | Convey("List Services", t, func() { 11 | setup(MockRoute{"GET", "/v2/services", listServicePayload, "", 200, ""}, t) 12 | defer teardown() 13 | c := &Config{ 14 | ApiAddress: server.URL, 15 | Token: "foobar", 16 | } 17 | client, err := NewClient(c) 18 | So(err, ShouldBeNil) 19 | 20 | services, err := client.ListServices() 21 | So(err, ShouldBeNil) 22 | 23 | So(len(services), ShouldEqual, 2) 24 | So(services[0].Guid, ShouldEqual, "a3d76c01-c08a-4505-b06d-8603265682a3") 25 | So(services[0].Label, ShouldEqual, "nats") 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /stacks_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListStacks(t *testing.T) { 10 | Convey("List Stacks", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/stacks", listStacksPayloadPage1, "", 200, ""}, 13 | {"GET", "/v2/stacks_page_2", listStacksPayloadPage2, "", 200, ""}, 14 | } 15 | setupMultiple(mocks, t) 16 | defer teardown() 17 | c := &Config{ 18 | ApiAddress: server.URL, 19 | Token: "foobar", 20 | } 21 | client, err := NewClient(c) 22 | So(err, ShouldBeNil) 23 | 24 | stacks, err := client.ListStacks() 25 | So(err, ShouldBeNil) 26 | 27 | So(len(stacks), ShouldEqual, 2) 28 | So(stacks[0].Guid, ShouldEqual, "67e019a3-322a-407a-96e0-178e95bd0e55") 29 | So(stacks[0].Name, ShouldEqual, "cflinuxfs2") 30 | So(stacks[0].Description, ShouldEqual, "Cloud Foundry Linux-based filesystem") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Long Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /routes_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListRoutes(t *testing.T) { 10 | Convey("List Routes", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/routes", listRoutesPayloadPage1, "", 200, ""}, 13 | {"GET", "/v2/routes_page_2", listRoutesPayloadPage2, "", 200, ""}, 14 | } 15 | setupMultiple(mocks, t) 16 | defer teardown() 17 | c := &Config{ 18 | ApiAddress: server.URL, 19 | Token: "foobar", 20 | } 21 | client, err := NewClient(c) 22 | So(err, ShouldBeNil) 23 | 24 | routes, err := client.ListRoutes() 25 | So(err, ShouldBeNil) 26 | 27 | So(len(routes), ShouldEqual, 2) 28 | So(routes[0].Guid, ShouldEqual, "24707add-83b8-4fd8-a8f4-b7297199c805") 29 | So(routes[0].Host, ShouldEqual, "test-1") 30 | So(routes[0].Path, ShouldEqual, "/foo") 31 | So(routes[0].DomainGuid, ShouldEqual, "0b183484-45cc-4855-94d4-892f80f20c13") 32 | So(routes[0].SpaceGuid, ShouldEqual, "494d8b64-8181-4183-a6d3-6279db8fec6e") 33 | So(routes[0].ServiceInstanceGuid, ShouldEqual, "") 34 | So(routes[0].Port, ShouldEqual, 0) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-cfclient 2 | 3 | ### Overview 4 | 5 | [![Build Status](https://img.shields.io/travis/cloudfoundry-community/go-cfclient.svg)](https://travis-ci.org/cloudfoundry-community/go-cfclient) [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/cloudfoundry-community/go-cfclient) 6 | 7 | `cfclient` is a package to assist you in writing apps that need information out of [Cloud Foundry](http://cloudfoundry.org). It provides functions and structures to retrieve 8 | 9 | 10 | ### Usage 11 | 12 | ``` 13 | go get github.com/cloudfoundry-community/go-cfclient 14 | ``` 15 | 16 | Some example code: 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "github.com/cloudfoundry-community/go-cfclient" 23 | ) 24 | 25 | func main() { 26 | c := &cfclient.Config{ 27 | ApiAddress: "https://api.10.244.0.34.xip.io", 28 | Username: "admin", 29 | Password: "admin", 30 | } 31 | client, _ := cfclient.NewClient(c) 32 | apps, _ := client.ListApps() 33 | fmt.Println(apps) 34 | } 35 | ``` 36 | 37 | ### Developing & Contributing 38 | 39 | You can use Godep to restore the dependency 40 | Tested with go1.5.3 41 | ```bash 42 | godep go build 43 | ``` 44 | 45 | Pull requests welcome. 46 | -------------------------------------------------------------------------------- /services.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/url" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type ServicesResponse struct { 12 | Count int `json:"total_results"` 13 | Pages int `json:"total_pages"` 14 | Resources []ServicesResource `json:"resources"` 15 | } 16 | 17 | type ServicesResource struct { 18 | Meta Meta `json:"metadata"` 19 | Entity Service `json:"entity"` 20 | } 21 | 22 | type Service struct { 23 | Guid string `json:"guid"` 24 | Label string `json:"label"` 25 | c *Client 26 | } 27 | 28 | type ServiceSummary struct { 29 | Guid string `json:"guid"` 30 | Name string `json:"name"` 31 | BoundAppCount int `json:"bound_app_count"` 32 | } 33 | 34 | func (c *Client) ListServicesByQuery(query url.Values) ([]Service, error) { 35 | var services []Service 36 | var serviceResp ServicesResponse 37 | r := c.NewRequest("GET", "/v2/services?"+query.Encode()) 38 | resp, err := c.DoRequest(r) 39 | if err != nil { 40 | return nil, errors.Wrap(err, "Error requesting services") 41 | } 42 | resBody, err := ioutil.ReadAll(resp.Body) 43 | if err != nil { 44 | return nil, errors.Wrap(err, "Error reading services request:") 45 | } 46 | 47 | err = json.Unmarshal(resBody, &serviceResp) 48 | if err != nil { 49 | return nil, errors.Wrap(err, "Error unmarshaling services") 50 | } 51 | for _, service := range serviceResp.Resources { 52 | service.Entity.Guid = service.Meta.Guid 53 | service.Entity.c = c 54 | services = append(services, service.Entity) 55 | } 56 | return services, nil 57 | } 58 | 59 | func (c *Client) ListServices() ([]Service, error) { 60 | return c.ListServicesByQuery(nil) 61 | } 62 | -------------------------------------------------------------------------------- /stacks.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/url" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type StacksResponse struct { 12 | Count int `json:"total_results"` 13 | Pages int `json:"total_pages"` 14 | NextUrl string `json:"next_url"` 15 | Resources []StacksResource `json:"resources"` 16 | } 17 | 18 | type StacksResource struct { 19 | Meta Meta `json:"metadata"` 20 | Entity Stack `json:"entity"` 21 | } 22 | 23 | type Stack struct { 24 | Guid string `json:"guid"` 25 | Name string `json:"name"` 26 | Description string `json:"description"` 27 | c *Client 28 | } 29 | 30 | func (c *Client) ListStacksByQuery(query url.Values) ([]Stack, error) { 31 | var stacks []Stack 32 | requestUrl := "/v2/stacks?" + query.Encode() 33 | for { 34 | stacksResp, err := c.getStacksResponse(requestUrl) 35 | if err != nil { 36 | return []Stack{}, err 37 | } 38 | for _, stack := range stacksResp.Resources { 39 | stack.Entity.Guid = stack.Meta.Guid 40 | stack.Entity.c = c 41 | stacks = append(stacks, stack.Entity) 42 | } 43 | requestUrl = stacksResp.NextUrl 44 | if requestUrl == "" { 45 | break 46 | } 47 | } 48 | return stacks, nil 49 | } 50 | 51 | func (c *Client) ListStacks() ([]Stack, error) { 52 | return c.ListStacksByQuery(nil) 53 | } 54 | 55 | func (c *Client) getStacksResponse(requestUrl string) (StacksResponse, error) { 56 | var stacksResp StacksResponse 57 | r := c.NewRequest("GET", requestUrl) 58 | resp, err := c.DoRequest(r) 59 | if err != nil { 60 | return StacksResponse{}, errors.Wrap(err, "Error requesting stacks") 61 | } 62 | resBody, err := ioutil.ReadAll(resp.Body) 63 | defer resp.Body.Close() 64 | if err != nil { 65 | return StacksResponse{}, errors.Wrap(err, "Error reading stacks body") 66 | } 67 | err = json.Unmarshal(resBody, &stacksResp) 68 | if err != nil { 69 | return StacksResponse{}, errors.Wrap(err, "Error unmarshalling stacks") 70 | } 71 | return stacksResp, nil 72 | } 73 | -------------------------------------------------------------------------------- /routes.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/url" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type RoutesResponse struct { 12 | Count int `json:"total_results"` 13 | Pages int `json:"total_pages"` 14 | NextUrl string `json:"next_url"` 15 | Resources []RoutesResource `json:"resources"` 16 | } 17 | 18 | type RoutesResource struct { 19 | Meta Meta `json:"metadata"` 20 | Entity Route `json:"entity"` 21 | } 22 | 23 | type Route struct { 24 | Guid string `json:"guid"` 25 | Host string `json:"host"` 26 | Path string `json:"path"` 27 | DomainGuid string `json:"domain_guid"` 28 | SpaceGuid string `json:"space_guid"` 29 | ServiceInstanceGuid string `json:"service_instance_guid"` 30 | Port int `json:"port"` 31 | c *Client 32 | } 33 | 34 | func (c *Client) ListRoutesByQuery(query url.Values) ([]Route, error) { 35 | return c.fetchRoutes("/v2/routes?" + query.Encode()) 36 | } 37 | 38 | func (c *Client) fetchRoutes(requestUrl string) ([]Route, error) { 39 | var routes []Route 40 | for { 41 | routesResp, err := c.getRoutesResponse(requestUrl) 42 | if err != nil { 43 | return []Route{}, err 44 | } 45 | for _, route := range routesResp.Resources { 46 | route.Entity.Guid = route.Meta.Guid 47 | route.Entity.c = c 48 | routes = append(routes, route.Entity) 49 | } 50 | requestUrl = routesResp.NextUrl 51 | if requestUrl == "" { 52 | break 53 | } 54 | } 55 | return routes, nil 56 | } 57 | 58 | func (c *Client) ListRoutes() ([]Route, error) { 59 | return c.ListRoutesByQuery(nil) 60 | } 61 | 62 | func (c *Client) getRoutesResponse(requestUrl string) (RoutesResponse, error) { 63 | var routesResp RoutesResponse 64 | r := c.NewRequest("GET", requestUrl) 65 | resp, err := c.DoRequest(r) 66 | if err != nil { 67 | return RoutesResponse{}, errors.Wrap(err, "Error requesting routes") 68 | } 69 | resBody, err := ioutil.ReadAll(resp.Body) 70 | defer resp.Body.Close() 71 | if err != nil { 72 | return RoutesResponse{}, errors.Wrap(err, "Error reading routes body") 73 | } 74 | err = json.Unmarshal(resBody, &routesResp) 75 | if err != nil { 76 | return RoutesResponse{}, errors.Wrap(err, "Error unmarshalling routes") 77 | } 78 | return routesResp, nil 79 | } 80 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/gomega" 7 | . "github.com/smartystreets/goconvey/convey" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | func TestDefaultConfig(t *testing.T) { 13 | Convey("Default config", t, func() { 14 | c := DefaultConfig() 15 | So(c.ApiAddress, ShouldEqual, "http://api.bosh-lite.com") 16 | So(c.Username, ShouldEqual, "admin") 17 | So(c.Password, ShouldEqual, "admin") 18 | So(c.SkipSslValidation, ShouldEqual, false) 19 | So(c.Token, ShouldEqual, "") 20 | So(c.UserAgent, ShouldEqual, "Go-CF-client/1.1") 21 | }) 22 | } 23 | 24 | func TestMakeRequest(t *testing.T) { 25 | Convey("Test making request b", t, func() { 26 | setup(MockRoute{"GET", "/v2/organizations", listOrgsPayload, "", 200, ""}, t) 27 | defer teardown() 28 | c := &Config{ 29 | ApiAddress: server.URL, 30 | Username: "foo", 31 | Password: "bar", 32 | SkipSslValidation: true, 33 | } 34 | client, err := NewClient(c) 35 | So(err, ShouldBeNil) 36 | req := client.NewRequest("GET", "/v2/organizations") 37 | resp, err := client.DoRequest(req) 38 | So(err, ShouldBeNil) 39 | So(resp, ShouldNotBeNil) 40 | }) 41 | } 42 | 43 | func TestMakeRequestWithTimeout(t *testing.T) { 44 | Convey("Test making request b", t, func() { 45 | setup(MockRoute{"GET", "/v2/organizations", listOrgsPayload, "", 200, ""}, t) 46 | defer teardown() 47 | c := &Config{ 48 | ApiAddress: server.URL, 49 | Username: "foo", 50 | Password: "bar", 51 | SkipSslValidation: true, 52 | HttpClient: &http.Client{Timeout: 10 * time.Nanosecond}, 53 | } 54 | client, err := NewClient(c) 55 | So(err, ShouldNotBeNil) 56 | So(client, ShouldBeNil) 57 | }) 58 | } 59 | 60 | func TestTokenRefresh(t *testing.T) { 61 | gomega.RegisterTestingT(t) 62 | Convey("Test making request", t, func() { 63 | setup(MockRoute{"GET", "/v2/organizations", listOrgsPayload, "", 200, ""}, t) 64 | c := &Config{ 65 | ApiAddress: server.URL, 66 | Username: "foo", 67 | Password: "bar", 68 | } 69 | client, err := NewClient(c) 70 | So(err, ShouldBeNil) 71 | 72 | token, err := client.GetToken() 73 | So(err, ShouldBeNil) 74 | 75 | gomega.Consistently(token).Should(gomega.Equal("bearer foobar2")) 76 | // gomega.Eventually(client.GetToken(), "3s").Should(gomega.Equal("bearer foobar3")) 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /space_quotas_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListSpaceQuotas(t *testing.T) { 10 | Convey("List Space Quotas", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/space_quota_definitions", listSpaceQuotasPayloadPage1, "", 200, ""}, 13 | {"GET", "/v2/space_quota_definitions_page_2", listSpaceQuotasPayloadPage2, "", 200, ""}, 14 | } 15 | setupMultiple(mocks, t) 16 | defer teardown() 17 | c := &Config{ 18 | ApiAddress: server.URL, 19 | Token: "foobar", 20 | } 21 | client, err := NewClient(c) 22 | So(err, ShouldBeNil) 23 | 24 | spaceQuotas, err := client.ListSpaceQuotas() 25 | So(err, ShouldBeNil) 26 | 27 | So(len(spaceQuotas), ShouldEqual, 2) 28 | So(spaceQuotas[0].Guid, ShouldEqual, "889aa2ed-a883-4cc0-abe5-804b2503f15d") 29 | So(spaceQuotas[0].Name, ShouldEqual, "test-1") 30 | So(spaceQuotas[0].NonBasicServicesAllowed, ShouldEqual, true) 31 | So(spaceQuotas[0].TotalServices, ShouldEqual, -1) 32 | So(spaceQuotas[0].TotalRoutes, ShouldEqual, 100) 33 | So(spaceQuotas[0].MemoryLimit, ShouldEqual, 102400) 34 | So(spaceQuotas[0].InstanceMemoryLimit, ShouldEqual, -1) 35 | So(spaceQuotas[0].AppInstanceLimit, ShouldEqual, -1) 36 | So(spaceQuotas[0].AppTaskLimit, ShouldEqual, -1) 37 | So(spaceQuotas[0].TotalServiceKeys, ShouldEqual, -1) 38 | So(spaceQuotas[0].TotalReservedRoutePorts, ShouldEqual, -1) 39 | }) 40 | } 41 | 42 | func TestGetSpaceQuotaByName(t *testing.T) { 43 | Convey("Get Space Quota By Name", t, func() { 44 | setup(MockRoute{"GET", "/v2/space_quota_definitions", listSpaceQuotasPayloadPage2, "", 200, "q=name:test-2"}, t) 45 | defer teardown() 46 | c := &Config{ 47 | ApiAddress: server.URL, 48 | Token: "foobar", 49 | } 50 | client, err := NewClient(c) 51 | So(err, ShouldBeNil) 52 | 53 | spaceQuota, err := client.GetSpaceQuotaByName("test-2") 54 | So(err, ShouldBeNil) 55 | 56 | So(spaceQuota.Guid, ShouldEqual, "9ffd7c5c-d83c-4786-b399-b7bd54883977") 57 | So(spaceQuota.Name, ShouldEqual, "test-2") 58 | So(spaceQuota.NonBasicServicesAllowed, ShouldEqual, false) 59 | So(spaceQuota.TotalServices, ShouldEqual, 10) 60 | So(spaceQuota.TotalRoutes, ShouldEqual, 20) 61 | So(spaceQuota.MemoryLimit, ShouldEqual, 30) 62 | So(spaceQuota.InstanceMemoryLimit, ShouldEqual, 40) 63 | So(spaceQuota.AppInstanceLimit, ShouldEqual, 50) 64 | So(spaceQuota.AppTaskLimit, ShouldEqual, 60) 65 | So(spaceQuota.TotalServiceKeys, ShouldEqual, 70) 66 | So(spaceQuota.TotalReservedRoutePorts, ShouldEqual, 80) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /org_quotas_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListOrgQuotas(t *testing.T) { 10 | Convey("List Org Quotas", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/quota_definitions", listOrgQuotasPayloadPage1, "", 200, ""}, 13 | {"GET", "/v2/quota_definitions_page_2", listOrgQuotasPayloadPage2, "", 200, ""}, 14 | } 15 | setupMultiple(mocks, t) 16 | defer teardown() 17 | c := &Config{ 18 | ApiAddress: server.URL, 19 | Token: "foobar", 20 | } 21 | client, err := NewClient(c) 22 | So(err, ShouldBeNil) 23 | 24 | orgQuotas, err := client.ListOrgQuotas() 25 | So(err, ShouldBeNil) 26 | 27 | So(len(orgQuotas), ShouldEqual, 2) 28 | So(orgQuotas[0].Guid, ShouldEqual, "6f9d3100-44ab-49e2-a4f8-9d7d67651ae7") 29 | So(orgQuotas[0].Name, ShouldEqual, "test-1") 30 | So(orgQuotas[0].NonBasicServicesAllowed, ShouldEqual, true) 31 | So(orgQuotas[0].TotalServices, ShouldEqual, -1) 32 | So(orgQuotas[0].TotalRoutes, ShouldEqual, 100) 33 | So(orgQuotas[0].TotalPrivateDomains, ShouldEqual, -1) 34 | So(orgQuotas[0].MemoryLimit, ShouldEqual, 102400) 35 | So(orgQuotas[0].TrialDBAllowed, ShouldEqual, false) 36 | So(orgQuotas[0].InstanceMemoryLimit, ShouldEqual, -1) 37 | So(orgQuotas[0].AppInstanceLimit, ShouldEqual, -1) 38 | So(orgQuotas[0].AppTaskLimit, ShouldEqual, -1) 39 | So(orgQuotas[0].TotalServiceKeys, ShouldEqual, -1) 40 | So(orgQuotas[0].TotalReservedRoutePorts, ShouldEqual, -1) 41 | }) 42 | } 43 | 44 | func TestGetOrgQuotaByName(t *testing.T) { 45 | Convey("Get Org Quota By Name", t, func() { 46 | setup(MockRoute{"GET", "/v2/quota_definitions", listOrgQuotasPayloadPage2, "", 200, "q=name:default2"}, t) 47 | defer teardown() 48 | c := &Config{ 49 | ApiAddress: server.URL, 50 | Token: "foobar", 51 | } 52 | client, err := NewClient(c) 53 | So(err, ShouldBeNil) 54 | 55 | orgQuota, err := client.GetOrgQuotaByName("default2") 56 | So(err, ShouldBeNil) 57 | 58 | So(orgQuota.Guid, ShouldEqual, "a537761f-9d93-4b30-af17-3d73dbca181b") 59 | So(orgQuota.Name, ShouldEqual, "test-2") 60 | So(orgQuota.NonBasicServicesAllowed, ShouldEqual, false) 61 | So(orgQuota.TotalServices, ShouldEqual, 10) 62 | So(orgQuota.TotalRoutes, ShouldEqual, 20) 63 | So(orgQuota.TotalPrivateDomains, ShouldEqual, 30) 64 | So(orgQuota.MemoryLimit, ShouldEqual, 40) 65 | So(orgQuota.TrialDBAllowed, ShouldEqual, true) 66 | So(orgQuota.InstanceMemoryLimit, ShouldEqual, 50) 67 | So(orgQuota.AppInstanceLimit, ShouldEqual, 60) 68 | So(orgQuota.AppTaskLimit, ShouldEqual, 70) 69 | So(orgQuota.TotalServiceKeys, ShouldEqual, 80) 70 | So(orgQuota.TotalReservedRoutePorts, ShouldEqual, 90) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /service_instances_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListServicesInstances(t *testing.T) { 10 | Convey("List Service Instances", t, func() { 11 | setup(MockRoute{"GET", "/v2/service_instances", listServiceInstancePayload, "", 200, ""}, t) 12 | defer teardown() 13 | c := &Config{ 14 | ApiAddress: server.URL, 15 | Token: "foobar", 16 | } 17 | client, err := NewClient(c) 18 | So(err, ShouldBeNil) 19 | 20 | instances, err := client.ListServiceInstances() 21 | So(err, ShouldBeNil) 22 | 23 | So(len(instances), ShouldEqual, 2) 24 | So(instances[0].Guid, ShouldEqual, "8423ca96-90ad-411f-b77a-0907844949fc") 25 | So(instances[0].Name, ShouldEqual, "fortunes-db") 26 | }) 27 | } 28 | 29 | func TestServiceInstanceByGuid(t *testing.T) { 30 | Convey("Service instance by Guid", t, func() { 31 | setup(MockRoute{"GET", "/v2/service_instances/8423ca96-90ad-411f-b77a-0907844949fc", serviceInstancePayload, "", 200, ""}, t) 32 | defer teardown() 33 | 34 | c := &Config{ 35 | ApiAddress: server.URL, 36 | Token: "foobar", 37 | } 38 | client, err := NewClient(c) 39 | So(err, ShouldBeNil) 40 | 41 | service, err := client.ServiceInstanceByGuid("8423ca96-90ad-411f-b77a-0907844949fc") 42 | So(err, ShouldBeNil) 43 | 44 | expected := ServiceInstance{ 45 | Guid: "8423ca96-90ad-411f-b77a-0907844949fc", 46 | Credentials: map[string]interface{}{}, 47 | Name: "fortunes-db", 48 | LastOperation: LastOperation{ 49 | Type: "create", 50 | State: "succeeded", 51 | Description: "", 52 | UpdatedAt: "", 53 | CreatedAt: "2016-10-21T18:22:56Z", 54 | }, 55 | Tags: []string{}, 56 | ServiceGuid: "440ce9d9-b108-4bbe-80b4-08338f3cc25b", 57 | ServicePlanGuid: "f48419f7-4717-4706-86e4-a24973848a77", 58 | SpaceGuid: "21e5fdc7-5131-4743-8447-6373cf336a77", 59 | DashboardUrl: "https://p-mysql.system.example.com/manage/instances/8423ca96-90ad-411f-b77a-0907844949fc", 60 | Type: "managed_service_instance", 61 | SpaceUrl: "/v2/spaces/21e5fdc7-5131-4743-8447-6373cf336a77", 62 | ServicePlanUrl: "/v2/service_plans/f48419f7-4717-4706-86e4-a24973848a77", 63 | ServiceBindingsUrl: "/v2/service_instances/8423ca96-90ad-411f-b77a-0907844949fc/service_bindings", 64 | ServiceKeysUrl: "/v2/service_instances/8423ca96-90ad-411f-b77a-0907844949fc/service_keys", 65 | RoutesUrl: "/v2/service_instances/8423ca96-90ad-411f-b77a-0907844949fc/routes", 66 | ServiceUrl: "/v2/services/440ce9d9-b108-4bbe-80b4-08338f3cc25b", 67 | c: client, 68 | } 69 | So(service, ShouldResemble, expected) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /appevents_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListAppEvents(t *testing.T) { 10 | Convey("List App Events", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/events", listAppsCreatedEventPayload, "", 200, "q=type:audit.app.create"}, 13 | {"GET", "/v2/events2", listAppsCreatedEventPayload2, "", 200, ""}, 14 | } 15 | setupMultiple(mocks, t) 16 | defer teardown() 17 | c := &Config{ 18 | ApiAddress: server.URL, 19 | Token: "foobar", 20 | } 21 | client, err := NewClient(c) 22 | So(err, ShouldBeNil) 23 | appEvents, err := client.ListAppEvents("blub") 24 | So(err.Error(), ShouldEqual, "Unsupported app event type blub") 25 | appEvents, err = client.ListAppEvents(AppCreate) 26 | So(err, ShouldEqual, nil) 27 | So(len(appEvents), ShouldEqual, 2) 28 | So(appEvents[0].MetaData.Request.State, ShouldEqual, "STOPPED") 29 | So(appEvents[1].MetaData.Request.State, ShouldEqual, "STARTED") 30 | }) 31 | } 32 | 33 | func TestListAppEventsByQuery(t *testing.T) { 34 | Convey("List App Events By Query", t, func() { 35 | mocks := []MockRoute{ 36 | {"GET", "/v2/events", listAppsCreatedEventPayload, "", 200, "q=type:audit.app.create&q=actee:3ca436ff-67a8-468a-8c7d-27ec68a6cfe5"}, 37 | {"GET", "/v2/events2", listAppsCreatedEventPayload2, "", 200, ""}, 38 | } 39 | setupMultiple(mocks, t) 40 | defer teardown() 41 | c := &Config{ 42 | ApiAddress: server.URL, 43 | Token: "foobar", 44 | } 45 | client, err := NewClient(c) 46 | So(err, ShouldBeNil) 47 | 48 | appEvents, err := client.ListAppEventsByQuery("blub", []AppEventQuery{}) 49 | So(err.Error(), ShouldEqual, "Unsupported app event type blub") 50 | 51 | appEventQuery := AppEventQuery{ 52 | Filter: "nofilter", 53 | Operator: ":", 54 | Value: "retlifon", 55 | } 56 | appEvents, err = client.ListAppEventsByQuery(AppCreate, []AppEventQuery{appEventQuery}) 57 | So(err.Error(), ShouldEqual, "Unsupported query filter type nofilter") 58 | 59 | appEventQuery = AppEventQuery{ 60 | Filter: FilterTimestamp, 61 | Operator: "not", 62 | Value: "retlifon", 63 | } 64 | appEvents, err = client.ListAppEventsByQuery(AppCreate, []AppEventQuery{appEventQuery}) 65 | So(err.Error(), ShouldEqual, "Unsupported query operator type not") 66 | 67 | appEventQuery = AppEventQuery{ 68 | Filter: FilterActee, 69 | Operator: ":", 70 | Value: "3ca436ff-67a8-468a-8c7d-27ec68a6cfe5", 71 | } 72 | appEvents, err = client.ListAppEventsByQuery(AppCreate, []AppEventQuery{appEventQuery}) 73 | So(err, ShouldEqual, nil) 74 | So(len(appEvents), ShouldEqual, 2) 75 | So(appEvents[0].MetaData.Request.State, ShouldEqual, "STOPPED") 76 | So(appEvents[1].MetaData.Request.State, ShouldEqual, "STARTED") 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /domains_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListDomains(t *testing.T) { 10 | Convey("List domains", t, func() { 11 | setup(MockRoute{"GET", "/v2/private_domains", listDomainsPayload, "", 200, ""}, t) 12 | defer teardown() 13 | c := &Config{ 14 | ApiAddress: server.URL, 15 | Token: "foobar", 16 | } 17 | client, err := NewClient(c) 18 | So(err, ShouldBeNil) 19 | 20 | domains, err := client.ListDomains() 21 | So(err, ShouldBeNil) 22 | 23 | So(len(domains), ShouldEqual, 4) 24 | So(domains[0].Guid, ShouldEqual, "b2a35f0c-d5ad-4a59-bea7-461711d96b0d") 25 | So(domains[0].Name, ShouldEqual, "vcap.me") 26 | So(domains[0].OwningOrganizationGuid, ShouldEqual, "4cf3bc47-eccd-4662-9322-7833c3bdcded") 27 | So(domains[0].OwningOrganizationUrl, ShouldEqual, "/v2/organizations/4cf3bc47-eccd-4662-9322-7833c3bdcded") 28 | So(domains[0].SharedOrganizationsUrl, ShouldEqual, "/v2/private_domains/b2a35f0c-d5ad-4a59-bea7-461711d96b0d/shared_organizations") 29 | }) 30 | } 31 | 32 | func TestListSharedDomains(t *testing.T) { 33 | Convey("List shared domains", t, func() { 34 | setup(MockRoute{"GET", "/v2/shared_domains", listSharedDomainsPayload, "", 200, ""}, t) 35 | defer teardown() 36 | c := &Config{ 37 | ApiAddress: server.URL, 38 | Token: "foobar", 39 | } 40 | client, err := NewClient(c) 41 | So(err, ShouldBeNil) 42 | 43 | domains, err := client.ListSharedDomains() 44 | So(err, ShouldBeNil) 45 | 46 | So(len(domains), ShouldEqual, 1) 47 | So(domains[0].Guid, ShouldEqual, "91977695-8ad9-40db-858f-4df782603ec3") 48 | So(domains[0].Name, ShouldEqual, "domain-49.example.com") 49 | So(domains[0].RouterGroupGuid, ShouldEqual, "my-random-guid") 50 | So(domains[0].RouterGroupType, ShouldEqual, "tcp") 51 | }) 52 | } 53 | 54 | func TestCreateDomain(t *testing.T) { 55 | Convey("Create domain", t, func() { 56 | setup(MockRoute{"POST", "/v2/private_domains", postDomainPayload, "", 201, ""}, t) 57 | defer teardown() 58 | c := &Config{ 59 | ApiAddress: server.URL, 60 | Token: "foobar", 61 | } 62 | client, err := NewClient(c) 63 | So(err, ShouldBeNil) 64 | 65 | domain, err := client.CreateDomain("exmaple.com", "8483e4f1-d3a3-43e2-ab8c-b05ea40ef8db") 66 | So(err, ShouldBeNil) 67 | 68 | So(domain.Guid, ShouldEqual, "b98aeca1-22b9-49f9-8428-3ace9ea2ba11") 69 | }) 70 | } 71 | 72 | func TestDeleteDomain(t *testing.T) { 73 | Convey("Delete domain", t, func() { 74 | setup(MockRoute{"DELETE", "/v2/private_domains/b2a35f0c-d5ad-4a59-bea7-461711d96b0d", "", "", 204, ""}, t) 75 | defer teardown() 76 | c := &Config{ 77 | ApiAddress: server.URL, 78 | Token: "foobar", 79 | } 80 | client, err := NewClient(c) 81 | So(err, ShouldBeNil) 82 | 83 | err = client.DeleteDomain("b2a35f0c-d5ad-4a59-bea7-461711d96b0d") 84 | So(err, ShouldBeNil) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /org_quotas.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/url" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type OrgQuotasResponse struct { 13 | Count int `json:"total_results"` 14 | Pages int `json:"total_pages"` 15 | NextUrl string `json:"next_url"` 16 | Resources []OrgQuotasResource `json:"resources"` 17 | } 18 | 19 | type OrgQuotasResource struct { 20 | Meta Meta `json:"metadata"` 21 | Entity OrgQuota `json:"entity"` 22 | } 23 | 24 | type OrgQuota struct { 25 | Guid string `json:"guid"` 26 | Name string `json:"name"` 27 | NonBasicServicesAllowed bool `json:"non_basic_services_allowed"` 28 | TotalServices int `json:"total_services"` 29 | TotalRoutes int `json:"total_routes"` 30 | TotalPrivateDomains int `json:"total_private_domains"` 31 | MemoryLimit int `json:"memory_limit"` 32 | TrialDBAllowed bool `json:"trial_db_allowed"` 33 | InstanceMemoryLimit int `json:"instance_memory_limit"` 34 | AppInstanceLimit int `json:"app_instance_limit"` 35 | AppTaskLimit int `json:"app_task_limit"` 36 | TotalServiceKeys int `json:"total_service_keys"` 37 | TotalReservedRoutePorts int `json:"total_reserved_route_ports"` 38 | c *Client 39 | } 40 | 41 | func (c *Client) ListOrgQuotasByQuery(query url.Values) ([]OrgQuota, error) { 42 | var orgQuotas []OrgQuota 43 | requestUrl := "/v2/quota_definitions?" + query.Encode() 44 | for { 45 | orgQuotasResp, err := c.getOrgQuotasResponse(requestUrl) 46 | if err != nil { 47 | return []OrgQuota{}, err 48 | } 49 | for _, org := range orgQuotasResp.Resources { 50 | org.Entity.Guid = org.Meta.Guid 51 | org.Entity.c = c 52 | orgQuotas = append(orgQuotas, org.Entity) 53 | } 54 | requestUrl = orgQuotasResp.NextUrl 55 | if requestUrl == "" { 56 | break 57 | } 58 | } 59 | return orgQuotas, nil 60 | } 61 | 62 | func (c *Client) ListOrgQuotas() ([]OrgQuota, error) { 63 | return c.ListOrgQuotasByQuery(nil) 64 | } 65 | 66 | func (c *Client) GetOrgQuotaByName(name string) (OrgQuota, error) { 67 | q := url.Values{} 68 | q.Set("q", "name:"+name) 69 | orgQuotas, err := c.ListOrgQuotasByQuery(q) 70 | if err != nil { 71 | return OrgQuota{}, err 72 | } 73 | if len(orgQuotas) != 1 { 74 | return OrgQuota{}, fmt.Errorf("Unable to find org quota " + name) 75 | } 76 | return orgQuotas[0], nil 77 | } 78 | 79 | func (c *Client) getOrgQuotasResponse(requestUrl string) (OrgQuotasResponse, error) { 80 | var orgQuotasResp OrgQuotasResponse 81 | r := c.NewRequest("GET", requestUrl) 82 | resp, err := c.DoRequest(r) 83 | if err != nil { 84 | return OrgQuotasResponse{}, errors.Wrap(err, "Error requesting org quotas") 85 | } 86 | resBody, err := ioutil.ReadAll(resp.Body) 87 | defer resp.Body.Close() 88 | if err != nil { 89 | return OrgQuotasResponse{}, errors.Wrap(err, "Error reading org quotas body") 90 | } 91 | err = json.Unmarshal(resBody, &orgQuotasResp) 92 | if err != nil { 93 | return OrgQuotasResponse{}, errors.Wrap(err, "Error unmarshalling org quotas") 94 | } 95 | return orgQuotasResp, nil 96 | } 97 | -------------------------------------------------------------------------------- /space_quotas.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/url" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type SpaceQuotasResponse struct { 13 | Count int `json:"total_results"` 14 | Pages int `json:"total_pages"` 15 | NextUrl string `json:"next_url"` 16 | Resources []SpaceQuotasResource `json:"resources"` 17 | } 18 | 19 | type SpaceQuotasResource struct { 20 | Meta Meta `json:"metadata"` 21 | Entity SpaceQuota `json:"entity"` 22 | } 23 | 24 | type SpaceQuota struct { 25 | Guid string `json:"guid"` 26 | Name string `json:"name"` 27 | OrganizationGuid string `json:"organization_guid"` 28 | NonBasicServicesAllowed bool `json:"non_basic_services_allowed"` 29 | TotalServices int `json:"total_services"` 30 | TotalRoutes int `json:"total_routes"` 31 | MemoryLimit int `json:"memory_limit"` 32 | InstanceMemoryLimit int `json:"instance_memory_limit"` 33 | AppInstanceLimit int `json:"app_instance_limit"` 34 | AppTaskLimit int `json:"app_task_limit"` 35 | TotalServiceKeys int `json:"total_service_keys"` 36 | TotalReservedRoutePorts int `json:"total_reserved_route_ports"` 37 | c *Client 38 | } 39 | 40 | func (c *Client) ListSpaceQuotasByQuery(query url.Values) ([]SpaceQuota, error) { 41 | var spaceQuotas []SpaceQuota 42 | requestUrl := "/v2/space_quota_definitions?" + query.Encode() 43 | for { 44 | spaceQuotasResp, err := c.getSpaceQuotasResponse(requestUrl) 45 | if err != nil { 46 | return []SpaceQuota{}, err 47 | } 48 | for _, space := range spaceQuotasResp.Resources { 49 | space.Entity.Guid = space.Meta.Guid 50 | space.Entity.c = c 51 | spaceQuotas = append(spaceQuotas, space.Entity) 52 | } 53 | requestUrl = spaceQuotasResp.NextUrl 54 | if requestUrl == "" { 55 | break 56 | } 57 | } 58 | return spaceQuotas, nil 59 | } 60 | 61 | func (c *Client) ListSpaceQuotas() ([]SpaceQuota, error) { 62 | return c.ListSpaceQuotasByQuery(nil) 63 | } 64 | 65 | func (c *Client) GetSpaceQuotaByName(name string) (SpaceQuota, error) { 66 | q := url.Values{} 67 | q.Set("q", "name:"+name) 68 | spaceQuotas, err := c.ListSpaceQuotasByQuery(q) 69 | if err != nil { 70 | return SpaceQuota{}, err 71 | } 72 | if len(spaceQuotas) != 1 { 73 | return SpaceQuota{}, fmt.Errorf("Unable to find space quota " + name) 74 | } 75 | return spaceQuotas[0], nil 76 | } 77 | 78 | func (c *Client) getSpaceQuotasResponse(requestUrl string) (SpaceQuotasResponse, error) { 79 | var spaceQuotasResp SpaceQuotasResponse 80 | r := c.NewRequest("GET", requestUrl) 81 | resp, err := c.DoRequest(r) 82 | if err != nil { 83 | return SpaceQuotasResponse{}, errors.Wrap(err, "Error requesting space quotas") 84 | } 85 | resBody, err := ioutil.ReadAll(resp.Body) 86 | defer resp.Body.Close() 87 | if err != nil { 88 | return SpaceQuotasResponse{}, errors.Wrap(err, "Error reading space quotas body") 89 | } 90 | err = json.Unmarshal(resBody, &spaceQuotasResp) 91 | if err != nil { 92 | return SpaceQuotasResponse{}, errors.Wrap(err, "Error unmarshalling space quotas") 93 | } 94 | return spaceQuotasResp, nil 95 | } 96 | -------------------------------------------------------------------------------- /cf_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/go-martini/martini" 11 | "github.com/martini-contrib/render" 12 | ) 13 | 14 | var ( 15 | mux *http.ServeMux 16 | server *httptest.Server 17 | fakeUAAServer *httptest.Server 18 | ) 19 | 20 | type MockRoute struct { 21 | Method string 22 | Endpoint string 23 | Output string 24 | UserAgent string 25 | Status int 26 | QueryString string 27 | } 28 | 29 | func setup(mock MockRoute, t *testing.T) { 30 | setupMultiple([]MockRoute{mock}, t) 31 | } 32 | 33 | func testQueryString(QueryString string, QueryStringExp string, t *testing.T) { 34 | value, _ := url.QueryUnescape(QueryString) 35 | 36 | if QueryStringExp != value { 37 | t.Fatalf("Error: Query string '%s' should be equal to '%s'", QueryStringExp, value) 38 | } 39 | } 40 | 41 | func testUserAgent(UserAgent string, UserAgentExp string, t *testing.T) { 42 | if len(UserAgentExp) < 1 { 43 | UserAgentExp = "Go-CF-client/1.1" 44 | } 45 | if UserAgent != UserAgentExp { 46 | t.Fatalf("Error Agent %s should be equal to %s", UserAgent, UserAgentExp) 47 | } 48 | } 49 | 50 | func setupMultiple(mockEndpoints []MockRoute, t *testing.T) { 51 | mux = http.NewServeMux() 52 | server = httptest.NewServer(mux) 53 | fakeUAAServer = FakeUAAServer() 54 | m := martini.New() 55 | m.Use(render.Renderer()) 56 | r := martini.NewRouter() 57 | for _, mock := range mockEndpoints { 58 | method := mock.Method 59 | endpoint := mock.Endpoint 60 | output := mock.Output 61 | userAgent := mock.UserAgent 62 | status := mock.Status 63 | queryString := mock.QueryString 64 | if method == "GET" { 65 | r.Get(endpoint, func(req *http.Request) string { 66 | testUserAgent(req.Header.Get("User-Agent"), userAgent, t) 67 | testQueryString(req.URL.RawQuery, queryString, t) 68 | return output 69 | }) 70 | } else if method == "POST" { 71 | r.Post(endpoint, func(req *http.Request) (int, string) { 72 | testUserAgent(req.Header.Get("User-Agent"), userAgent, t) 73 | testQueryString(req.URL.RawQuery, queryString, t) 74 | return status, output 75 | }) 76 | } else if method == "DELETE" { 77 | r.Delete(endpoint, func(req *http.Request) (int, string) { 78 | testUserAgent(req.Header.Get("User-Agent"), userAgent, t) 79 | testQueryString(req.URL.RawQuery, queryString, t) 80 | return status, output 81 | }) 82 | } else if method == "PUT" { 83 | r.Put(endpoint, func(req *http.Request) (int, string) { 84 | testUserAgent(req.Header.Get("User-Agent"), userAgent, t) 85 | testQueryString(req.URL.RawQuery, queryString, t) 86 | return status, output 87 | }) 88 | } 89 | } 90 | r.Get("/v2/info", func(r render.Render) { 91 | r.JSON(200, map[string]interface{}{ 92 | "authorization_endpoint": fakeUAAServer.URL, 93 | "token_endpoint": fakeUAAServer.URL, 94 | "logging_endpoint": server.URL, 95 | }) 96 | 97 | }) 98 | 99 | m.Action(r.Handle) 100 | mux.Handle("/", m) 101 | } 102 | 103 | func FakeUAAServer() *httptest.Server { 104 | mux := http.NewServeMux() 105 | server := httptest.NewServer(mux) 106 | m := martini.New() 107 | m.Use(render.Renderer()) 108 | r := martini.NewRouter() 109 | count := 1 110 | r.Post("/oauth/token", func(r render.Render) { 111 | r.JSON(200, map[string]interface{}{ 112 | "token_type": "bearer", 113 | "access_token": "foobar" + strconv.Itoa(count), 114 | "refresh_token": "barfoo", 115 | "expires_in": 3, 116 | }) 117 | count = count + 1 118 | }) 119 | r.NotFound(func() string { return "" }) 120 | m.Action(r.Handle) 121 | mux.Handle("/", m) 122 | return server 123 | } 124 | 125 | func teardown() { 126 | server.Close() 127 | fakeUAAServer.Close() 128 | } 129 | -------------------------------------------------------------------------------- /service_instances.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/url" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type ServiceInstancesResponse struct { 12 | Count int `json:"total_results"` 13 | Pages int `json:"total_pages"` 14 | NextUrl string `json:"next_url"` 15 | Resources []ServiceInstanceResource `json:"resources"` 16 | } 17 | 18 | type ServiceInstanceResource struct { 19 | Meta Meta `json:"metadata"` 20 | Entity ServiceInstance `json:"entity"` 21 | } 22 | 23 | type ServiceInstance struct { 24 | Name string `json:"name"` 25 | Credentials map[string]interface{} `json:"credentials"` 26 | ServicePlanGuid string `json:"service_plan_guid"` 27 | SpaceGuid string `json:"space_guid"` 28 | DashboardUrl string `json:"dashboard_url"` 29 | Type string `json:"type"` 30 | LastOperation LastOperation `json:"last_operation"` 31 | Tags []string `json:"tags"` 32 | ServiceGuid string `json:"service_guid"` 33 | SpaceUrl string `json:"space_url"` 34 | ServicePlanUrl string `json:"service_plan_url"` 35 | ServiceBindingsUrl string `json:"service_bindings_url"` 36 | ServiceKeysUrl string `json:"service_keys_url"` 37 | RoutesUrl string `json:"routes_url"` 38 | ServiceUrl string `json:"service_url"` 39 | Guid string `json:"guid"` 40 | c *Client 41 | } 42 | 43 | type LastOperation struct { 44 | Type string `json:"type"` 45 | State string `json:"state"` 46 | Description string `json:"description"` 47 | UpdatedAt string `json:"updated_at"` 48 | CreatedAt string `json:"created_at"` 49 | } 50 | 51 | func (c *Client) ListServiceInstancesByQuery(query url.Values) ([]ServiceInstance, error) { 52 | var instances []ServiceInstance 53 | 54 | requestUrl := "/v2/service_instances?" + query.Encode() 55 | for { 56 | var sir ServiceInstancesResponse 57 | r := c.NewRequest("GET", requestUrl) 58 | resp, err := c.DoRequest(r) 59 | if err != nil { 60 | return nil, errors.Wrap(err, "Error requesting service instances") 61 | } 62 | resBody, err := ioutil.ReadAll(resp.Body) 63 | if err != nil { 64 | return nil, errors.Wrap(err, "Error reading service instances request:") 65 | } 66 | 67 | err = json.Unmarshal(resBody, &sir) 68 | if err != nil { 69 | return nil, errors.Wrap(err, "Error unmarshaling service instances") 70 | } 71 | for _, instance := range sir.Resources { 72 | instance.Entity.Guid = instance.Meta.Guid 73 | instance.Entity.c = c 74 | instances = append(instances, instance.Entity) 75 | } 76 | 77 | requestUrl = sir.NextUrl 78 | if requestUrl == "" { 79 | break 80 | } 81 | } 82 | return instances, nil 83 | } 84 | 85 | func (c *Client) ListServiceInstances() ([]ServiceInstance, error) { 86 | return c.ListServiceInstancesByQuery(nil) 87 | } 88 | 89 | func (c *Client) ServiceInstanceByGuid(guid string) (ServiceInstance, error) { 90 | var sir ServiceInstanceResource 91 | req := c.NewRequest("GET", "/v2/service_instances/"+guid) 92 | res, err := c.DoRequest(req) 93 | if err != nil { 94 | return ServiceInstance{}, errors.Wrap(err, "Error requesting service instance") 95 | } 96 | 97 | data, err := ioutil.ReadAll(res.Body) 98 | if err != nil { 99 | return ServiceInstance{}, errors.Wrap(err, "Error reading service instance response") 100 | } 101 | err = json.Unmarshal(data, &sir) 102 | if err != nil { 103 | return ServiceInstance{}, errors.Wrap(err, "Error JSON parsing service instance response") 104 | } 105 | sir.Entity.Guid = sir.Meta.Guid 106 | sir.Entity.c = c 107 | return sir.Entity, nil 108 | } 109 | -------------------------------------------------------------------------------- /secgroups_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListSecGroups(t *testing.T) { 10 | Convey("List SecGroups", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/security_groups", listSecGroupsPayload, "", 200, "inline-relations-depth=1"}, 13 | {"GET", "/v2/security_groupsPage2", listSecGroupsPayloadPage2, "", 200, ""}, 14 | {"GET", "/v2/security_groups/af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c/spaces", emptyResources, "", 200, ""}, 15 | } 16 | setupMultiple(mocks, t) 17 | defer teardown() 18 | c := &Config{ 19 | ApiAddress: server.URL, 20 | Token: "foobar", 21 | } 22 | client, err := NewClient(c) 23 | So(err, ShouldBeNil) 24 | 25 | secGroups, err := client.ListSecGroups() 26 | So(err, ShouldBeNil) 27 | 28 | So(len(secGroups), ShouldEqual, 2) 29 | So(secGroups[0].Guid, ShouldEqual, "af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c") 30 | So(secGroups[0].Name, ShouldEqual, "secgroup-test") 31 | So(secGroups[0].Running, ShouldEqual, true) 32 | So(secGroups[0].Staging, ShouldEqual, true) 33 | So(secGroups[0].Rules[0].Protocol, ShouldEqual, "tcp") 34 | So(secGroups[0].Rules[0].Ports, ShouldEqual, "443,4443") 35 | So(secGroups[0].Rules[0].Destination, ShouldEqual, "1.1.1.1") 36 | So(secGroups[0].Rules[1].Protocol, ShouldEqual, "udp") 37 | So(secGroups[0].Rules[1].Ports, ShouldEqual, "1111") 38 | So(secGroups[0].Rules[1].Destination, ShouldEqual, "1.2.3.4") 39 | So(secGroups[0].SpacesURL, ShouldEqual, "/v2/security_groups/af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c/spaces") 40 | So(secGroups[0].SpacesData, ShouldBeEmpty) 41 | So(secGroups[1].Guid, ShouldEqual, "f9ad202b-76dd-44ec-b7c2-fd2417a561e8") 42 | So(secGroups[1].Name, ShouldEqual, "secgroup-test2") 43 | So(secGroups[1].Running, ShouldEqual, false) 44 | So(secGroups[1].Staging, ShouldEqual, false) 45 | So(secGroups[1].Rules[0].Protocol, ShouldEqual, "udp") 46 | So(secGroups[1].Rules[0].Ports, ShouldEqual, "2222") 47 | So(secGroups[1].Rules[0].Destination, ShouldEqual, "2.2.2.2") 48 | So(secGroups[1].Rules[1].Protocol, ShouldEqual, "tcp") 49 | So(secGroups[1].Rules[1].Ports, ShouldEqual, "443,4443") 50 | So(secGroups[1].Rules[1].Destination, ShouldEqual, "4.3.2.1") 51 | So(secGroups[1].SpacesData[0].Entity.Guid, ShouldEqual, "e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4") 52 | So(secGroups[1].SpacesData[0].Entity.Name, ShouldEqual, "space-test") 53 | So(secGroups[1].SpacesData[1].Entity.Guid, ShouldEqual, "a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333") 54 | So(secGroups[1].SpacesData[1].Entity.Name, ShouldEqual, "space-test2") 55 | So(secGroups[1].SpacesData[2].Entity.Guid, ShouldEqual, "c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1") 56 | So(secGroups[1].SpacesData[2].Entity.Name, ShouldEqual, "space-test3") 57 | }) 58 | } 59 | 60 | func TestSecGroupListSpaceResources(t *testing.T) { 61 | Convey("List Space Resources", t, func() { 62 | mocks := []MockRoute{ 63 | {"GET", "/v2/security_groups/123/spaces", listSpacesPayload, "", 200, ""}, 64 | {"GET", "/v2/spacesPage2", listSpacesPayloadPage2, "", 200, ""}, 65 | } 66 | setupMultiple(mocks, t) 67 | defer teardown() 68 | c := &Config{ 69 | ApiAddress: server.URL, 70 | Token: "foobar", 71 | } 72 | client, err := NewClient(c) 73 | So(err, ShouldBeNil) 74 | 75 | secGroup := &SecGroup{ 76 | Guid: "123", 77 | Name: "test-sec-group", 78 | SpacesURL: "/v2/security_groups/123/spaces", 79 | c: client, 80 | } 81 | spaces, err := secGroup.ListSpaceResources() 82 | So(err, ShouldBeNil) 83 | 84 | So(len(spaces), ShouldEqual, 4) 85 | So(spaces[0].Entity.Guid, ShouldEqual, "8efd7c5c-d83c-4786-b399-b7bd548839e1") 86 | So(spaces[0].Entity.Name, ShouldEqual, "dev") 87 | So(spaces[1].Entity.Guid, ShouldEqual, "657b5923-7de0-486a-9928-b4d78ee24931") 88 | So(spaces[1].Entity.Name, ShouldEqual, "demo") 89 | So(spaces[2].Entity.Guid, ShouldEqual, "9ffd7c5c-d83c-4786-b399-b7bd54883977") 90 | So(spaces[2].Entity.Name, ShouldEqual, "test") 91 | So(spaces[3].Entity.Guid, ShouldEqual, "329b5923-7de0-486a-9928-b4d78ee24982") 92 | So(spaces[3].Entity.Name, ShouldEqual, "prod") 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/cloudfoundry-community/go-cfclient", 3 | "GoVersion": "go1.5.3", 4 | "GodepVersion": "v79", 5 | "Deps": [ 6 | { 7 | "ImportPath": "github.com/codegangsta/inject", 8 | "Comment": "v1.0-rc1-10-g33e0aa1", 9 | "Rev": "33e0aa1cb7c019ccc3fbe049a8262a6403d30504" 10 | }, 11 | { 12 | "ImportPath": "github.com/go-martini/martini", 13 | "Comment": "v1.0-117-g2047b73", 14 | "Rev": "2047b7394d319aa3feb49db1452202336fb66fb6" 15 | }, 16 | { 17 | "ImportPath": "github.com/jtolds/gls", 18 | "Rev": "9a4a02dbe491bef4bab3c24fd9f3087d6c4c6690" 19 | }, 20 | { 21 | "ImportPath": "github.com/martini-contrib/render", 22 | "Rev": "b65063311fac6cbf84d3538f5715f7847e648322" 23 | }, 24 | { 25 | "ImportPath": "github.com/onsi/gomega", 26 | "Comment": "v1.0-35-g4dfabf7", 27 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 28 | }, 29 | { 30 | "ImportPath": "github.com/onsi/gomega/format", 31 | "Comment": "v1.0-35-g4dfabf7", 32 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 33 | }, 34 | { 35 | "ImportPath": "github.com/onsi/gomega/internal/assertion", 36 | "Comment": "v1.0-35-g4dfabf7", 37 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 38 | }, 39 | { 40 | "ImportPath": "github.com/onsi/gomega/internal/asyncassertion", 41 | "Comment": "v1.0-35-g4dfabf7", 42 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 43 | }, 44 | { 45 | "ImportPath": "github.com/onsi/gomega/internal/testingtsupport", 46 | "Comment": "v1.0-35-g4dfabf7", 47 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 48 | }, 49 | { 50 | "ImportPath": "github.com/onsi/gomega/matchers", 51 | "Comment": "v1.0-35-g4dfabf7", 52 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 53 | }, 54 | { 55 | "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph", 56 | "Comment": "v1.0-35-g4dfabf7", 57 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 58 | }, 59 | { 60 | "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/edge", 61 | "Comment": "v1.0-35-g4dfabf7", 62 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 63 | }, 64 | { 65 | "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/node", 66 | "Comment": "v1.0-35-g4dfabf7", 67 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 68 | }, 69 | { 70 | "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/util", 71 | "Comment": "v1.0-35-g4dfabf7", 72 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 73 | }, 74 | { 75 | "ImportPath": "github.com/onsi/gomega/types", 76 | "Comment": "v1.0-35-g4dfabf7", 77 | "Rev": "4dfabf7db2e4147ec99a86db32b2f2a3484cfee8" 78 | }, 79 | { 80 | "ImportPath": "github.com/oxtoacart/bpool", 81 | "Rev": "754d38fccfc6e52083355014b6abc8d626445dfd" 82 | }, 83 | { 84 | "ImportPath": "github.com/pkg/errors", 85 | "Comment": "v0.8.0-3-gbfd5150", 86 | "Rev": "bfd5150e4e41705ded2129ec33379de1cb90b513" 87 | }, 88 | { 89 | "ImportPath": "github.com/smartystreets/assertions", 90 | "Comment": "1.5.0-414-g287b434", 91 | "Rev": "287b4346dc4e71a038c346375a9d572453bc469b" 92 | }, 93 | { 94 | "ImportPath": "github.com/smartystreets/assertions/internal/go-render/render", 95 | "Comment": "1.5.0-414-g287b434", 96 | "Rev": "287b4346dc4e71a038c346375a9d572453bc469b" 97 | }, 98 | { 99 | "ImportPath": "github.com/smartystreets/assertions/internal/oglematchers", 100 | "Comment": "1.5.0-414-g287b434", 101 | "Rev": "287b4346dc4e71a038c346375a9d572453bc469b" 102 | }, 103 | { 104 | "ImportPath": "github.com/smartystreets/goconvey/convey", 105 | "Comment": "1.5.0-386-geb2e83c", 106 | "Rev": "eb2e83c1df892d2c9ad5a3c85672da30be585dfd" 107 | }, 108 | { 109 | "ImportPath": "github.com/smartystreets/goconvey/convey/gotest", 110 | "Comment": "1.5.0-386-geb2e83c", 111 | "Rev": "eb2e83c1df892d2c9ad5a3c85672da30be585dfd" 112 | }, 113 | { 114 | "ImportPath": "github.com/smartystreets/goconvey/convey/reporting", 115 | "Comment": "1.5.0-386-geb2e83c", 116 | "Rev": "eb2e83c1df892d2c9ad5a3c85672da30be585dfd" 117 | }, 118 | { 119 | "ImportPath": "golang.org/x/net/context", 120 | "Rev": "7dbad50ab5b31073856416cdcfeb2796d682f844" 121 | }, 122 | { 123 | "ImportPath": "golang.org/x/oauth2", 124 | "Rev": "2baa8a1b9338cf13d9eeb27696d761155fa480be" 125 | }, 126 | { 127 | "ImportPath": "golang.org/x/oauth2/clientcredentials", 128 | "Rev": "2baa8a1b9338cf13d9eeb27696d761155fa480be" 129 | }, 130 | { 131 | "ImportPath": "golang.org/x/oauth2/internal", 132 | "Rev": "2baa8a1b9338cf13d9eeb27696d761155fa480be" 133 | } 134 | ] 135 | } 136 | -------------------------------------------------------------------------------- /users.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type UserRequest struct { 15 | Guid string `json:"guid"` 16 | DefaultSpaceGuid string `json:"default_space_guid,omitempty"` 17 | } 18 | 19 | type Users []User 20 | 21 | type User struct { 22 | Guid string `json:"guid"` 23 | Admin bool `json:"admin"` 24 | Active bool `json:"active"` 25 | DefaultSpaceGUID string `json:"default_space_guid"` 26 | Username string `json:"username"` 27 | SpacesURL string `json:"spaces_url"` 28 | OrgsURL string `json:"organizations_url"` 29 | ManagedOrgsURL string `json:"managed_organizations_url"` 30 | BillingManagedOrgsURL string `json:"billing_managed_organizations_url"` 31 | AuditedOrgsURL string `json:"audited_organizations_url"` 32 | ManagedSpacesURL string `json:"managed_spaces_url"` 33 | AuditedSpacesURL string `json:"audited_spaces_url"` 34 | c *Client 35 | } 36 | 37 | type UserResource struct { 38 | Meta Meta `json:"metadata"` 39 | Entity User `json:"entity"` 40 | } 41 | 42 | type UserResponse struct { 43 | Count int `json:"total_results"` 44 | Pages int `json:"total_pages"` 45 | NextUrl string `json:"next_url"` 46 | Resources []UserResource `json:"resources"` 47 | } 48 | 49 | func (c *Client) ListUsersByQuery(query url.Values) (Users, error) { 50 | var users []User 51 | requestUrl := "/v2/users?" + query.Encode() 52 | for { 53 | userResp, err := c.getUserResponse(requestUrl) 54 | if err != nil { 55 | return []User{}, err 56 | } 57 | for _, user := range userResp.Resources { 58 | user.Entity.Guid = user.Meta.Guid 59 | user.Entity.c = c 60 | users = append(users, user.Entity) 61 | } 62 | requestUrl = userResp.NextUrl 63 | if requestUrl == "" { 64 | break 65 | } 66 | } 67 | return users, nil 68 | } 69 | 70 | func (c *Client) ListUsers() (Users, error) { 71 | return c.ListUsersByQuery(nil) 72 | } 73 | 74 | func (c *Client) ListUserSpaces(userGuid string) ([]Space, error) { 75 | return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/spaces", userGuid)) 76 | } 77 | 78 | func (c *Client) ListUserAuditedSpaces(userGuid string) ([]Space, error) { 79 | return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/audited_spaces", userGuid)) 80 | } 81 | 82 | func (c *Client) ListUserManagedSpaces(userGuid string) ([]Space, error) { 83 | return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/managed_spaces", userGuid)) 84 | } 85 | 86 | func (c *Client) CreateUser(req UserRequest) (User, error) { 87 | buf := bytes.NewBuffer(nil) 88 | err := json.NewEncoder(buf).Encode(req) 89 | if err != nil { 90 | return User{}, err 91 | } 92 | r := c.NewRequestWithBody("POST", "/v2/users", buf) 93 | resp, err := c.DoRequest(r) 94 | if err != nil { 95 | return User{}, err 96 | } 97 | if resp.StatusCode != http.StatusCreated { 98 | return User{}, errors.Wrapf(err, "Error creating user, response code: %d", resp.StatusCode) 99 | } 100 | body, err := ioutil.ReadAll(resp.Body) 101 | defer resp.Body.Close() 102 | if err != nil { 103 | return User{}, err 104 | } 105 | var userResource UserResource 106 | err = json.Unmarshal(body, &userResource) 107 | if err != nil { 108 | return User{}, err 109 | } 110 | user := userResource.Entity 111 | user.Guid = userResource.Meta.Guid 112 | user.c = c 113 | return user, nil 114 | } 115 | 116 | func (c *Client) DeleteUser(userGuid string) error { 117 | resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/users/%s", userGuid))) 118 | if err != nil { 119 | return err 120 | } 121 | if resp.StatusCode != http.StatusNoContent { 122 | return errors.Wrapf(err, "Error deleting user %s, response code: %d", userGuid, resp.StatusCode) 123 | } 124 | return nil 125 | } 126 | 127 | func (u Users) GetUserByUsername(username string) User { 128 | for _, user := range u { 129 | if user.Username == username { 130 | return user 131 | } 132 | } 133 | return User{} 134 | } 135 | 136 | func (c *Client) getUserResponse(requestUrl string) (UserResponse, error) { 137 | var userResp UserResponse 138 | r := c.NewRequest("GET", requestUrl) 139 | resp, err := c.DoRequest(r) 140 | if err != nil { 141 | return UserResponse{}, errors.Wrap(err, "Error requesting users") 142 | } 143 | resBody, err := ioutil.ReadAll(resp.Body) 144 | defer resp.Body.Close() 145 | if err != nil { 146 | return UserResponse{}, errors.Wrap(err, "Error reading user request") 147 | } 148 | err = json.Unmarshal(resBody, &userResp) 149 | if err != nil { 150 | return UserResponse{}, errors.Wrap(err, "Error unmarshalling user") 151 | } 152 | return userResp, nil 153 | } 154 | -------------------------------------------------------------------------------- /users_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListUsers(t *testing.T) { 10 | Convey("List Users", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/users", listUsersPayload, "", 200, ""}, 13 | {"GET", "/v2/usersPage2", listUsersPayloadPage2, "", 200, ""}, 14 | } 15 | setupMultiple(mocks, t) 16 | defer teardown() 17 | c := &Config{ 18 | ApiAddress: server.URL, 19 | Token: "foobar", 20 | } 21 | client, err := NewClient(c) 22 | So(err, ShouldBeNil) 23 | 24 | users, err := client.ListUsers() 25 | So(err, ShouldBeNil) 26 | 27 | So(len(users), ShouldEqual, 4) 28 | So(users[0].Guid, ShouldEqual, "ccec6d06-5f71-48a0-a4c5-c91a1d9f2fac") 29 | So(users[0].Username, ShouldEqual, "testUser1") 30 | So(users[1].Guid, ShouldEqual, "f97f5699-c920-4633-aa23-bd70f3db0808") 31 | So(users[1].Username, ShouldEqual, "testUser2") 32 | So(users[2].Guid, ShouldEqual, "cadd6389-fcf6-4928-84f0-6153556bf693") 33 | So(users[2].Username, ShouldEqual, "testUser3") 34 | So(users[3].Guid, ShouldEqual, "79c854b0-c12a-41b7-8d3c-fdd6e116e385") 35 | So(users[3].Username, ShouldEqual, "testUser4") 36 | }) 37 | } 38 | 39 | func TestListUserSpaces(t *testing.T) { 40 | Convey("List User Spaces", t, func() { 41 | mocks := []MockRoute{ 42 | {"GET", "/v2/users/cadd6389-fcf6-4928-84f0-6153556bf693/spaces", listUserSpacesPayload, "", 200, ""}, 43 | } 44 | setupMultiple(mocks, t) 45 | defer teardown() 46 | c := &Config{ 47 | ApiAddress: server.URL, 48 | Token: "foobar", 49 | } 50 | client, err := NewClient(c) 51 | So(err, ShouldBeNil) 52 | 53 | spaces, err := client.ListUserSpaces("cadd6389-fcf6-4928-84f0-6153556bf693") 54 | So(err, ShouldBeNil) 55 | 56 | So(len(spaces), ShouldEqual, 1) 57 | So(spaces[0].Guid, ShouldEqual, "9881c79e-d269-4a53-9d77-cb21b745356e") 58 | So(spaces[0].Name, ShouldEqual, "dev") 59 | So(spaces[0].OrganizationGuid, ShouldEqual, "6a2a2d18-7620-43cf-a332-353824b431b2") 60 | }) 61 | } 62 | 63 | func TestListUserManagedSpaces(t *testing.T) { 64 | Convey("List User Audited Spaces", t, func() { 65 | mocks := []MockRoute{ 66 | {"GET", "/v2/users/cadd6389-fcf6-4928-84f0-6153556bf693/managed_spaces", listUserSpacesPayload, "", 200, ""}, 67 | } 68 | setupMultiple(mocks, t) 69 | defer teardown() 70 | c := &Config{ 71 | ApiAddress: server.URL, 72 | Token: "foobar", 73 | } 74 | client, err := NewClient(c) 75 | So(err, ShouldBeNil) 76 | 77 | spaces, err := client.ListUserManagedSpaces("cadd6389-fcf6-4928-84f0-6153556bf693") 78 | So(err, ShouldBeNil) 79 | 80 | So(len(spaces), ShouldEqual, 1) 81 | So(spaces[0].Guid, ShouldEqual, "9881c79e-d269-4a53-9d77-cb21b745356e") 82 | So(spaces[0].Name, ShouldEqual, "dev") 83 | So(spaces[0].OrganizationGuid, ShouldEqual, "6a2a2d18-7620-43cf-a332-353824b431b2") 84 | }) 85 | } 86 | 87 | func TestListUserAuditedSpaces(t *testing.T) { 88 | Convey("List User Managed Spaces", t, func() { 89 | mocks := []MockRoute{ 90 | {"GET", "/v2/users/cadd6389-fcf6-4928-84f0-6153556bf693/audited_spaces", listUserSpacesPayload, "", 200, ""}, 91 | } 92 | setupMultiple(mocks, t) 93 | defer teardown() 94 | c := &Config{ 95 | ApiAddress: server.URL, 96 | Token: "foobar", 97 | } 98 | client, err := NewClient(c) 99 | So(err, ShouldBeNil) 100 | 101 | spaces, err := client.ListUserAuditedSpaces("cadd6389-fcf6-4928-84f0-6153556bf693") 102 | So(err, ShouldBeNil) 103 | 104 | So(len(spaces), ShouldEqual, 1) 105 | So(spaces[0].Guid, ShouldEqual, "9881c79e-d269-4a53-9d77-cb21b745356e") 106 | So(spaces[0].Name, ShouldEqual, "dev") 107 | So(spaces[0].OrganizationGuid, ShouldEqual, "6a2a2d18-7620-43cf-a332-353824b431b2") 108 | }) 109 | } 110 | 111 | func TestGetUserByUsername(t *testing.T) { 112 | Convey("Get User by Username", t, func() { 113 | user1 := User{Guid: "ccec6d06-5f71-48a0-a4c5-c91a1d9f2fac", Username: "testUser1"} 114 | user2 := User{Guid: "f97f5699-c920-4633-aa23-bd70f3db0808", Username: "testUser2"} 115 | user3 := User{Guid: "cadd6389-fcf6-4928-84f0-6153556bf693", Username: "testUser3"} 116 | user4 := User{Guid: "79c854b0-c12a-41b7-8d3c-fdd6e116e385", Username: "testUser4"} 117 | users := Users{user1, user2, user3, user4} 118 | 119 | So(users.GetUserByUsername("testUser1"), ShouldResemble, user1) 120 | So(users.GetUserByUsername("testUser2"), ShouldResemble, user2) 121 | So(users.GetUserByUsername("testUser3"), ShouldResemble, user3) 122 | So(users.GetUserByUsername("testUser4"), ShouldResemble, user4) 123 | }) 124 | } 125 | 126 | func TestCreateUser(t *testing.T) { 127 | Convey("Create user", t, func() { 128 | setup(MockRoute{"POST", "/v2/users", createUserPayload, "", 201, ""}, t) 129 | defer teardown() 130 | c := &Config{ 131 | ApiAddress: server.URL, 132 | Token: "foobar", 133 | } 134 | client, err := NewClient(c) 135 | So(err, ShouldBeNil) 136 | 137 | user, err := client.CreateUser(UserRequest{Guid: "guid-cb24b36d-4656-468e-a50d-b53113ac6177"}) 138 | So(err, ShouldBeNil) 139 | So(user.Guid, ShouldEqual, "guid-cb24b36d-4656-468e-a50d-b53113ac6177") 140 | }) 141 | } 142 | 143 | func TestDeleteUser(t *testing.T) { 144 | Convey("Delete user", t, func() { 145 | setup(MockRoute{"DELETE", "/v2/users/guid-cb24b36d-4656-468e-a50d-b53113ac6177", "", "", 204, ""}, t) 146 | defer teardown() 147 | c := &Config{ 148 | ApiAddress: server.URL, 149 | Token: "foobar", 150 | } 151 | client, err := NewClient(c) 152 | So(err, ShouldBeNil) 153 | 154 | err = client.DeleteUser("guid-cb24b36d-4656-468e-a50d-b53113ac6177") 155 | So(err, ShouldBeNil) 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /domains.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type DomainsResponse struct { 15 | Count int `json:"total_results"` 16 | Pages int `json:"total_pages"` 17 | NextUrl string `json:"next_url"` 18 | Resources []DomainResource `json:"resources"` 19 | } 20 | 21 | type SharedDomainsResponse struct { 22 | Count int `json:"total_results"` 23 | Pages int `json:"total_pages"` 24 | NextUrl string `json:"next_url"` 25 | Resources []SharedDomainResource `json:"resources"` 26 | } 27 | 28 | type DomainResource struct { 29 | Meta Meta `json:"metadata"` 30 | Entity Domain `json:"entity"` 31 | } 32 | 33 | type SharedDomainResource struct { 34 | Meta Meta `json:"metadata"` 35 | Entity SharedDomain `json:"entity"` 36 | } 37 | 38 | type Domain struct { 39 | Guid string `json:"guid"` 40 | Name string `json:"name"` 41 | OwningOrganizationGuid string `json:"owning_organization_guid"` 42 | OwningOrganizationUrl string `json:"owning_organization_url"` 43 | SharedOrganizationsUrl string `json:"shared_organizations_url"` 44 | c *Client 45 | } 46 | 47 | type SharedDomain struct { 48 | Guid string `json:"guid"` 49 | Name string `json:"name"` 50 | RouterGroupGuid string `json:"router_group_guid"` 51 | RouterGroupType string `json:"router_group_type"` 52 | c *Client 53 | } 54 | 55 | func (c *Client) ListDomainsByQuery(query url.Values) ([]Domain, error) { 56 | var domains []Domain 57 | requestUrl := "/v2/private_domains?" + query.Encode() 58 | for { 59 | var domainResp DomainsResponse 60 | r := c.NewRequest("GET", requestUrl) 61 | resp, err := c.DoRequest(r) 62 | if err != nil { 63 | return nil, errors.Wrap(err, "Error requesting domains") 64 | } 65 | resBody, err := ioutil.ReadAll(resp.Body) 66 | if err != nil { 67 | return nil, errors.Wrap(err, "Error reading domains request") 68 | } 69 | 70 | err = json.Unmarshal(resBody, &domainResp) 71 | if err != nil { 72 | return nil, errors.Wrap(err, "Error unmarshaling domains") 73 | } 74 | for _, domain := range domainResp.Resources { 75 | domain.Entity.Guid = domain.Meta.Guid 76 | domain.Entity.c = c 77 | domains = append(domains, domain.Entity) 78 | } 79 | requestUrl = domainResp.NextUrl 80 | if requestUrl == "" { 81 | break 82 | } 83 | } 84 | return domains, nil 85 | } 86 | 87 | func (c *Client) ListDomains() ([]Domain, error) { 88 | return c.ListDomainsByQuery(nil) 89 | } 90 | 91 | func (c *Client) ListSharedDomainsByQuery(query url.Values) ([]SharedDomain, error) { 92 | var domains []SharedDomain 93 | requestUrl := "/v2/shared_domains?" + query.Encode() 94 | for { 95 | var domainResp SharedDomainsResponse 96 | r := c.NewRequest("GET", requestUrl) 97 | resp, err := c.DoRequest(r) 98 | if err != nil { 99 | return nil, errors.Wrap(err, "Error requesting shared domains") 100 | } 101 | resBody, err := ioutil.ReadAll(resp.Body) 102 | if err != nil { 103 | return nil, errors.Wrap(err, "Error reading shared domains request") 104 | } 105 | 106 | err = json.Unmarshal(resBody, &domainResp) 107 | if err != nil { 108 | return nil, errors.Wrap(err, "Error unmarshaling shared domains") 109 | } 110 | for _, domain := range domainResp.Resources { 111 | domain.Entity.Guid = domain.Meta.Guid 112 | domain.Entity.c = c 113 | domains = append(domains, domain.Entity) 114 | } 115 | requestUrl = domainResp.NextUrl 116 | if requestUrl == "" { 117 | break 118 | } 119 | } 120 | return domains, nil 121 | } 122 | 123 | func (c *Client) ListSharedDomains() ([]SharedDomain, error) { 124 | return c.ListSharedDomainsByQuery(nil) 125 | } 126 | 127 | func (c *Client) GetDomainByName(name string) (Domain, error) { 128 | q := url.Values{} 129 | q.Set("q", "name:"+name) 130 | domains, err := c.ListDomainsByQuery(q) 131 | if err != nil { 132 | return Domain{}, nil 133 | } 134 | if len(domains) == 0 { 135 | return Domain{}, errors.Wrapf(err, "Unable to find domain %s", name) 136 | } 137 | return domains[0], nil 138 | } 139 | 140 | func (c *Client) GetSharedDomainByName(name string) (SharedDomain, error) { 141 | q := url.Values{} 142 | q.Set("q", "name:"+name) 143 | domains, err := c.ListSharedDomainsByQuery(q) 144 | if err != nil { 145 | return SharedDomain{}, nil 146 | } 147 | if len(domains) == 0 { 148 | return SharedDomain{}, errors.Wrapf(err, "Unable to find shared domain %s", name) 149 | } 150 | return domains[0], nil 151 | } 152 | 153 | func (c *Client) CreateDomain(name, orgGuid string) (*Domain, error) { 154 | req := c.NewRequest("POST", "/v2/private_domains") 155 | req.obj = map[string]interface{}{ 156 | "name": name, 157 | "owning_organization_guid": orgGuid, 158 | } 159 | resp, err := c.DoRequest(req) 160 | if err != nil { 161 | return nil, err 162 | } 163 | if resp.StatusCode != http.StatusCreated { 164 | return nil, errors.Wrapf(err, "Error creating domain %s, response code: %d", name, resp.StatusCode) 165 | } 166 | return respBodyToDomain(resp.Body, c) 167 | } 168 | 169 | func (c *Client) DeleteDomain(guid string) error { 170 | resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/private_domains/%s", guid))) 171 | if err != nil { 172 | return err 173 | } 174 | if resp.StatusCode != http.StatusNoContent { 175 | return errors.Wrapf(err, "Error deleting domain %s, response code: %d", guid, resp.StatusCode) 176 | } 177 | return nil 178 | } 179 | 180 | func respBodyToDomain(body io.ReadCloser, c *Client) (*Domain, error) { 181 | bodyRaw, err := ioutil.ReadAll(body) 182 | if err != nil { 183 | return nil, err 184 | } 185 | domainRes := DomainResource{} 186 | err = json.Unmarshal([]byte(bodyRaw), &domainRes) 187 | if err != nil { 188 | return nil, err 189 | } 190 | domain := domainRes.Entity 191 | domain.Guid = domainRes.Meta.Guid 192 | domain.c = c 193 | return &domain, nil 194 | } 195 | -------------------------------------------------------------------------------- /appevents.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | const ( 12 | //AppCrash app.crash event const 13 | AppCrash = "app.crash" 14 | //AppStart audit.app.start event const 15 | AppStart = "audit.app.start" 16 | //AppStop audit.app.stop event const 17 | AppStop = "audit.app.stop" 18 | //AppUpdate audit.app.update event const 19 | AppUpdate = "audit.app.update" 20 | //AppCreate audit.app.create event const 21 | AppCreate = "audit.app.create" 22 | //AppDelete audit.app.delete-request event const 23 | AppDelete = "audit.app.delete-request" 24 | //AppSSHAuth audit.app.ssh-authorized event const 25 | AppSSHAuth = "audit.app.ssh-authorized" 26 | //AppSSHUnauth audit.app.ssh-unauthorized event const 27 | AppSSHUnauth = "audit.app.ssh-unauthorized" 28 | //AppRestage audit.app.restage event const 29 | AppRestage = "audit.app.restage" 30 | //AppMapRoute audit.app.map-route event const 31 | AppMapRoute = "audit.app.map-route" 32 | //AppUnmapRoute audit.app.unmap-route event const 33 | AppUnmapRoute = "audit.app.unmap-route" 34 | //FilterTimestamp const for query filter timestamp 35 | FilterTimestamp = "timestamp" 36 | //FilterActee const for query filter actee 37 | FilterActee = "actee" 38 | ) 39 | 40 | //ValidOperators const for all valid operators in a query 41 | var ValidOperators = []string{":", ">=", "<=", "<", ">", "IN"} 42 | 43 | // AppEventResponse the entire response 44 | type AppEventResponse struct { 45 | Results int `json:"total_results"` 46 | Pages int `json:"total_pages"` 47 | PrevURL string `json:"prev_url"` 48 | NextURL string `json:"next_url"` 49 | Resources []AppEventResource `json:"resources"` 50 | } 51 | 52 | // AppEventResource the event resources 53 | type AppEventResource struct { 54 | Meta Meta `json:"metadata"` 55 | Entity AppEventEntity `json:"entity"` 56 | } 57 | 58 | //AppEventQuery a struct for defining queries like 'q=filter>value' or 'q=filter IN a,b,c' 59 | type AppEventQuery struct { 60 | Filter string 61 | Operator string 62 | Value string 63 | } 64 | 65 | // The AppEventEntity the actual app event body 66 | type AppEventEntity struct { 67 | //EventTypes are app.crash, audit.app.start, audit.app.stop, audit.app.update, audit.app.create, audit.app.delete-request 68 | EventType string `json:"type"` 69 | //The GUID of the actor. 70 | Actor string `json:"actor"` 71 | //The actor type, user or app 72 | ActorType string `json:"actor_type"` 73 | //The name of the actor. 74 | ActorName string `json:"actor_name"` 75 | //The GUID of the actee. 76 | Actee string `json:"actee"` 77 | //The actee type, space, app or v3-app 78 | ActeeType string `json:"actee_type"` 79 | //The name of the actee. 80 | ActeeName string `json:"actee_name"` 81 | //Timestamp format "2016-02-26T13:29:44Z". The event creation time. 82 | Timestamp time.Time `json:"timestamp"` 83 | MetaData struct { 84 | Request struct { 85 | Name string `json:"name,omitempty"` 86 | Instances float64 `json:"instances,omitempty"` 87 | State string `json:"state,omitempty"` 88 | Memory float64 `json:"memory,omitempty"` 89 | EnvironmentVars string `json:"environment_json,omitempty"` 90 | DockerCredentials string `json:"docker_credentials_json,omitempty"` 91 | //audit.app.create event fields 92 | Console bool `json:"console,omitempty"` 93 | Buildpack string `json:"buildpack,omitempty"` 94 | Space string `json:"space_guid,omitempty"` 95 | HealthcheckType string `json:"health_check_type,omitempty"` 96 | HealthcheckTimeout float64 `json:"health_check_timeout,omitempty"` 97 | Production bool `json:"production,omitempty"` 98 | //app.crash event fields 99 | Index float64 `json:"index,omitempty"` 100 | ExitStatus string `json:"exit_status,omitempty"` 101 | ExitDescription string `json:"exit_description,omitempty"` 102 | ExitReason string `json:"reason,omitempty"` 103 | } `json:"request"` 104 | } `json:"metadata"` 105 | } 106 | 107 | // ListAppEvents returns all app events based on eventType 108 | func (c *Client) ListAppEvents(eventType string) ([]AppEventEntity, error) { 109 | return c.ListAppEventsByQuery(eventType, nil) 110 | } 111 | 112 | // ListAppEventsByQuery returns all app events based on eventType and queries 113 | func (c *Client) ListAppEventsByQuery(eventType string, queries []AppEventQuery) ([]AppEventEntity, error) { 114 | var events []AppEventEntity 115 | 116 | if eventType != AppCrash && eventType != AppStart && eventType != AppStop && eventType != AppUpdate && eventType != AppCreate && 117 | eventType != AppDelete && eventType != AppSSHAuth && eventType != AppSSHUnauth && eventType != AppRestage && 118 | eventType != AppMapRoute && eventType != AppUnmapRoute { 119 | return nil, errors.New("Unsupported app event type " + eventType) 120 | } 121 | 122 | var query = "/v2/events?q=type:" + eventType 123 | //adding the additional queries 124 | if queries != nil && len(queries) > 0 { 125 | for _, eventQuery := range queries { 126 | if eventQuery.Filter != FilterTimestamp && eventQuery.Filter != FilterActee { 127 | return nil, errors.New("Unsupported query filter type " + eventQuery.Filter) 128 | } 129 | if !stringInSlice(eventQuery.Operator, ValidOperators) { 130 | return nil, errors.New("Unsupported query operator type " + eventQuery.Operator) 131 | } 132 | query += "&q=" + eventQuery.Filter + eventQuery.Operator + eventQuery.Value 133 | } 134 | } 135 | 136 | for { 137 | eventResponse, err := c.getAppEventsResponse(query) 138 | if err != nil { 139 | return []AppEventEntity{}, err 140 | } 141 | for _, event := range eventResponse.Resources { 142 | events = append(events, event.Entity) 143 | } 144 | query = eventResponse.NextURL 145 | if query == "" { 146 | break 147 | } 148 | } 149 | 150 | return events, nil 151 | } 152 | 153 | func (c *Client) getAppEventsResponse(query string) (AppEventResponse, error) { 154 | var eventResponse AppEventResponse 155 | r := c.NewRequest("GET", query) 156 | resp, err := c.DoRequest(r) 157 | if err != nil { 158 | return AppEventResponse{}, errors.Wrap(err, "Error requesting appevents") 159 | } 160 | resBody, err := ioutil.ReadAll(resp.Body) 161 | defer resp.Body.Close() 162 | if err != nil { 163 | return AppEventResponse{}, errors.Wrap(err, "Error reading appevents response body") 164 | } 165 | 166 | err = json.Unmarshal(resBody, &eventResponse) 167 | if err != nil { 168 | return AppEventResponse{}, errors.Wrap(err, "Error unmarshalling appevent") 169 | } 170 | return eventResponse, nil 171 | } 172 | 173 | func stringInSlice(str string, list []string) bool { 174 | for _, v := range list { 175 | if v == str { 176 | return true 177 | } 178 | } 179 | return false 180 | } 181 | -------------------------------------------------------------------------------- /tasks.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/url" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // TaskListResponse is the JSON response from the API. 16 | type TaskListResponse struct { 17 | Pagination struct { 18 | TotalResults int `json:"total_results"` 19 | TotalPages int `json:"total_pages"` 20 | First struct { 21 | Href string `json:"href"` 22 | } `json:"first"` 23 | Last struct { 24 | Href string `json:"href"` 25 | } `json:"last"` 26 | Next interface{} `json:"next"` 27 | Previous interface{} `json:"previous"` 28 | } `json:"pagination"` 29 | Tasks []Task `json:"resources"` 30 | } 31 | 32 | // Task is a description of a task element. 33 | type Task struct { 34 | GUID string `json:"guid"` 35 | SequenceID int `json:"sequence_id"` 36 | Name string `json:"name"` 37 | Command string `json:"command"` 38 | State string `json:"state"` 39 | MemoryInMb int `json:"memory_in_mb"` 40 | DiskInMb int `json:"disk_in_mb"` 41 | Result struct { 42 | FailureReason string `json:"failure_reason"` 43 | } `json:"result"` 44 | CreatedAt time.Time `json:"created_at"` 45 | UpdatedAt time.Time `json:"updated_at"` 46 | DropletGUID string `json:"droplet_guid"` 47 | Links struct { 48 | Self struct { 49 | Href string `json:"href"` 50 | } `json:"self"` 51 | App struct { 52 | Href string `json:"href"` 53 | } `json:"app"` 54 | Droplet struct { 55 | Href string `json:"href"` 56 | } `json:"droplet"` 57 | } `json:"links"` 58 | } 59 | 60 | // TaskRequest is a v3 JSON object as described in: 61 | // http://v3-apidocs.cloudfoundry.org/version/3.0.0/index.html#create-a-task 62 | type TaskRequest struct { 63 | Command string `json:"command"` 64 | Name string `json:"name"` 65 | MemoryInMegabyte int `json:"memory_in_mb"` 66 | DiskInMegabyte int `json:"disk_in_mb"` 67 | DropletGUID string `json:"droplet_guid"` 68 | } 69 | 70 | func (c *Client) makeTaskListRequestWithParams(baseUrl string, query url.Values) ([]byte, error) { 71 | requestUrl := baseUrl + "?" + query.Encode() 72 | req := c.NewRequest("GET", requestUrl) 73 | resp, err := c.DoRequest(req) 74 | if err != nil { 75 | return nil, errors.Wrap(err, "Error requesting tasks") 76 | } 77 | defer resp.Body.Close() 78 | if resp.StatusCode != 200 { 79 | return nil, errors.Wrapf(err, "Error requesting tasks: status code not 200, it was %d", resp.StatusCode) 80 | } 81 | return ioutil.ReadAll(resp.Body) 82 | } 83 | 84 | func parseTaskListRespones(answer []byte) (TaskListResponse, error) { 85 | var response TaskListResponse 86 | err := json.Unmarshal(answer, &response) 87 | if err != nil { 88 | return response, errors.Wrap(err, "Error unmarshaling response %v") 89 | } 90 | return response, nil 91 | } 92 | 93 | func (c *Client) handleTasksApiCall(apiUrl string, query url.Values) ([]Task, error) { 94 | body, err := c.makeTaskListRequestWithParams(apiUrl, query) 95 | if err != nil { 96 | return nil, errors.Wrap(err, "Error requesting tasks") 97 | } 98 | response, err := parseTaskListRespones(body) 99 | if err != nil { 100 | return nil, errors.Wrap(err, "Error reading tasks") 101 | } 102 | return response.Tasks, nil 103 | } 104 | 105 | // ListTasks returns all tasks the user has access to. 106 | // See http://v3-apidocs.cloudfoundry.org/version/3.12.0/index.html#list-tasks 107 | func (c *Client) ListTasks() ([]Task, error) { 108 | return c.handleTasksApiCall("/v3/tasks", url.Values{}) 109 | } 110 | 111 | // ListTasksByQuery returns all tasks the user has access to, with query parameters. 112 | // See http://v3-apidocs.cloudfoundry.org/version/3.12.0/index.html#list-tasks 113 | func (c *Client) ListTasksByQuery(query url.Values) ([]Task, error) { 114 | return c.handleTasksApiCall("/v3/tasks", query) 115 | } 116 | 117 | // TasksByApp returns task structures which aligned to an app identified by the given guid. 118 | // See: http://v3-apidocs.cloudfoundry.org/version/3.12.0/index.html#list-tasks-for-an-app 119 | func (c *Client) TasksByApp(guid string) ([]Task, error) { 120 | return c.TasksByAppByQuery(guid, url.Values{}) 121 | } 122 | 123 | // TasksByAppByQuery returns task structures which aligned to an app identified by the given guid 124 | // and filtered by the given query parameters. 125 | // See: http://v3-apidocs.cloudfoundry.org/version/3.12.0/index.html#list-tasks-for-an-app 126 | func (c *Client) TasksByAppByQuery(guid string, query url.Values) ([]Task, error) { 127 | uri := fmt.Sprintf("/v3/apps/%s/tasks", guid) 128 | return c.handleTasksApiCall(uri, query) 129 | } 130 | 131 | func createReader(tr TaskRequest) (io.Reader, error) { 132 | rmap := make(map[string]string) 133 | rmap["command"] = tr.Command 134 | if tr.Name != "" { 135 | rmap["name"] = tr.Name 136 | } 137 | // setting droplet GUID causing issues 138 | if tr.MemoryInMegabyte != 0 { 139 | rmap["memory_in_mb"] = fmt.Sprintf("%d", tr.MemoryInMegabyte) 140 | } 141 | if tr.DiskInMegabyte != 0 { 142 | rmap["disk_in_mb"] = fmt.Sprintf("%d", tr.DiskInMegabyte) 143 | } 144 | 145 | bodyReader := bytes.NewBuffer(nil) 146 | enc := json.NewEncoder(bodyReader) 147 | if err := enc.Encode(rmap); err != nil { 148 | return nil, errors.Wrap(err, "Error during encoding task request") 149 | } 150 | return bodyReader, nil 151 | } 152 | 153 | // CreateTask creates a new task in CF system and returns its structure. 154 | func (c *Client) CreateTask(tr TaskRequest) (task Task, err error) { 155 | bodyReader, err := createReader(tr) 156 | if err != nil { 157 | return task, err 158 | } 159 | 160 | request := fmt.Sprintf("/v3/apps/%s/tasks", tr.DropletGUID) 161 | req := c.NewRequestWithBody("POST", request, bodyReader) 162 | 163 | resp, err := c.DoRequest(req) 164 | if err != nil { 165 | return task, errors.Wrap(err, "Error creating task") 166 | } 167 | defer resp.Body.Close() 168 | 169 | body, err := ioutil.ReadAll(resp.Body) 170 | if err != nil { 171 | return task, errors.Wrap(err, "Error reading task after creation") 172 | } 173 | 174 | err = json.Unmarshal(body, &task) 175 | if err != nil { 176 | return task, errors.Wrap(err, "Error unmarshaling task") 177 | } 178 | return task, err 179 | } 180 | 181 | // TaskByGuid returns a task structure by requesting it with the tasks GUID. 182 | func (c *Client) TaskByGuid(guid string) (task Task, err error) { 183 | request := fmt.Sprintf("/v3/tasks/%s", guid) 184 | req := c.NewRequest("GET", request) 185 | 186 | resp, err := c.DoRequest(req) 187 | if err != nil { 188 | return task, errors.Wrap(err, "Error requesting task") 189 | } 190 | defer resp.Body.Close() 191 | 192 | body, err := ioutil.ReadAll(resp.Body) 193 | if err != nil { 194 | return task, errors.Wrap(err, "Error reading task") 195 | } 196 | 197 | err = json.Unmarshal(body, &task) 198 | if err != nil { 199 | return task, errors.Wrap(err, "Error unmarshaling task") 200 | } 201 | return task, err 202 | } 203 | 204 | // TerminateTask cancels a task identified by its GUID. 205 | func (c *Client) TerminateTask(guid string) error { 206 | req := c.NewRequest("PUT", fmt.Sprintf("/v3/tasks/%s/cancel", guid)) 207 | resp, err := c.DoRequest(req) 208 | if err != nil { 209 | return errors.Wrap(err, "Error terminating task") 210 | } 211 | defer resp.Body.Close() 212 | 213 | if resp.StatusCode != 202 { 214 | return errors.Wrapf(err, "Failed terminating task, response status code %d", resp.StatusCode) 215 | } 216 | return nil 217 | } 218 | -------------------------------------------------------------------------------- /tasks_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestListTasks(t *testing.T) { 12 | Convey("List Tasks", t, func() { 13 | mocks := []MockRoute{ 14 | {"GET", "/v3/tasks", listTasksPayload, "", 200, ""}, 15 | } 16 | setupMultiple(mocks, t) 17 | defer teardown() 18 | c := &Config{ 19 | ApiAddress: server.URL, 20 | Token: "foobar", 21 | } 22 | client, err := NewClient(c) 23 | So(err, ShouldBeNil) 24 | 25 | task, err := client.ListTasks() 26 | So(err, ShouldBeNil) 27 | 28 | So(len(task), ShouldEqual, 2) 29 | 30 | So(task[0].GUID, ShouldEqual, "xxxxxxxx-e99c-4d60-xxx-e066eb45f8a7") 31 | So(task[0].State, ShouldEqual, "FAILED") 32 | So(task[0].SequenceID, ShouldEqual, 1) 33 | So(task[0].MemoryInMb, ShouldEqual, 1024) 34 | So(task[0].DiskInMb, ShouldEqual, 1024) 35 | So(task[0].CreatedAt.String(), ShouldEqual, time.Date(2016, 12, 22, 13, 24, 20, 0, time.FixedZone("UTC", 0)).String()) 36 | 37 | So(task[1].GUID, ShouldEqual, "xxxxxxxx-5a25-4110-xxx-b309dc5cb0aa") 38 | So(task[1].State, ShouldEqual, "FAILED") 39 | So(task[1].SequenceID, ShouldEqual, 2) 40 | So(task[1].MemoryInMb, ShouldEqual, 1024) 41 | So(task[1].DiskInMb, ShouldEqual, 1024) 42 | So(task[1].CreatedAt.String(), ShouldEqual, time.Date(2016, 12, 22, 13, 24, 36, 0, time.FixedZone("UTC", 0)).String()) 43 | }) 44 | } 45 | func TestListTasksByQuery(t *testing.T) { 46 | Convey("List Tasks", t, func() { 47 | mocks := []MockRoute{ 48 | {"GET", "/v3/tasks", listTasksPayload, "", 200, "names=my-fancy-name&page=1"}, 49 | } 50 | setupMultiple(mocks, t) 51 | defer teardown() 52 | c := &Config{ 53 | ApiAddress: server.URL, 54 | Token: "foobar", 55 | } 56 | client, err := NewClient(c) 57 | So(err, ShouldBeNil) 58 | 59 | query := url.Values{} 60 | query.Add("names", "my-fancy-name") 61 | query.Add("page", "1") 62 | task, err := client.ListTasksByQuery(query) 63 | So(err, ShouldBeNil) 64 | 65 | So(len(task), ShouldEqual, 2) 66 | 67 | So(task[0].GUID, ShouldEqual, "xxxxxxxx-e99c-4d60-xxx-e066eb45f8a7") 68 | So(task[0].State, ShouldEqual, "FAILED") 69 | So(task[0].SequenceID, ShouldEqual, 1) 70 | So(task[0].MemoryInMb, ShouldEqual, 1024) 71 | So(task[0].DiskInMb, ShouldEqual, 1024) 72 | So(task[0].CreatedAt.String(), ShouldEqual, time.Date(2016, 12, 22, 13, 24, 20, 0, time.FixedZone("UTC", 0)).String()) 73 | 74 | So(task[1].GUID, ShouldEqual, "xxxxxxxx-5a25-4110-xxx-b309dc5cb0aa") 75 | So(task[1].State, ShouldEqual, "FAILED") 76 | So(task[1].SequenceID, ShouldEqual, 2) 77 | So(task[1].MemoryInMb, ShouldEqual, 1024) 78 | So(task[1].DiskInMb, ShouldEqual, 1024) 79 | So(task[1].CreatedAt.String(), ShouldEqual, time.Date(2016, 12, 22, 13, 24, 36, 0, time.FixedZone("UTC", 0)).String()) 80 | }) 81 | } 82 | 83 | func TestCreateTask(t *testing.T) { 84 | Convey("Create Task", t, func() { 85 | mocks := []MockRoute{ 86 | {"POST", "/v3/apps/740ebd2b-162b-469a-bd72-3edb96fabd9a/tasks", createTaskPayload, "", 201, ""}, 87 | } 88 | setupMultiple(mocks, t) 89 | defer teardown() 90 | c := &Config{ 91 | ApiAddress: server.URL, 92 | Token: "foobar", 93 | } 94 | client, err := NewClient(c) 95 | So(err, ShouldBeNil) 96 | 97 | tr := TaskRequest{ 98 | Command: "rake db:migrate", 99 | Name: "migrate", 100 | MemoryInMegabyte: 512, 101 | DiskInMegabyte: 1024, 102 | DropletGUID: "740ebd2b-162b-469a-bd72-3edb96fabd9a", 103 | } 104 | task, err := client.CreateTask(tr) 105 | So(err, ShouldBeNil) 106 | 107 | So(task.Command, ShouldEqual, "rake db:migrate") 108 | So(task.Name, ShouldEqual, "migrate") 109 | So(task.DiskInMb, ShouldEqual, 1024) 110 | So(task.MemoryInMb, ShouldEqual, 512) 111 | So(task.DropletGUID, ShouldEqual, "740ebd2b-162b-469a-bd72-3edb96fabd9a") 112 | }) 113 | } 114 | 115 | func TestTerminateTask(t *testing.T) { 116 | Convey("Terminate Task", t, func() { 117 | mocks := []MockRoute{ 118 | {"PUT", "/v3/tasks/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/cancel", "", "", 202, ""}, 119 | } 120 | setupMultiple(mocks, t) 121 | defer teardown() 122 | c := &Config{ 123 | ApiAddress: server.URL, 124 | Token: "foobar", 125 | } 126 | client, err := NewClient(c) 127 | So(err, ShouldBeNil) 128 | 129 | errTerm := client.TerminateTask("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") 130 | So(errTerm, ShouldBeNil) 131 | }) 132 | } 133 | 134 | func TestGetTask(t *testing.T) { 135 | Convey("Create Task", t, func() { 136 | mocks := []MockRoute{ 137 | {"GET", "/v3/tasks/740ebd2b-162b-469a-bd72-3edb96fabd9a", createTaskPayload, "", 200, ""}, 138 | } 139 | setupMultiple(mocks, t) 140 | defer teardown() 141 | c := &Config{ 142 | ApiAddress: server.URL, 143 | Token: "foobar", 144 | } 145 | client, err := NewClient(c) 146 | So(err, ShouldBeNil) 147 | 148 | task, err := client.TaskByGuid("740ebd2b-162b-469a-bd72-3edb96fabd9a") 149 | So(err, ShouldBeNil) 150 | 151 | So(task.Command, ShouldEqual, "rake db:migrate") 152 | So(task.Name, ShouldEqual, "migrate") 153 | So(task.DiskInMb, ShouldEqual, 1024) 154 | So(task.MemoryInMb, ShouldEqual, 512) 155 | So(task.DropletGUID, ShouldEqual, "740ebd2b-162b-469a-bd72-3edb96fabd9a") 156 | }) 157 | } 158 | 159 | func TestTasksByApp(t *testing.T) { 160 | Convey("List Tasks by App", t, func() { 161 | mocks := []MockRoute{ 162 | {"GET", "/v3/apps/ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5/tasks", listTasksPayload, "", 200, ""}, 163 | } 164 | setupMultiple(mocks, t) 165 | defer teardown() 166 | c := &Config{ 167 | ApiAddress: server.URL, 168 | Token: "foobar", 169 | } 170 | client, err := NewClient(c) 171 | So(err, ShouldBeNil) 172 | 173 | task, err := client.TasksByApp("ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5") 174 | So(err, ShouldBeNil) 175 | 176 | So(len(task), ShouldEqual, 2) 177 | 178 | So(task[0].GUID, ShouldEqual, "xxxxxxxx-e99c-4d60-xxx-e066eb45f8a7") 179 | So(task[0].State, ShouldEqual, "FAILED") 180 | So(task[0].SequenceID, ShouldEqual, 1) 181 | So(task[0].MemoryInMb, ShouldEqual, 1024) 182 | So(task[0].DiskInMb, ShouldEqual, 1024) 183 | So(task[0].CreatedAt.String(), ShouldEqual, time.Date(2016, 12, 22, 13, 24, 20, 0, time.FixedZone("UTC", 0)).String()) 184 | 185 | So(task[1].GUID, ShouldEqual, "xxxxxxxx-5a25-4110-xxx-b309dc5cb0aa") 186 | So(task[1].State, ShouldEqual, "FAILED") 187 | So(task[1].SequenceID, ShouldEqual, 2) 188 | So(task[1].MemoryInMb, ShouldEqual, 1024) 189 | So(task[1].DiskInMb, ShouldEqual, 1024) 190 | So(task[1].CreatedAt.String(), ShouldEqual, time.Date(2016, 12, 22, 13, 24, 36, 0, time.FixedZone("UTC", 0)).String()) 191 | }) 192 | } 193 | 194 | func TestTasksByAppByQuery(t *testing.T) { 195 | Convey("List Tasks by App", t, func() { 196 | mocks := []MockRoute{ 197 | {"GET", "/v3/apps/ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5/tasks", listTasksPayload, "", 200, "names=my-fancy-name&page=1"}, 198 | } 199 | setupMultiple(mocks, t) 200 | defer teardown() 201 | c := &Config{ 202 | ApiAddress: server.URL, 203 | Token: "foobar", 204 | } 205 | client, err := NewClient(c) 206 | So(err, ShouldBeNil) 207 | 208 | query := url.Values{} 209 | query.Add("names", "my-fancy-name") 210 | query.Add("page", "1") 211 | task, err := client.TasksByAppByQuery("ccc25a0f-c8f4-4b39-9f1b-de9f328d0ee5", query) 212 | So(err, ShouldBeNil) 213 | 214 | So(len(task), ShouldEqual, 2) 215 | 216 | So(task[0].GUID, ShouldEqual, "xxxxxxxx-e99c-4d60-xxx-e066eb45f8a7") 217 | So(task[0].State, ShouldEqual, "FAILED") 218 | So(task[0].SequenceID, ShouldEqual, 1) 219 | So(task[0].MemoryInMb, ShouldEqual, 1024) 220 | So(task[0].DiskInMb, ShouldEqual, 1024) 221 | So(task[0].CreatedAt.String(), ShouldEqual, time.Date(2016, 12, 22, 13, 24, 20, 0, time.FixedZone("UTC", 0)).String()) 222 | 223 | So(task[1].GUID, ShouldEqual, "xxxxxxxx-5a25-4110-xxx-b309dc5cb0aa") 224 | So(task[1].State, ShouldEqual, "FAILED") 225 | So(task[1].SequenceID, ShouldEqual, 2) 226 | So(task[1].MemoryInMb, ShouldEqual, 1024) 227 | So(task[1].DiskInMb, ShouldEqual, 1024) 228 | So(task[1].CreatedAt.String(), ShouldEqual, time.Date(2016, 12, 22, 13, 24, 36, 0, time.FixedZone("UTC", 0)).String()) 229 | }) 230 | } 231 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/pkg/errors" 12 | "golang.org/x/net/context" 13 | "golang.org/x/oauth2" 14 | "golang.org/x/oauth2/clientcredentials" 15 | ) 16 | 17 | //Client used to communicate with Cloud Foundry 18 | type Client struct { 19 | config Config 20 | Endpoint Endpoint 21 | } 22 | 23 | type Endpoint struct { 24 | DopplerEndpoint string `json:"doppler_logging_endpoint"` 25 | LoggingEndpoint string `json:"logging_endpoint"` 26 | AuthEndpoint string `json:"authorization_endpoint"` 27 | TokenEndpoint string `json:"token_endpoint"` 28 | } 29 | 30 | //Config is used to configure the creation of a client 31 | type Config struct { 32 | ApiAddress string `json:"api_url"` 33 | Username string `json:"user"` 34 | Password string `json:"password"` 35 | ClientID string `json:"client_id"` 36 | ClientSecret string `json:"client_secret"` 37 | SkipSslValidation bool `json:"skip_ssl_validation"` 38 | HttpClient *http.Client 39 | Token string `json:"auth_token"` 40 | TokenSource oauth2.TokenSource 41 | UserAgent string `json:"user_agent"` 42 | } 43 | 44 | // request is used to help build up a request 45 | type request struct { 46 | method string 47 | url string 48 | params url.Values 49 | body io.Reader 50 | obj interface{} 51 | } 52 | 53 | //DefaultConfig configuration for client 54 | //Keep LoginAdress for backward compatibility 55 | //Need to be remove in close future 56 | func DefaultConfig() *Config { 57 | return &Config{ 58 | ApiAddress: "http://api.bosh-lite.com", 59 | Username: "admin", 60 | Password: "admin", 61 | Token: "", 62 | SkipSslValidation: false, 63 | HttpClient: http.DefaultClient, 64 | UserAgent: "Go-CF-client/1.1", 65 | } 66 | } 67 | 68 | func DefaultEndpoint() *Endpoint { 69 | return &Endpoint{ 70 | DopplerEndpoint: "wss://doppler.10.244.0.34.xip.io:443", 71 | LoggingEndpoint: "wss://loggregator.10.244.0.34.xip.io:443", 72 | TokenEndpoint: "https://uaa.10.244.0.34.xip.io", 73 | AuthEndpoint: "https://login.10.244.0.34.xip.io", 74 | } 75 | } 76 | 77 | // NewClient returns a new client 78 | func NewClient(config *Config) (client *Client, err error) { 79 | // bootstrap the config 80 | defConfig := DefaultConfig() 81 | 82 | if len(config.ApiAddress) == 0 { 83 | config.ApiAddress = defConfig.ApiAddress 84 | } 85 | 86 | if len(config.Username) == 0 { 87 | config.Username = defConfig.Username 88 | } 89 | 90 | if len(config.Password) == 0 { 91 | config.Password = defConfig.Password 92 | } 93 | 94 | if len(config.Token) == 0 { 95 | config.Token = defConfig.Token 96 | } 97 | 98 | if len(config.UserAgent) == 0 { 99 | config.UserAgent = defConfig.UserAgent 100 | } 101 | 102 | if config.HttpClient == nil { 103 | config.HttpClient = defConfig.HttpClient 104 | } 105 | 106 | if config.HttpClient.Transport == nil { 107 | config.HttpClient.Transport = http.DefaultTransport 108 | } 109 | 110 | tp := config.HttpClient.Transport.(*http.Transport) 111 | if tp.TLSClientConfig == nil { 112 | tp.TLSClientConfig = &tls.Config{} 113 | } 114 | 115 | // we want to keep the Timeout value from config.HttpClient 116 | timeout := config.HttpClient.Timeout 117 | 118 | ctx := context.Background() 119 | 120 | tp.TLSClientConfig.InsecureSkipVerify = config.SkipSslValidation 121 | ctx = context.WithValue(ctx, oauth2.HTTPClient, config.HttpClient) 122 | 123 | endpoint, err := getInfo(config.ApiAddress, oauth2.NewClient(ctx, nil)) 124 | 125 | if err != nil { 126 | return nil, errors.Wrap(err, "Could not get api /v2/info") 127 | } 128 | 129 | switch { 130 | case config.Token != "": 131 | config = getUserTokenAuth(config, endpoint, ctx) 132 | case config.ClientID != "": 133 | config = getClientAuth(config, endpoint, ctx) 134 | default: 135 | config, err = getUserAuth(config, endpoint, ctx) 136 | if err != nil { 137 | return nil, err 138 | } 139 | } 140 | // make sure original Timeout value will be used 141 | if config.HttpClient.Timeout != timeout { 142 | config.HttpClient.Timeout = timeout 143 | } 144 | client = &Client{ 145 | config: *config, 146 | Endpoint: *endpoint, 147 | } 148 | return client, nil 149 | } 150 | 151 | func getUserAuth(config *Config, endpoint *Endpoint, ctx context.Context) (*Config, error) { 152 | authConfig := &oauth2.Config{ 153 | ClientID: "cf", 154 | Scopes: []string{""}, 155 | Endpoint: oauth2.Endpoint{ 156 | AuthURL: endpoint.AuthEndpoint + "/oauth/auth", 157 | TokenURL: endpoint.TokenEndpoint + "/oauth/token", 158 | }, 159 | } 160 | 161 | token, err := authConfig.PasswordCredentialsToken(ctx, config.Username, config.Password) 162 | 163 | if err != nil { 164 | return nil, errors.Wrap(err, "Error getting token") 165 | } 166 | 167 | config.TokenSource = authConfig.TokenSource(ctx, token) 168 | config.HttpClient = oauth2.NewClient(ctx, config.TokenSource) 169 | 170 | return config, err 171 | } 172 | 173 | func getClientAuth(config *Config, endpoint *Endpoint, ctx context.Context) *Config { 174 | authConfig := &clientcredentials.Config{ 175 | ClientID: config.ClientID, 176 | ClientSecret: config.ClientSecret, 177 | TokenURL: endpoint.TokenEndpoint + "/oauth/token", 178 | } 179 | 180 | config.TokenSource = authConfig.TokenSource(ctx) 181 | config.HttpClient = authConfig.Client(ctx) 182 | return config 183 | } 184 | 185 | // Initialize client credentials from existing bearer token 186 | func getUserTokenAuth(config *Config, endpoint *Endpoint, ctx context.Context) *Config { 187 | authConfig := &oauth2.Config{ 188 | ClientID: "cf", 189 | Scopes: []string{""}, 190 | Endpoint: oauth2.Endpoint{ 191 | AuthURL: endpoint.AuthEndpoint + "/oauth/auth", 192 | TokenURL: endpoint.TokenEndpoint + "/oauth/token", 193 | }, 194 | } 195 | 196 | // Token is expected to have no "bearer" prefix 197 | token := &oauth2.Token{ 198 | AccessToken: config.Token, 199 | TokenType: "Bearer"} 200 | 201 | config.TokenSource = authConfig.TokenSource(ctx, token) 202 | config.HttpClient = oauth2.NewClient(ctx, config.TokenSource) 203 | 204 | return config 205 | } 206 | 207 | func getInfo(api string, httpClient *http.Client) (*Endpoint, error) { 208 | var endpoint Endpoint 209 | 210 | if api == "" { 211 | return DefaultEndpoint(), nil 212 | } 213 | 214 | resp, err := httpClient.Get(api + "/v2/info") 215 | if err != nil { 216 | return nil, err 217 | } 218 | defer resp.Body.Close() 219 | 220 | err = decodeBody(resp, &endpoint) 221 | if err != nil { 222 | return nil, err 223 | } 224 | 225 | return &endpoint, err 226 | } 227 | 228 | // NewRequest is used to create a new request 229 | func (c *Client) NewRequest(method, path string) *request { 230 | r := &request{ 231 | method: method, 232 | url: c.config.ApiAddress + path, 233 | params: make(map[string][]string), 234 | } 235 | return r 236 | } 237 | 238 | // NewRequestWithBody is used to create a new request with 239 | // arbigtrary body io.Reader. 240 | func (c *Client) NewRequestWithBody(method, path string, body io.Reader) *request { 241 | r := c.NewRequest(method, path) 242 | 243 | // Set request body 244 | r.body = body 245 | 246 | return r 247 | } 248 | 249 | // DoRequest runs a request with our client 250 | func (c *Client) DoRequest(r *request) (*http.Response, error) { 251 | req, err := r.toHTTP() 252 | if err != nil { 253 | return nil, err 254 | } 255 | req.Header.Set("User-Agent", c.config.UserAgent) 256 | if r.body != nil { 257 | req.Header.Set("Content-type", "application/json") 258 | } 259 | 260 | resp, err := c.config.HttpClient.Do(req) 261 | 262 | if resp.StatusCode >= http.StatusBadRequest { 263 | cfErr := &CloudFoundryErrors{} 264 | if err := decodeBody(resp, cfErr); err != nil { 265 | return resp, errors.Wrap(err, "Unable to decode body") 266 | } 267 | return nil, cfErr 268 | } 269 | 270 | return resp, err 271 | } 272 | 273 | // toHTTP converts the request to an HTTP request 274 | func (r *request) toHTTP() (*http.Request, error) { 275 | 276 | // Check if we should encode the body 277 | if r.body == nil && r.obj != nil { 278 | b, err := encodeBody(r.obj) 279 | if err != nil { 280 | return nil, err 281 | } 282 | r.body = b 283 | } 284 | 285 | // Create the HTTP request 286 | return http.NewRequest(r.method, r.url, r.body) 287 | } 288 | 289 | // decodeBody is used to JSON decode a body 290 | func decodeBody(resp *http.Response, out interface{}) error { 291 | defer resp.Body.Close() 292 | dec := json.NewDecoder(resp.Body) 293 | return dec.Decode(out) 294 | } 295 | 296 | // encodeBody is used to encode a request body 297 | func encodeBody(obj interface{}) (io.Reader, error) { 298 | buf := bytes.NewBuffer(nil) 299 | enc := json.NewEncoder(buf) 300 | if err := enc.Encode(obj); err != nil { 301 | return nil, err 302 | } 303 | return buf, nil 304 | } 305 | 306 | func (c *Client) GetToken() (string, error) { 307 | token, err := c.config.TokenSource.Token() 308 | if err != nil { 309 | return "", errors.Wrap(err, "Error getting bearer token") 310 | } 311 | return "bearer " + token.AccessToken, nil 312 | } 313 | -------------------------------------------------------------------------------- /spaces_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListSpaces(t *testing.T) { 10 | Convey("List Space", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/spaces", listSpacesPayload, "", 200, ""}, 13 | {"GET", "/v2/spacesPage2", listSpacesPayloadPage2, "", 200, ""}, 14 | } 15 | setupMultiple(mocks, t) 16 | defer teardown() 17 | c := &Config{ 18 | ApiAddress: server.URL, 19 | Token: "foobar", 20 | } 21 | client, err := NewClient(c) 22 | So(err, ShouldBeNil) 23 | 24 | spaces, err := client.ListSpaces() 25 | So(err, ShouldBeNil) 26 | 27 | So(len(spaces), ShouldEqual, 4) 28 | So(spaces[0].Guid, ShouldEqual, "8efd7c5c-d83c-4786-b399-b7bd548839e1") 29 | So(spaces[0].Name, ShouldEqual, "dev") 30 | So(spaces[0].OrganizationGuid, ShouldEqual, "a537761f-9d93-4b30-af17-3d73dbca181b") 31 | So(spaces[1].Guid, ShouldEqual, "657b5923-7de0-486a-9928-b4d78ee24931") 32 | So(spaces[1].Name, ShouldEqual, "demo") 33 | So(spaces[1].OrganizationGuid, ShouldEqual, "da0dba14-6064-4f7a-b15a-ff9e677e49b2") 34 | So(spaces[2].Guid, ShouldEqual, "9ffd7c5c-d83c-4786-b399-b7bd54883977") 35 | So(spaces[2].Name, ShouldEqual, "test") 36 | So(spaces[2].OrganizationGuid, ShouldEqual, "a537761f-9d93-4b30-af17-3d73dbca181b") 37 | So(spaces[3].Guid, ShouldEqual, "329b5923-7de0-486a-9928-b4d78ee24982") 38 | So(spaces[3].Name, ShouldEqual, "prod") 39 | So(spaces[3].OrganizationGuid, ShouldEqual, "da0dba14-6064-4f7a-b15a-ff9e677e49b2") 40 | }) 41 | } 42 | 43 | func TestCreateSpace(t *testing.T) { 44 | Convey("Create Space", t, func() { 45 | setup(MockRoute{"POST", "/v2/spaces", spacePayload, "", 201, ""}, t) 46 | defer teardown() 47 | c := &Config{ 48 | ApiAddress: server.URL, 49 | Token: "foobar", 50 | } 51 | client, err := NewClient(c) 52 | So(err, ShouldBeNil) 53 | 54 | spaceRequest := SpaceRequest{Name: "test-space", OrganizationGuid: "da0dba14-6064-4f7a-b15a-ff9e677e49b2"} 55 | 56 | space, err := client.CreateSpace(spaceRequest) 57 | So(err, ShouldBeNil) 58 | 59 | So(space.Name, ShouldEqual, "test-space") 60 | So(space.OrganizationGuid, ShouldEqual, "da0dba14-6064-4f7a-b15a-ff9e677e49b2") 61 | }) 62 | } 63 | 64 | func TestSpaceOrg(t *testing.T) { 65 | Convey("Find space org", t, func() { 66 | setup(MockRoute{"GET", "/v2/org/foobar", orgPayload, "", 200, ""}, t) 67 | defer teardown() 68 | c := &Config{ 69 | ApiAddress: server.URL, 70 | Token: "foobar", 71 | } 72 | client, err := NewClient(c) 73 | So(err, ShouldBeNil) 74 | 75 | space := &Space{ 76 | Guid: "123", 77 | Name: "test space", 78 | OrgURL: "/v2/org/foobar", 79 | c: client, 80 | } 81 | org, err := space.Org() 82 | So(err, ShouldBeNil) 83 | 84 | So(org.Name, ShouldEqual, "test-org") 85 | So(org.Guid, ShouldEqual, "da0dba14-6064-4f7a-b15a-ff9e677e49b2") 86 | }) 87 | } 88 | 89 | func TestSpaceQuota(t *testing.T) { 90 | Convey("Get space quota", t, func() { 91 | setup(MockRoute{"GET", "/v2/space_quota_definitions/9ffd7c5c-d83c-4786-b399-b7bd54883977", spaceQuotaPayload, "", 200, ""}, t) 92 | defer teardown() 93 | c := &Config{ 94 | ApiAddress: server.URL, 95 | Token: "foobar", 96 | } 97 | client, err := NewClient(c) 98 | So(err, ShouldBeNil) 99 | 100 | space := &Space{ 101 | QuotaDefinitionGuid: "9ffd7c5c-d83c-4786-b399-b7bd54883977", 102 | c: client, 103 | } 104 | 105 | spaceQuota, err := space.Quota() 106 | So(err, ShouldBeNil) 107 | 108 | So(spaceQuota.Guid, ShouldEqual, "9ffd7c5c-d83c-4786-b399-b7bd54883977") 109 | So(spaceQuota.Name, ShouldEqual, "test-2") 110 | So(spaceQuota.NonBasicServicesAllowed, ShouldEqual, false) 111 | So(spaceQuota.TotalServices, ShouldEqual, 10) 112 | So(spaceQuota.TotalRoutes, ShouldEqual, 20) 113 | So(spaceQuota.MemoryLimit, ShouldEqual, 30) 114 | So(spaceQuota.InstanceMemoryLimit, ShouldEqual, 40) 115 | So(spaceQuota.AppInstanceLimit, ShouldEqual, 50) 116 | So(spaceQuota.AppTaskLimit, ShouldEqual, 60) 117 | So(spaceQuota.TotalServiceKeys, ShouldEqual, 70) 118 | So(spaceQuota.TotalReservedRoutePorts, ShouldEqual, 80) 119 | }) 120 | } 121 | 122 | func TestSpaceSummary(t *testing.T) { 123 | Convey("Get space summary", t, func() { 124 | setup(MockRoute{"GET", "/v2/spaces/494d8b64-8181-4183-a6d3-6279db8fec6e/summary", spaceSummaryPayload, "", 200, ""}, t) 125 | defer teardown() 126 | c := &Config{ 127 | ApiAddress: server.URL, 128 | Token: "foobar", 129 | } 130 | client, err := NewClient(c) 131 | So(err, ShouldBeNil) 132 | 133 | space := &Space{ 134 | Guid: "494d8b64-8181-4183-a6d3-6279db8fec6e", 135 | c: client, 136 | } 137 | 138 | summary, err := space.Summary() 139 | So(err, ShouldBeNil) 140 | 141 | So(summary.Guid, ShouldEqual, "494d8b64-8181-4183-a6d3-6279db8fec6e") 142 | So(summary.Name, ShouldEqual, "test") 143 | 144 | So(len(summary.Apps), ShouldEqual, 1) 145 | So(summary.Apps[0].Guid, ShouldEqual, "b5f0d1bd-a3a9-40a4-af1a-312ad26e5379") 146 | So(summary.Apps[0].Name, ShouldEqual, "test-app") 147 | So(summary.Apps[0].ServiceCount, ShouldEqual, 1) 148 | So(summary.Apps[0].RunningInstances, ShouldEqual, 1) 149 | So(summary.Apps[0].Memory, ShouldEqual, 256) 150 | So(summary.Apps[0].Instances, ShouldEqual, 1) 151 | So(summary.Apps[0].DiskQuota, ShouldEqual, 512) 152 | So(summary.Apps[0].State, ShouldEqual, "STARTED") 153 | So(summary.Apps[0].Diego, ShouldEqual, true) 154 | 155 | So(len(summary.Services), ShouldEqual, 1) 156 | So(summary.Services[0].Guid, ShouldEqual, "3c5c758c-6b76-46f6-89d5-677909bfc975") 157 | So(summary.Services[0].Name, ShouldEqual, "test-service") 158 | So(summary.Services[0].BoundAppCount, ShouldEqual, 1) 159 | }) 160 | } 161 | 162 | func TestSpaceRoles(t *testing.T) { 163 | Convey("Get space roles", t, func() { 164 | setup(MockRoute{"GET", "/v2/spaces/494d8b64-8181-4183-a6d3-6279db8fec6e/user_roles", spaceRolesPayload, "", 200, ""}, t) 165 | defer teardown() 166 | c := &Config{ 167 | ApiAddress: server.URL, 168 | Token: "foobar", 169 | } 170 | client, err := NewClient(c) 171 | So(err, ShouldBeNil) 172 | 173 | space := &Space{ 174 | Guid: "494d8b64-8181-4183-a6d3-6279db8fec6e", 175 | c: client, 176 | } 177 | 178 | roles, err := space.Roles() 179 | So(err, ShouldBeNil) 180 | 181 | So(len(roles), ShouldEqual, 1) 182 | So(roles[0].Guid, ShouldEqual, "uaa-id-363") 183 | So(roles[0].Admin, ShouldEqual, false) 184 | So(roles[0].Active, ShouldEqual, false) 185 | So(roles[0].DefaultSpaceGuid, ShouldEqual, "") 186 | So(roles[0].Username, ShouldEqual, "everything@example.com") 187 | So(roles[0].SpaceRoles, ShouldResemble, []string{"space_developer", "space_manager", "space_auditor"}) 188 | So(roles[0].SpacesUrl, ShouldEqual, "/v2/users/uaa-id-363/spaces") 189 | So(roles[0].OrganizationsUrl, ShouldEqual, "/v2/users/uaa-id-363/organizations") 190 | So(roles[0].ManagedOrganizationsUrl, ShouldEqual, "/v2/users/uaa-id-363/managed_organizations") 191 | So(roles[0].BillingManagedOrganizationsUrl, ShouldEqual, "/v2/users/uaa-id-363/billing_managed_organizations") 192 | So(roles[0].AuditedOrganizationsUrl, ShouldEqual, "/v2/users/uaa-id-363/audited_organizations") 193 | So(roles[0].ManagedSpacesUrl, ShouldEqual, "/v2/users/uaa-id-363/managed_spaces") 194 | So(roles[0].AuditedSpacesUrl, ShouldEqual, "/v2/users/uaa-id-363/audited_spaces") 195 | }) 196 | } 197 | 198 | func TestAssociateSpaceAuditorByUsername(t *testing.T) { 199 | Convey("Associate auditor by username", t, func() { 200 | setup(MockRoute{"PUT", "/v2/spaces/bc7b4caf-f4b8-4d85-b126-0729b9351e56/auditors", associateSpaceAuditorPayload, "", 201, ""}, t) 201 | defer teardown() 202 | c := &Config{ 203 | ApiAddress: server.URL, 204 | Token: "foobar", 205 | } 206 | client, err := NewClient(c) 207 | So(err, ShouldBeNil) 208 | 209 | space := &Space{ 210 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 211 | c: client, 212 | } 213 | 214 | newSpace, err := space.AssociateAuditorByUsername("user-name") 215 | So(err, ShouldBeNil) 216 | So(newSpace.Guid, ShouldEqual, "bc7b4caf-f4b8-4d85-b126-0729b9351e56") 217 | }) 218 | } 219 | 220 | func TestAssociateSpaceDeveloperByUsername(t *testing.T) { 221 | Convey("Associate developer by username", t, func() { 222 | setup(MockRoute{"PUT", "/v2/spaces/bc7b4caf-f4b8-4d85-b126-0729b9351e56/developers", associateSpaceDeveloperPayload, "", 201, ""}, t) 223 | defer teardown() 224 | c := &Config{ 225 | ApiAddress: server.URL, 226 | Token: "foobar", 227 | } 228 | client, err := NewClient(c) 229 | So(err, ShouldBeNil) 230 | 231 | space := &Space{ 232 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 233 | c: client, 234 | } 235 | 236 | newSpace, err := space.AssociateDeveloperByUsername("user-name") 237 | So(err, ShouldBeNil) 238 | So(newSpace.Guid, ShouldEqual, "bc7b4caf-f4b8-4d85-b126-0729b9351e56") 239 | }) 240 | } 241 | 242 | func TestRemoveSpaceDeveloperByUsername(t *testing.T) { 243 | Convey("Remove developer by username", t, func() { 244 | setup(MockRoute{"DELETE", "/v2/spaces/bc7b4caf-f4b8-4d85-b126-0729b9351e56/developers", "", "", 204, ""}, t) 245 | defer teardown() 246 | c := &Config{ 247 | ApiAddress: server.URL, 248 | Token: "foobar", 249 | } 250 | client, err := NewClient(c) 251 | So(err, ShouldBeNil) 252 | 253 | space := &Space{ 254 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 255 | c: client, 256 | } 257 | 258 | err = space.RemoveDeveloperByUsername("user-name") 259 | So(err, ShouldBeNil) 260 | }) 261 | } 262 | func TestRemoveSpaceAuditorByUsername(t *testing.T) { 263 | Convey("Remove auditor by username", t, func() { 264 | setup(MockRoute{"DELETE", "/v2/spaces/bc7b4caf-f4b8-4d85-b126-0729b9351e56/auditors", "", "", 204, ""}, t) 265 | defer teardown() 266 | c := &Config{ 267 | ApiAddress: server.URL, 268 | Token: "foobar", 269 | } 270 | client, err := NewClient(c) 271 | So(err, ShouldBeNil) 272 | 273 | space := &Space{ 274 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 275 | c: client, 276 | } 277 | 278 | err = space.RemoveAuditorByUsername("user-name") 279 | So(err, ShouldBeNil) 280 | }) 281 | } 282 | -------------------------------------------------------------------------------- /apps_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestListApps(t *testing.T) { 11 | Convey("List Apps", t, func() { 12 | mocks := []MockRoute{ 13 | {"GET", "/v2/apps", listAppsPayload, "Test-golang", 200, "inline-relations-depth=2"}, 14 | {"GET", "/v2/appsPage2", listAppsPayloadPage2, "Test-golang", 200, ""}, 15 | } 16 | setupMultiple(mocks, t) 17 | defer teardown() 18 | c := &Config{ 19 | ApiAddress: server.URL, 20 | Token: "foobar", 21 | UserAgent: "Test-golang", 22 | } 23 | client, err := NewClient(c) 24 | So(err, ShouldBeNil) 25 | 26 | apps, err := client.ListApps() 27 | So(err, ShouldBeNil) 28 | 29 | So(len(apps), ShouldEqual, 2) 30 | So(apps[0].Guid, ShouldEqual, "af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c") 31 | So(apps[0].CreatedAt, ShouldEqual, "2014-10-10T21:03:13+00:00") 32 | So(apps[0].UpdatedAt, ShouldEqual, "2014-11-10T14:07:31+00:00") 33 | So(apps[0].Name, ShouldEqual, "app-test") 34 | So(apps[0].Memory, ShouldEqual, 256) 35 | So(apps[0].Instances, ShouldEqual, 1) 36 | So(apps[0].DiskQuota, ShouldEqual, 1024) 37 | So(apps[0].SpaceGuid, ShouldEqual, "8efd7c5c-d83c-4786-b399-b7bd548839e1") 38 | So(apps[0].StackGuid, ShouldEqual, "2c531037-68a2-4e2c-a9e0-71f9d0abf0d4") 39 | So(apps[0].State, ShouldEqual, "STARTED") 40 | So(apps[0].Command, ShouldEqual, "") 41 | So(apps[0].Buildpack, ShouldEqual, "https://github.com/cloudfoundry/buildpack-go.git") 42 | So(apps[0].DetectedBuildpack, ShouldEqual, "") 43 | So(apps[0].DetectedBuildpackGuid, ShouldEqual, "0d22f6a1-76c5-417f-ac6c-d9d21463ecbc") 44 | So(apps[0].HealthCheckHttpEndpoint, ShouldEqual, "") 45 | So(apps[0].HealthCheckType, ShouldEqual, "port") 46 | So(apps[0].HealthCheckTimeout, ShouldEqual, 0) 47 | So(apps[0].Diego, ShouldEqual, true) 48 | So(apps[0].EnableSSH, ShouldEqual, true) 49 | So(apps[0].DetectedStartCommand, ShouldEqual, "app-launching-service-broker") 50 | So(apps[0].DockerImage, ShouldEqual, "") 51 | So(apps[0].DockerCredentials["redacted_message"], ShouldEqual, "[PRIVATE DATA HIDDEN]") 52 | So(apps[0].Environment["FOOBAR"], ShouldEqual, "QUX") 53 | So(apps[0].StagingFailedReason, ShouldEqual, "") 54 | So(apps[0].StagingFailedDescription, ShouldEqual, "") 55 | So(apps[0].PackageState, ShouldEqual, "PENDING") 56 | So(len(apps[0].Ports), ShouldEqual, 1) 57 | So(apps[0].Ports[0], ShouldEqual, 8080) 58 | 59 | So(apps[1].Guid, ShouldEqual, "f9ad202b-76dd-44ec-b7c2-fd2417a561e8") 60 | So(apps[1].Name, ShouldEqual, "app-test2") 61 | }) 62 | } 63 | 64 | func TestAppByGuid(t *testing.T) { 65 | Convey("App By GUID", t, func() { 66 | setup(MockRoute{"GET", "/v2/apps/9902530c-c634-4864-a189-71d763cb12e2", appPayload, "", 200, "inline-relations-depth=2"}, t) 67 | defer teardown() 68 | c := &Config{ 69 | ApiAddress: server.URL, 70 | Token: "foobar", 71 | } 72 | client, err := NewClient(c) 73 | So(err, ShouldBeNil) 74 | 75 | app, err := client.AppByGuid("9902530c-c634-4864-a189-71d763cb12e2") 76 | So(err, ShouldBeNil) 77 | 78 | So(app.Guid, ShouldEqual, "9902530c-c634-4864-a189-71d763cb12e2") 79 | So(app.Name, ShouldEqual, "test-env") 80 | }) 81 | 82 | Convey("App By GUID with environment variables with different types", t, func() { 83 | setup(MockRoute{"GET", "/v2/apps/9902530c-c634-4864-a189-71d763cb12e2", appPayloadWithEnvironment_json, "", 200, "inline-relations-depth=2"}, t) 84 | defer teardown() 85 | c := &Config{ 86 | ApiAddress: server.URL, 87 | Token: "foobar", 88 | } 89 | client, err := NewClient(c) 90 | So(err, ShouldBeNil) 91 | 92 | app, err := client.AppByGuid("9902530c-c634-4864-a189-71d763cb12e2") 93 | So(err, ShouldBeNil) 94 | 95 | So(app.Environment["string"], ShouldEqual, "string") 96 | So(app.Environment["int"], ShouldEqual, 1) 97 | }) 98 | } 99 | 100 | func TestGetAppInstances(t *testing.T) { 101 | Convey("App completely running", t, func() { 102 | setup(MockRoute{"GET", "/v2/apps/9902530c-c634-4864-a189-71d763cb12e2/instances", appInstancePayload, "", 200, ""}, t) 103 | defer teardown() 104 | c := &Config{ 105 | ApiAddress: server.URL, 106 | Token: "foobar", 107 | } 108 | client, err := NewClient(c) 109 | So(err, ShouldBeNil) 110 | 111 | appInstances, err := client.GetAppInstances("9902530c-c634-4864-a189-71d763cb12e2") 112 | So(err, ShouldBeNil) 113 | 114 | So(appInstances["0"].State, ShouldEqual, "RUNNING") 115 | So(appInstances["1"].State, ShouldEqual, "RUNNING") 116 | 117 | var d0 float64 = 1455210430.5104606 118 | var d1 float64 = 1455210430.3912115 119 | date0 := time.Unix(int64(d0), 0) 120 | date1 := time.Unix(int64(d1), 0) 121 | 122 | So(appInstances["0"].Since.Format(time.UnixDate), ShouldEqual, date0.Format(time.UnixDate)) 123 | So(appInstances["1"].Since.Format(time.UnixDate), ShouldEqual, date1.Format(time.UnixDate)) 124 | So(appInstances["0"].Since.ToTime(), ShouldHaveSameTypeAs, date0) 125 | So(appInstances["1"].Since.ToTime(), ShouldHaveSameTypeAs, date1) 126 | 127 | }) 128 | 129 | Convey("App partially running", t, func() { 130 | setup(MockRoute{"GET", "/v2/apps/9902530c-c634-4864-a189-71d763cb12e2/instances", appInstanceUnhealthyPayload, "", 200, ""}, t) 131 | defer teardown() 132 | c := &Config{ 133 | ApiAddress: server.URL, 134 | Token: "foobar", 135 | } 136 | client, err := NewClient(c) 137 | So(err, ShouldBeNil) 138 | 139 | appInstances, err := client.GetAppInstances("9902530c-c634-4864-a189-71d763cb12e2") 140 | So(err, ShouldBeNil) 141 | 142 | So(appInstances["0"].State, ShouldEqual, "RUNNING") 143 | So(appInstances["1"].State, ShouldEqual, "STARTING") 144 | 145 | var d0 float64 = 1455210430.5104606 146 | var d1 float64 = 1455210430.3912115 147 | date0 := time.Unix(int64(d0), 0) 148 | date1 := time.Unix(int64(d1), 0) 149 | 150 | So(appInstances["0"].Since.Format(time.UnixDate), ShouldEqual, date0.Format(time.UnixDate)) 151 | So(appInstances["1"].Since.Format(time.UnixDate), ShouldEqual, date1.Format(time.UnixDate)) 152 | So(appInstances["0"].Since.ToTime(), ShouldHaveSameTypeAs, date0) 153 | So(appInstances["1"].Since.ToTime(), ShouldHaveSameTypeAs, date1) 154 | 155 | }) 156 | } 157 | 158 | func TestGetAppStats(t *testing.T) { 159 | Convey("App stats completely running", t, func() { 160 | setup(MockRoute{"GET", "/v2/apps/9902530c-c634-4864-a189-71d763cb12e2/stats", appStatsPayload, "", 200, ""}, t) 161 | defer teardown() 162 | c := &Config{ 163 | ApiAddress: server.URL, 164 | Token: "foobar", 165 | } 166 | client, err := NewClient(c) 167 | So(err, ShouldBeNil) 168 | 169 | appStats, err := client.GetAppStats("9902530c-c634-4864-a189-71d763cb12e2") 170 | So(err, ShouldBeNil) 171 | 172 | So(appStats["0"].State, ShouldEqual, "RUNNING") 173 | So(appStats["1"].State, ShouldEqual, "RUNNING") 174 | 175 | date0, _ := time.Parse("2006-01-02 15:04:05 -0700", "2016-09-17 15:46:17 +0000") 176 | date1, _ := time.Parse("2006-01-02 15:04:05 -0700", "2016-09-17 15:46:17 +0000") 177 | 178 | So(appStats["0"].Stats.Usage.Time.Format(time.UnixDate), ShouldEqual, date0.Format(time.UnixDate)) 179 | So(appStats["1"].Stats.Usage.Time.Format(time.RFC3339), ShouldEqual, date1.Format(time.RFC3339)) 180 | So(appStats["0"].Stats.Usage.Time.ToTime(), ShouldHaveSameTypeAs, date0) 181 | So(appStats["1"].Stats.Usage.Time.ToTime(), ShouldHaveSameTypeAs, date1) 182 | So(appStats["0"].Stats.Usage.CPU, ShouldEqual, 0.36580239597146486) 183 | So(appStats["1"].Stats.Usage.CPU, ShouldEqual, 0.33857742931636664) 184 | So(appStats["0"].Stats.Usage.Mem, ShouldEqual, 518123520) 185 | So(appStats["1"].Stats.Usage.Mem, ShouldEqual, 530731008) 186 | So(appStats["0"].Stats.Usage.Disk, ShouldEqual, 151150592) 187 | So(appStats["1"].Stats.Usage.Disk, ShouldEqual, 151150592) 188 | 189 | }) 190 | } 191 | 192 | func TestGetAppRoutes(t *testing.T) { 193 | Convey("List app routes", t, func() { 194 | setup(MockRoute{"GET", "/v2/apps/9902530c-c634-4864-a189-71d763cb12e2/routes", appRoutesPayload, "", 200, ""}, t) 195 | defer teardown() 196 | c := &Config{ 197 | ApiAddress: server.URL, 198 | Token: "foobar", 199 | } 200 | client, err := NewClient(c) 201 | So(err, ShouldBeNil) 202 | 203 | routes, err := client.GetAppRoutes("9902530c-c634-4864-a189-71d763cb12e2") 204 | So(err, ShouldBeNil) 205 | 206 | So(len(routes), ShouldEqual, 1) 207 | So(routes[0].Guid, ShouldEqual, "311d34d1-c045-4853-845f-05132377ad7d") 208 | So(routes[0].Host, ShouldEqual, "host-36") 209 | So(routes[0].Path, ShouldEqual, "/foo") 210 | So(routes[0].DomainGuid, ShouldEqual, "40a499f7-198a-4289-9aa2-605ba43f92ee") 211 | So(routes[0].SpaceGuid, ShouldEqual, "c7c0dd06-b078-43d7-adcb-3974cd785fdd") 212 | So(routes[0].ServiceInstanceGuid, ShouldEqual, "") 213 | So(routes[0].Port, ShouldEqual, 0) 214 | 215 | }) 216 | } 217 | 218 | func TestKillAppInstance(t *testing.T) { 219 | Convey("Kills an app instance", t, func() { 220 | setup(MockRoute{"DELETE", "/v2/apps/9902530c-c634-4864-a189-71d763cb12e2/instances/0", "", "", 204, ""}, t) 221 | defer teardown() 222 | c := &Config{ 223 | ApiAddress: server.URL, 224 | Token: "foobar", 225 | } 226 | client, err := NewClient(c) 227 | So(err, ShouldBeNil) 228 | 229 | So(client.KillAppInstance("9902530c-c634-4864-a189-71d763cb12e2", "0"), ShouldBeNil) 230 | }) 231 | } 232 | 233 | func TestAppEnv(t *testing.T) { 234 | Convey("Find app space", t, func() { 235 | setup(MockRoute{"GET", "/v2/apps/a7c47787-a982-467c-95d7-9ab17cbcc918/env", appEnvPayload, "", 200, ""}, t) 236 | defer teardown() 237 | c := &Config{ 238 | ApiAddress: server.URL, 239 | Token: "foobar", 240 | } 241 | client, err := NewClient(c) 242 | So(err, ShouldBeNil) 243 | 244 | appEnv, err := client.GetAppEnv("a7c47787-a982-467c-95d7-9ab17cbcc918") 245 | So(err, ShouldBeNil) 246 | So(appEnv.StagingEnv, ShouldResemble, map[string]interface{}{"STAGING_ENV": "staging_value"}) 247 | So(appEnv.RunningEnv, ShouldResemble, map[string]interface{}{"RUNNING_ENV": "running_value"}) 248 | So(appEnv.Environment, ShouldResemble, map[string]interface{}{"env_var": "env_val"}) 249 | So(appEnv.SystemEnv["VCAP_SERVICES"].(map[string]interface{})["abc"], ShouldEqual, 123) 250 | So(appEnv.ApplicationEnv["VCAP_APPLICATION"].(map[string]interface{})["application_name"], ShouldEqual, "name-2245") 251 | }) 252 | } 253 | 254 | func TestAppSpace(t *testing.T) { 255 | Convey("Find app space", t, func() { 256 | setup(MockRoute{"GET", "/v2/spaces/foobar", spacePayload, "", 200, ""}, t) 257 | defer teardown() 258 | c := &Config{ 259 | ApiAddress: server.URL, 260 | Token: "foobar", 261 | } 262 | client, err := NewClient(c) 263 | So(err, ShouldBeNil) 264 | 265 | app := &App{ 266 | Guid: "123", 267 | Name: "test app", 268 | SpaceURL: "/v2/spaces/foobar", 269 | c: client, 270 | } 271 | space, err := app.Space() 272 | So(err, ShouldBeNil) 273 | 274 | So(space.Name, ShouldEqual, "test-space") 275 | So(space.Guid, ShouldEqual, "a72fa1e8-c694-47b3-85f2-55f61fd00d73") 276 | }) 277 | } 278 | -------------------------------------------------------------------------------- /apps.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/url" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type AppResponse struct { 15 | Count int `json:"total_results"` 16 | Pages int `json:"total_pages"` 17 | NextUrl string `json:"next_url"` 18 | Resources []AppResource `json:"resources"` 19 | } 20 | 21 | type AppResource struct { 22 | Meta Meta `json:"metadata"` 23 | Entity App `json:"entity"` 24 | } 25 | 26 | type App struct { 27 | Guid string `json:"guid"` 28 | CreatedAt string `json:"created_at"` 29 | UpdatedAt string `json:"updated_at"` 30 | Name string `json:"name"` 31 | Memory int `json:"memory"` 32 | Instances int `json:"instances"` 33 | DiskQuota int `json:"disk_quota"` 34 | SpaceGuid string `json:"space_guid"` 35 | StackGuid string `json:"stack_guid"` 36 | State string `json:"state"` 37 | PackageState string `json:"package_state"` 38 | Command string `json:"command"` 39 | Buildpack string `json:"buildpack"` 40 | DetectedBuildpack string `json:"detected_buildpack"` 41 | DetectedBuildpackGuid string `json:"detected_buildpack_guid"` 42 | HealthCheckHttpEndpoint string `json:"health_check_http_endpoint"` 43 | HealthCheckType string `json:"health_check_type"` 44 | HealthCheckTimeout int `json:"health_check_timeout"` 45 | Diego bool `json:"diego"` 46 | EnableSSH bool `json:"enable_ssh"` 47 | DetectedStartCommand string `json:"detected_start_command"` 48 | DockerImage string `json:"docker_image"` 49 | DockerCredentials map[string]interface{} `json:"docker_credentials_json"` 50 | Environment map[string]interface{} `json:"environment_json"` 51 | StagingFailedReason string `json:"staging_failed_reason"` 52 | StagingFailedDescription string `json:"staging_failed_description"` 53 | Ports []int `json:"ports"` 54 | SpaceURL string `json:"space_url"` 55 | SpaceData SpaceResource `json:"space"` 56 | PackageUpdatedAt string `json:"package_updated_at"` 57 | c *Client 58 | } 59 | 60 | type AppInstance struct { 61 | State string `json:"state"` 62 | Since sinceTime `json:"since"` 63 | } 64 | 65 | type AppStats struct { 66 | State string `json:"state"` 67 | Stats struct { 68 | Name string `json:"name"` 69 | Uris []string `json:"uris"` 70 | Host string `json:"host"` 71 | Port int `json:"port"` 72 | Uptime int `json:"uptime"` 73 | MemQuota int `json:"mem_quota"` 74 | DiskQuota int `json:"disk_quota"` 75 | FdsQuota int `json:"fds_quota"` 76 | Usage struct { 77 | Time statTime `json:"time"` 78 | CPU float64 `json:"cpu"` 79 | Mem int `json:"mem"` 80 | Disk int `json:"disk"` 81 | } `json:"usage"` 82 | } `json:"stats"` 83 | } 84 | 85 | type AppSummary struct { 86 | Guid string `json:"guid"` 87 | Name string `json:"name"` 88 | ServiceCount int `json:"service_count"` 89 | RunningInstances int `json:"running_instances"` 90 | Memory int `json:"memory"` 91 | Instances int `json:"instances"` 92 | DiskQuota int `json:"disk_quota"` 93 | State string `json:"state"` 94 | Diego bool `json:"diego"` 95 | } 96 | 97 | type AppEnv struct { 98 | // These can have arbitrary JSON so need to map to interface{} 99 | Environment map[string]interface{} `json:"environment_json"` 100 | StagingEnv map[string]interface{} `json:"staging_env_json"` 101 | RunningEnv map[string]interface{} `json:"running_env_json"` 102 | SystemEnv map[string]interface{} `json:"system_env_json"` 103 | ApplicationEnv map[string]interface{} `json:"application_env_json"` 104 | } 105 | 106 | // Custom time types to handle non-RFC3339 formatting in API JSON 107 | 108 | type sinceTime struct { 109 | time.Time 110 | } 111 | 112 | func (s *sinceTime) UnmarshalJSON(b []byte) (err error) { 113 | timeFlt, err := strconv.ParseFloat(string(b), 64) 114 | if err != nil { 115 | return err 116 | } 117 | time := time.Unix(int64(timeFlt), 0) 118 | *s = sinceTime{time} 119 | return nil 120 | } 121 | 122 | func (s sinceTime) ToTime() time.Time { 123 | t, _ := time.Parse(time.UnixDate, s.Format(time.UnixDate)) 124 | return t 125 | } 126 | 127 | type statTime struct { 128 | time.Time 129 | } 130 | 131 | func (s *statTime) UnmarshalJSON(b []byte) (err error) { 132 | timeString, err := strconv.Unquote(string(b)) 133 | if err != nil { 134 | return err 135 | } 136 | // RFC3339 time format 137 | if (string(timeString[10]) == "T" && string(timeString[19]) == "Z"){ 138 | time, err := time.Parse("2006-01-02T15:04:05Z07:00", timeString) 139 | if err != nil { 140 | return err 141 | } 142 | *s = statTime{time} 143 | // Unix epoch time format 144 | } else { 145 | time, err := time.Parse("2006-01-02 15:04:05 -0700", timeString) 146 | if err != nil { 147 | return err 148 | } 149 | *s = statTime{time} 150 | } 151 | if err != nil { 152 | return err 153 | } 154 | return nil 155 | } 156 | 157 | func (s statTime) ToTime() time.Time { 158 | t, _ := time.Parse(time.UnixDate, s.Format(time.UnixDate)) 159 | return t 160 | } 161 | 162 | func (a *App) Space() (Space, error) { 163 | var spaceResource SpaceResource 164 | r := a.c.NewRequest("GET", a.SpaceURL) 165 | resp, err := a.c.DoRequest(r) 166 | if err != nil { 167 | return Space{}, errors.Wrap(err, "Error requesting space") 168 | } 169 | defer resp.Body.Close() 170 | resBody, err := ioutil.ReadAll(resp.Body) 171 | if err != nil { 172 | return Space{}, errors.Wrap(err, "Error reading space response") 173 | } 174 | 175 | err = json.Unmarshal(resBody, &spaceResource) 176 | if err != nil { 177 | return Space{}, errors.Wrap(err, "Error unmarshalling body") 178 | } 179 | spaceResource.Entity.Guid = spaceResource.Meta.Guid 180 | spaceResource.Entity.c = a.c 181 | return spaceResource.Entity, nil 182 | } 183 | 184 | func (c *Client) ListAppsByQuery(query url.Values) ([]App, error) { 185 | var apps []App 186 | 187 | requestUrl := "/v2/apps?" + query.Encode() 188 | for { 189 | var appResp AppResponse 190 | r := c.NewRequest("GET", requestUrl) 191 | resp, err := c.DoRequest(r) 192 | if err != nil { 193 | return nil, errors.Wrap(err, "Error requesting apps") 194 | } 195 | defer resp.Body.Close() 196 | resBody, err := ioutil.ReadAll(resp.Body) 197 | if err != nil { 198 | return nil, errors.Wrap(err, "Error reading app request") 199 | } 200 | 201 | err = json.Unmarshal(resBody, &appResp) 202 | if err != nil { 203 | return nil, errors.Wrap(err, "Error unmarshalling app") 204 | } 205 | 206 | for _, app := range appResp.Resources { 207 | app.Entity.Guid = app.Meta.Guid 208 | app.Entity.CreatedAt = app.Meta.CreatedAt 209 | app.Entity.UpdatedAt = app.Meta.UpdatedAt 210 | app.Entity.SpaceData.Entity.Guid = app.Entity.SpaceData.Meta.Guid 211 | app.Entity.SpaceData.Entity.OrgData.Entity.Guid = app.Entity.SpaceData.Entity.OrgData.Meta.Guid 212 | app.Entity.c = c 213 | apps = append(apps, app.Entity) 214 | } 215 | 216 | requestUrl = appResp.NextUrl 217 | if requestUrl == "" { 218 | break 219 | } 220 | } 221 | return apps, nil 222 | } 223 | 224 | func (c *Client) ListApps() ([]App, error) { 225 | q := url.Values{} 226 | q.Set("inline-relations-depth", "2") 227 | return c.ListAppsByQuery(q) 228 | } 229 | 230 | func (c *Client) GetAppInstances(guid string) (map[string]AppInstance, error) { 231 | var appInstances map[string]AppInstance 232 | 233 | requestURL := fmt.Sprintf("/v2/apps/%s/instances", guid) 234 | r := c.NewRequest("GET", requestURL) 235 | resp, err := c.DoRequest(r) 236 | if err != nil { 237 | return nil, errors.Wrap(err, "Error requesting app instances") 238 | } 239 | defer resp.Body.Close() 240 | resBody, err := ioutil.ReadAll(resp.Body) 241 | if err != nil { 242 | return nil, errors.Wrap(err, "Error reading app instances") 243 | } 244 | err = json.Unmarshal(resBody, &appInstances) 245 | if err != nil { 246 | return nil, errors.Wrap(err, "Error unmarshalling app instances") 247 | } 248 | return appInstances, nil 249 | } 250 | 251 | func (c *Client) GetAppEnv(guid string) (AppEnv, error) { 252 | var appEnv AppEnv 253 | 254 | requestURL := fmt.Sprintf("/v2/apps/%s/env", guid) 255 | r := c.NewRequest("GET", requestURL) 256 | resp, err := c.DoRequest(r) 257 | if err != nil { 258 | return appEnv, errors.Wrap(err, "Error requesting app env") 259 | } 260 | defer resp.Body.Close() 261 | resBody, err := ioutil.ReadAll(resp.Body) 262 | if err != nil { 263 | return appEnv, errors.Wrap(err, "Error reading app env") 264 | } 265 | err = json.Unmarshal(resBody, &appEnv) 266 | if err != nil { 267 | return appEnv, errors.Wrap(err, "Error unmarshalling app env") 268 | } 269 | return appEnv, nil 270 | } 271 | 272 | func (c *Client) GetAppRoutes(guid string) ([]Route, error) { 273 | return c.fetchRoutes(fmt.Sprintf("/v2/apps/%s/routes", guid)) 274 | } 275 | 276 | func (c *Client) GetAppStats(guid string) (map[string]AppStats, error) { 277 | var appStats map[string]AppStats 278 | 279 | requestURL := fmt.Sprintf("/v2/apps/%s/stats", guid) 280 | r := c.NewRequest("GET", requestURL) 281 | resp, err := c.DoRequest(r) 282 | if err != nil { 283 | return nil, errors.Wrap(err, "Error requesting app stats") 284 | } 285 | defer resp.Body.Close() 286 | resBody, err := ioutil.ReadAll(resp.Body) 287 | if err != nil { 288 | return nil, errors.Wrap(err, "Error reading app stats") 289 | } 290 | err = json.Unmarshal(resBody, &appStats) 291 | if err != nil { 292 | return nil, errors.Wrap(err, "Error unmarshalling app stats") 293 | } 294 | return appStats, nil 295 | } 296 | 297 | func (c *Client) KillAppInstance(guid string, index string) error { 298 | requestURL := fmt.Sprintf("/v2/apps/%s/instances/%s", guid, index) 299 | r := c.NewRequest("DELETE", requestURL) 300 | resp, err := c.DoRequest(r) 301 | if err != nil { 302 | return errors.Wrapf(err, "Error stopping app %s at index %s", guid, index) 303 | } 304 | defer resp.Body.Close() 305 | if resp.StatusCode != 204 { 306 | return errors.Wrapf(err, "Error stopping app %s at index %s", guid, index) 307 | } 308 | return nil 309 | } 310 | 311 | func (c *Client) AppByGuid(guid string) (App, error) { 312 | var appResource AppResource 313 | r := c.NewRequest("GET", "/v2/apps/"+guid+"?inline-relations-depth=2") 314 | resp, err := c.DoRequest(r) 315 | if err != nil { 316 | return App{}, errors.Wrap(err, "Error requesting apps") 317 | } 318 | defer resp.Body.Close() 319 | resBody, err := ioutil.ReadAll(resp.Body) 320 | if err != nil { 321 | return App{}, errors.Wrap(err, "Error reading app response body") 322 | } 323 | 324 | err = json.Unmarshal(resBody, &appResource) 325 | if err != nil { 326 | return App{}, errors.Wrap(err, "Error unmarshalling app") 327 | } 328 | appResource.Entity.Guid = appResource.Meta.Guid 329 | appResource.Entity.SpaceData.Entity.Guid = appResource.Entity.SpaceData.Meta.Guid 330 | appResource.Entity.SpaceData.Entity.OrgData.Entity.Guid = appResource.Entity.SpaceData.Entity.OrgData.Meta.Guid 331 | appResource.Entity.c = c 332 | return appResource.Entity, nil 333 | } 334 | -------------------------------------------------------------------------------- /secgroups.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type SecGroupResponse struct { 13 | Count int `json:"total_results"` 14 | Pages int `json:"total_pages"` 15 | NextUrl string `json:"next_url"` 16 | Resources []SecGroupResource `json:"resources"` 17 | } 18 | 19 | type SecGroupCreateResponse struct { 20 | Code int `json:"code"` 21 | ErrorCode string `json:"error_code"` 22 | Description string `json:"description"` 23 | } 24 | 25 | type SecGroupResource struct { 26 | Meta Meta `json:"metadata"` 27 | Entity SecGroup `json:"entity"` 28 | } 29 | 30 | type SecGroup struct { 31 | Guid string `json:"guid"` 32 | Name string `json:"name"` 33 | Rules []SecGroupRule `json:"rules"` 34 | Running bool `json:"running_default"` 35 | Staging bool `json:"staging_default"` 36 | SpacesURL string `json:"spaces_url"` 37 | SpacesData []SpaceResource `json:"spaces"` 38 | c *Client 39 | } 40 | 41 | type SecGroupRule struct { 42 | Protocol string `json:"protocol"` 43 | Ports string `json:"ports,omitempty"` //e.g. "4000-5000,9142" 44 | Destination string `json:"destination"` //CIDR Format 45 | Description string `json:"description,omitempty"` //Optional description 46 | Code int `json:"code,omitempty"` // ICMP code 47 | Type int `json:"type,omitempty"` //ICMP type. Only valid if Protocol=="icmp" 48 | Log bool `json:"log,omitempty"` //If true, log this rule 49 | } 50 | 51 | func (c *Client) ListSecGroups() (secGroups []SecGroup, err error) { 52 | requestURL := "/v2/security_groups?inline-relations-depth=1" 53 | for requestURL != "" { 54 | var secGroupResp SecGroupResponse 55 | r := c.NewRequest("GET", requestURL) 56 | resp, err := c.DoRequest(r) 57 | 58 | if err != nil { 59 | return nil, errors.Wrap(err, "Error requesting sec groups") 60 | } 61 | resBody, err := ioutil.ReadAll(resp.Body) 62 | if err != nil { 63 | return nil, errors.Wrap(err, "Error reading sec group response body") 64 | } 65 | 66 | err = json.Unmarshal(resBody, &secGroupResp) 67 | if err != nil { 68 | return nil, errors.Wrap(err, "Error unmarshaling sec group") 69 | } 70 | 71 | for _, secGroup := range secGroupResp.Resources { 72 | secGroup.Entity.Guid = secGroup.Meta.Guid 73 | secGroup.Entity.c = c 74 | for i, space := range secGroup.Entity.SpacesData { 75 | space.Entity.Guid = space.Meta.Guid 76 | secGroup.Entity.SpacesData[i] = space 77 | } 78 | if len(secGroup.Entity.SpacesData) == 0 { 79 | spaces, err := secGroup.Entity.ListSpaceResources() 80 | if err != nil { 81 | return nil, err 82 | } 83 | for _, space := range spaces { 84 | secGroup.Entity.SpacesData = append(secGroup.Entity.SpacesData, space) 85 | } 86 | } 87 | secGroups = append(secGroups, secGroup.Entity) 88 | } 89 | 90 | requestURL = secGroupResp.NextUrl 91 | resp.Body.Close() 92 | } 93 | return secGroups, nil 94 | } 95 | 96 | func (c *Client) GetSecGroupByName(name string) (secGroup SecGroup, err error) { 97 | requestURL := "/v2/security_groups?q=name:" + name 98 | var secGroupResp SecGroupResponse 99 | r := c.NewRequest("GET", requestURL) 100 | resp, err := c.DoRequest(r) 101 | 102 | if err != nil { 103 | return secGroup, errors.Wrap(err, "Error requesting sec groups") 104 | } 105 | resBody, err := ioutil.ReadAll(resp.Body) 106 | if err != nil { 107 | return secGroup, errors.Wrap(err, "Error reading sec group response body") 108 | } 109 | 110 | err = json.Unmarshal(resBody, &secGroupResp) 111 | if err != nil { 112 | return secGroup, errors.Wrap(err, "Error unmarshaling sec group") 113 | } 114 | if len(secGroupResp.Resources) == 0 { 115 | return secGroup, fmt.Errorf("No security group with name %v found", name) 116 | } 117 | secGroup = secGroupResp.Resources[0].Entity 118 | secGroup.Guid = secGroupResp.Resources[0].Meta.Guid 119 | secGroup.c = c 120 | 121 | resp.Body.Close() 122 | return secGroup, nil 123 | } 124 | 125 | func (secGroup *SecGroup) ListSpaceResources() ([]SpaceResource, error) { 126 | var spaceResources []SpaceResource 127 | requestURL := secGroup.SpacesURL 128 | for requestURL != "" { 129 | spaceResp, err := secGroup.c.getSpaceResponse(requestURL) 130 | if err != nil { 131 | return []SpaceResource{}, err 132 | } 133 | for i, spaceRes := range spaceResp.Resources { 134 | spaceRes.Entity.Guid = spaceRes.Meta.Guid 135 | spaceResp.Resources[i] = spaceRes 136 | } 137 | spaceResources = append(spaceResources, spaceResp.Resources...) 138 | requestURL = spaceResp.NextUrl 139 | } 140 | return spaceResources, nil 141 | } 142 | 143 | /* 144 | CreateSecGroup contacts the CF endpoint for creating a new security group. 145 | name: the name to give to the created security group 146 | rules: A slice of rule objects that describe the rules that this security group enforces. 147 | This can technically be nil or an empty slice - we won't judge you 148 | spaceGuids: The security group will be associated with the spaces specified by the contents of this slice. 149 | If nil, the security group will not be associated with any spaces initially. 150 | */ 151 | func (c *Client) CreateSecGroup(name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) { 152 | return c.secGroupCreateHelper("/v2/security_groups", "POST", name, rules, spaceGuids) 153 | } 154 | 155 | /* 156 | UpdateSecGroup contacts the CF endpoint to update an existing security group. 157 | guid: identifies the security group that you would like to update. 158 | name: the new name to give to the security group 159 | rules: A slice of rule objects that describe the rules that this security group enforces. 160 | If this is left nil, the rules will not be changed. 161 | spaceGuids: The security group will be associated with the spaces specified by the contents of this slice. 162 | If nil, the space associations will not be changed. 163 | */ 164 | func (c *Client) UpdateSecGroup(guid, name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) { 165 | return c.secGroupCreateHelper("/v2/security_groups/"+guid, "PUT", name, rules, spaceGuids) 166 | } 167 | 168 | /* 169 | DeleteSecGroup contacts the CF endpoint to delete an existing security group. 170 | guid: Indentifies the security group to be deleted. 171 | */ 172 | func (c *Client) DeleteSecGroup(guid string) error { 173 | //Perform the DELETE and check for errors 174 | resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s", guid))) 175 | if err != nil { 176 | return err 177 | } 178 | if resp.StatusCode != 204 { //204 No Content 179 | return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 180 | } 181 | return nil 182 | } 183 | 184 | /* 185 | GetSecGroup contacts the CF endpoint for fetching the info for a particular security group. 186 | guid: Identifies the security group to fetch information from 187 | */ 188 | func (c *Client) GetSecGroup(guid string) (*SecGroup, error) { 189 | //Perform the GET and check for errors 190 | resp, err := c.DoRequest(c.NewRequest("GET", "/v2/security_groups/"+guid)) 191 | if err != nil { 192 | return nil, err 193 | } 194 | if resp.StatusCode != 200 { 195 | return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 196 | } 197 | //get the json out of the response body 198 | return respBodyToSecGroup(resp.Body, c) 199 | } 200 | 201 | /* 202 | BindSecGroup contacts the CF endpoint to associate a space with a security group 203 | secGUID: identifies the security group to add a space to 204 | spaceGUID: identifies the space to associate 205 | */ 206 | func (c *Client) BindSecGroup(secGUID, spaceGUID string) error { 207 | //Perform the PUT and check for errors 208 | resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/security_groups/%s/spaces/%s", secGUID, spaceGUID))) 209 | if err != nil { 210 | return err 211 | } 212 | if resp.StatusCode != 201 { //201 Created 213 | return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 214 | } 215 | return nil 216 | } 217 | 218 | /* 219 | BindRunningSecGroup contacts the CF endpoint to associate a security group 220 | secGUID: identifies the security group to add a space to 221 | */ 222 | func (c *Client) BindRunningSecGroup(secGUID string) error { 223 | //Perform the PUT and check for errors 224 | resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/config/running_security_groups/%s", secGUID))) 225 | if err != nil { 226 | return err 227 | } 228 | if resp.StatusCode != 200 { //200 229 | return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 230 | } 231 | return nil 232 | } 233 | 234 | /* 235 | BindStagingSecGroup contacts the CF endpoint to associate a space with a security group 236 | secGUID: identifies the security group to add a space to 237 | */ 238 | func (c *Client) BindStagingSecGroup(secGUID string) error { 239 | //Perform the PUT and check for errors 240 | resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/config/staging_security_groups/%s", secGUID))) 241 | if err != nil { 242 | return err 243 | } 244 | if resp.StatusCode != 200 { //200 245 | return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 246 | } 247 | return nil 248 | } 249 | 250 | /* 251 | UnbindSecGroup contacts the CF endpoint to dissociate a space from a security group 252 | secGUID: identifies the security group to remove a space from 253 | spaceGUID: identifies the space to dissociate from the security group 254 | */ 255 | func (c *Client) UnbindSecGroup(secGUID, spaceGUID string) error { 256 | //Perform the DELETE and check for errors 257 | resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s/spaces/%s", secGUID, spaceGUID))) 258 | if err != nil { 259 | return err 260 | } 261 | if resp.StatusCode != 204 { //204 No Content 262 | return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 263 | } 264 | return nil 265 | } 266 | 267 | //Reads most security group response bodies into a SecGroup object 268 | func respBodyToSecGroup(body io.ReadCloser, c *Client) (*SecGroup, error) { 269 | //get the json from the response body 270 | bodyRaw, err := ioutil.ReadAll(body) 271 | if err != nil { 272 | return nil, errors.Wrap(err, "Could not read response body") 273 | } 274 | jStruct := SecGroupResource{} 275 | //make it a SecGroup 276 | err = json.Unmarshal([]byte(bodyRaw), &jStruct) 277 | if err != nil { 278 | return nil, errors.Wrap(err, "Could not unmarshal response body as json") 279 | } 280 | //pull a few extra fields from other places 281 | ret := jStruct.Entity 282 | ret.Guid = jStruct.Meta.Guid 283 | ret.c = c 284 | return &ret, nil 285 | } 286 | 287 | //Create and Update secGroup pretty much do the same thing, so this function abstracts those out. 288 | func (c *Client) secGroupCreateHelper(url, method, name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) { 289 | req := c.NewRequest(method, url) 290 | //set up request body 291 | req.obj = map[string]interface{}{ 292 | "name": name, 293 | "rules": rules, 294 | "space_guids": spaceGuids, 295 | } 296 | //fire off the request and check for problems 297 | resp, err := c.DoRequest(req) 298 | if err != nil { 299 | return nil, err 300 | } 301 | if resp.StatusCode != 201 { // Both create and update should give 201 CREATED 302 | var response SecGroupCreateResponse 303 | 304 | bodyRaw, _ := ioutil.ReadAll(resp.Body) 305 | 306 | err = json.Unmarshal(bodyRaw, &response) 307 | if err != nil { 308 | return nil, errors.Wrap(err, "Error unmarshaling response") 309 | } 310 | 311 | return nil, fmt.Errorf(`Request failed CF API returned with status code %d 312 | ------------------------------- 313 | Error Code %s 314 | Code %d 315 | Description %s`, 316 | resp.StatusCode, response.ErrorCode, response.Code, response.Description) 317 | } 318 | //get the json from the response body 319 | return respBodyToSecGroup(resp.Body, c) 320 | } 321 | -------------------------------------------------------------------------------- /spaces.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type SpaceRequest struct { 15 | Name string `json:"name"` 16 | OrganizationGuid string `json:"organization_guid"` 17 | DeveloperGuid []string `json:"developer_guids"` 18 | ManagerGuid []string `json:"manager_guids"` 19 | AuditorGuid []string `json:"auditor_guids"` 20 | DomainGuid []string `json:"domain_guids"` 21 | SecurityGroupGuids []string `json:"security_group_guids"` 22 | SpaceQuotaDefGuid []string `json:"space_quota_definition_guid"` 23 | AllowSSH []string `json:"allow_ssh"` 24 | } 25 | 26 | type SpaceResponse struct { 27 | Count int `json:"total_results"` 28 | Pages int `json:"total_pages"` 29 | NextUrl string `json:"next_url"` 30 | Resources []SpaceResource `json:"resources"` 31 | } 32 | 33 | type SpaceResource struct { 34 | Meta Meta `json:"metadata"` 35 | Entity Space `json:"entity"` 36 | } 37 | 38 | type Space struct { 39 | Guid string `json:"guid"` 40 | Name string `json:"name"` 41 | OrganizationGuid string `json:"organization_guid"` 42 | OrgURL string `json:"organization_url"` 43 | OrgData OrgResource `json:"organization"` 44 | QuotaDefinitionGuid string `json:"space_quota_definition_guid"` 45 | c *Client 46 | } 47 | 48 | type SpaceSummary struct { 49 | Guid string `json:"guid"` 50 | Name string `json:"name"` 51 | Apps []AppSummary `json:"apps"` 52 | Services []ServiceSummary `json:"services"` 53 | } 54 | 55 | type SpaceRoleResponse struct { 56 | Count int `json:"total_results"` 57 | Pages int `json:"total_pages"` 58 | NextUrl string `json:"next_url"` 59 | Resources []SpaceRoleResource `json:"resources"` 60 | } 61 | 62 | type SpaceRoleResource struct { 63 | Meta Meta `json:"metadata"` 64 | Entity SpaceRole `json:"entity"` 65 | } 66 | 67 | type SpaceRole struct { 68 | Guid string `json:"guid"` 69 | Admin bool `json:"admin"` 70 | Active bool `json:"active"` 71 | DefaultSpaceGuid string `json:"default_space_guid"` 72 | Username string `json:"username"` 73 | SpaceRoles []string `json:"space_roles"` 74 | SpacesUrl string `json:"spaces_url"` 75 | OrganizationsUrl string `json:"organizations_url"` 76 | ManagedOrganizationsUrl string `json:"managed_organizations_url"` 77 | BillingManagedOrganizationsUrl string `json:"billing_managed_organizations_url"` 78 | AuditedOrganizationsUrl string `json:"audited_organizations_url"` 79 | ManagedSpacesUrl string `json:"managed_spaces_url"` 80 | AuditedSpacesUrl string `json:"audited_spaces_url"` 81 | c *Client 82 | } 83 | 84 | func (s *Space) Org() (Org, error) { 85 | var orgResource OrgResource 86 | r := s.c.NewRequest("GET", s.OrgURL) 87 | resp, err := s.c.DoRequest(r) 88 | if err != nil { 89 | return Org{}, errors.Wrap(err, "Error requesting org") 90 | } 91 | resBody, err := ioutil.ReadAll(resp.Body) 92 | if err != nil { 93 | return Org{}, errors.Wrap(err, "Error reading org request") 94 | } 95 | 96 | err = json.Unmarshal(resBody, &orgResource) 97 | if err != nil { 98 | return Org{}, errors.Wrap(err, "Error unmarshaling org") 99 | } 100 | orgResource.Entity.Guid = orgResource.Meta.Guid 101 | orgResource.Entity.c = s.c 102 | return orgResource.Entity, nil 103 | } 104 | 105 | func (s *Space) Quota() (*SpaceQuota, error) { 106 | var spaceQuota *SpaceQuota 107 | var spaceQuotaResource SpaceQuotasResource 108 | if s.QuotaDefinitionGuid == "" { 109 | return nil, nil 110 | } 111 | requestUrl := fmt.Sprintf("/v2/space_quota_definitions/%s", s.QuotaDefinitionGuid) 112 | r := s.c.NewRequest("GET", requestUrl) 113 | resp, err := s.c.DoRequest(r) 114 | if err != nil { 115 | return &SpaceQuota{}, errors.Wrap(err, "Error requesting space quota") 116 | } 117 | resBody, err := ioutil.ReadAll(resp.Body) 118 | defer resp.Body.Close() 119 | if err != nil { 120 | return &SpaceQuota{}, errors.Wrap(err, "Error reading space quota body") 121 | } 122 | err = json.Unmarshal(resBody, &spaceQuotaResource) 123 | if err != nil { 124 | return &SpaceQuota{}, errors.Wrap(err, "Error unmarshalling space quota") 125 | } 126 | spaceQuota = &spaceQuotaResource.Entity 127 | spaceQuota.Guid = spaceQuotaResource.Meta.Guid 128 | spaceQuota.c = s.c 129 | return spaceQuota, nil 130 | } 131 | 132 | func (s *Space) Summary() (SpaceSummary, error) { 133 | var spaceSummary SpaceSummary 134 | requestUrl := fmt.Sprintf("/v2/spaces/%s/summary", s.Guid) 135 | r := s.c.NewRequest("GET", requestUrl) 136 | resp, err := s.c.DoRequest(r) 137 | if err != nil { 138 | return SpaceSummary{}, errors.Wrap(err, "Error requesting space summary") 139 | } 140 | resBody, err := ioutil.ReadAll(resp.Body) 141 | defer resp.Body.Close() 142 | if err != nil { 143 | return SpaceSummary{}, errors.Wrap(err, "Error reading space summary body") 144 | } 145 | err = json.Unmarshal(resBody, &spaceSummary) 146 | if err != nil { 147 | return SpaceSummary{}, errors.Wrap(err, "Error unmarshalling space summary") 148 | } 149 | return spaceSummary, nil 150 | } 151 | 152 | func (s *Space) Roles() ([]SpaceRole, error) { 153 | var roles []SpaceRole 154 | requestUrl := fmt.Sprintf("/v2/spaces/%s/user_roles", s.Guid) 155 | for { 156 | rolesResp, err := s.c.getSpaceRolesResponse(requestUrl) 157 | if err != nil { 158 | return roles, err 159 | } 160 | for _, role := range rolesResp.Resources { 161 | role.Entity.Guid = role.Meta.Guid 162 | role.Entity.c = s.c 163 | roles = append(roles, role.Entity) 164 | } 165 | requestUrl = rolesResp.NextUrl 166 | if requestUrl == "" { 167 | break 168 | } 169 | } 170 | return roles, nil 171 | } 172 | 173 | func (c *Client) CreateSpace(req SpaceRequest) (Space, error) { 174 | buf := bytes.NewBuffer(nil) 175 | err := json.NewEncoder(buf).Encode(req) 176 | if err != nil { 177 | return Space{}, err 178 | } 179 | r := c.NewRequestWithBody("POST", "/v2/spaces", buf) 180 | resp, err := c.DoRequest(r) 181 | if err != nil { 182 | return Space{}, err 183 | } 184 | if resp.StatusCode != http.StatusCreated { 185 | return Space{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 186 | } 187 | return c.handleSpaceResp(resp) 188 | } 189 | 190 | func (c *Client) AssociateSpaceDeveloperByUsername(spaceGUID, name string) (Space, error) { 191 | space := Space{Guid: spaceGUID, c: c} 192 | return space.AssociateDeveloperByUsername(name) 193 | } 194 | 195 | func (c *Client) RemoveSpaceDeveloperByUsername(spaceGUID, name string) error { 196 | space := Space{Guid: spaceGUID, c: c} 197 | return space.RemoveDeveloperByUsername(name) 198 | } 199 | 200 | func (c *Client) AssociateSpaceAuditorByUsername(spaceGUID, name string) (Space, error) { 201 | space := Space{Guid: spaceGUID, c: c} 202 | return space.AssociateAuditorByUsername(name) 203 | } 204 | 205 | func (c *Client) RemoveSpaceAuditorByUsername(spaceGUID, name string) error { 206 | space := Space{Guid: spaceGUID, c: c} 207 | return space.RemoveAuditorByUsername(name) 208 | } 209 | 210 | func (s *Space) AssociateDeveloperByUsername(name string) (Space, error) { 211 | requestUrl := fmt.Sprintf("/v2/spaces/%s/developers", s.Guid) 212 | buf := bytes.NewBuffer(nil) 213 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 214 | if err != nil { 215 | return Space{}, err 216 | } 217 | r := s.c.NewRequestWithBody("PUT", requestUrl, buf) 218 | resp, err := s.c.DoRequest(r) 219 | if err != nil { 220 | return Space{}, err 221 | } 222 | if resp.StatusCode != http.StatusCreated { 223 | return Space{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 224 | } 225 | return s.c.handleSpaceResp(resp) 226 | } 227 | 228 | func (s *Space) RemoveDeveloperByUsername(name string) error { 229 | requestUrl := fmt.Sprintf("/v2/spaces/%s/developers", s.Guid) 230 | buf := bytes.NewBuffer(nil) 231 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 232 | if err != nil { 233 | return err 234 | } 235 | r := s.c.NewRequestWithBody("DELETE", requestUrl, buf) 236 | resp, err := s.c.DoRequest(r) 237 | if err != nil { 238 | return err 239 | } 240 | if resp.StatusCode != http.StatusNoContent { 241 | return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 242 | } 243 | return nil 244 | } 245 | func (s *Space) AssociateAuditorByUsername(name string) (Space, error) { 246 | requestUrl := fmt.Sprintf("/v2/spaces/%s/auditors", s.Guid) 247 | buf := bytes.NewBuffer(nil) 248 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 249 | if err != nil { 250 | return Space{}, err 251 | } 252 | r := s.c.NewRequestWithBody("PUT", requestUrl, buf) 253 | resp, err := s.c.DoRequest(r) 254 | if err != nil { 255 | return Space{}, err 256 | } 257 | if resp.StatusCode != http.StatusCreated { 258 | return Space{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 259 | } 260 | return s.c.handleSpaceResp(resp) 261 | } 262 | 263 | func (s *Space) RemoveAuditorByUsername(name string) error { 264 | requestUrl := fmt.Sprintf("/v2/spaces/%s/auditors", s.Guid) 265 | buf := bytes.NewBuffer(nil) 266 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 267 | if err != nil { 268 | return err 269 | } 270 | r := s.c.NewRequestWithBody("DELETE", requestUrl, buf) 271 | resp, err := s.c.DoRequest(r) 272 | if err != nil { 273 | return err 274 | } 275 | if resp.StatusCode != http.StatusNoContent { 276 | return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) 277 | } 278 | return nil 279 | } 280 | 281 | func (c *Client) ListSpacesByQuery(query url.Values) ([]Space, error) { 282 | return c.fetchSpaces("/v2/spaces?" + query.Encode()) 283 | } 284 | 285 | func (c *Client) ListSpaces() ([]Space, error) { 286 | return c.ListSpacesByQuery(nil) 287 | } 288 | 289 | func (c *Client) fetchSpaces(requestUrl string) ([]Space, error) { 290 | var spaces []Space 291 | for { 292 | spaceResp, err := c.getSpaceResponse(requestUrl) 293 | if err != nil { 294 | return []Space{}, err 295 | } 296 | for _, space := range spaceResp.Resources { 297 | space.Entity.Guid = space.Meta.Guid 298 | space.Entity.c = c 299 | spaces = append(spaces, space.Entity) 300 | } 301 | requestUrl = spaceResp.NextUrl 302 | if requestUrl == "" { 303 | break 304 | } 305 | } 306 | return spaces, nil 307 | } 308 | 309 | func (c *Client) GetSpaceByName(spaceName string, orgGuid string) (Space, error) { 310 | var space Space 311 | q := url.Values{} 312 | q.Set("q", "organization_guid:"+orgGuid) 313 | q.Set("&q", "name:"+spaceName) 314 | spaces, err := c.ListSpacesByQuery(q) 315 | if err != nil { 316 | return space, err 317 | } 318 | 319 | if len(spaces) == 0 { 320 | return space, fmt.Errorf("Unable to find space %s", spaceName) 321 | } 322 | 323 | return spaces[0], nil 324 | 325 | } 326 | 327 | func (c *Client) getSpaceResponse(requestUrl string) (SpaceResponse, error) { 328 | var spaceResp SpaceResponse 329 | r := c.NewRequest("GET", requestUrl) 330 | resp, err := c.DoRequest(r) 331 | if err != nil { 332 | return SpaceResponse{}, errors.Wrap(err, "Error requesting spaces") 333 | } 334 | resBody, err := ioutil.ReadAll(resp.Body) 335 | defer resp.Body.Close() 336 | if err != nil { 337 | return SpaceResponse{}, errors.Wrap(err, "Error reading space request") 338 | } 339 | err = json.Unmarshal(resBody, &spaceResp) 340 | if err != nil { 341 | return SpaceResponse{}, errors.Wrap(err, "Error unmarshalling space") 342 | } 343 | return spaceResp, nil 344 | } 345 | 346 | func (c *Client) getSpaceRolesResponse(requestUrl string) (SpaceRoleResponse, error) { 347 | var roleResp SpaceRoleResponse 348 | r := c.NewRequest("GET", requestUrl) 349 | resp, err := c.DoRequest(r) 350 | if err != nil { 351 | return roleResp, errors.Wrap(err, "Error requesting space roles") 352 | } 353 | resBody, err := ioutil.ReadAll(resp.Body) 354 | defer resp.Body.Close() 355 | if err != nil { 356 | return roleResp, errors.Wrap(err, "Error reading space roles request") 357 | } 358 | err = json.Unmarshal(resBody, &roleResp) 359 | if err != nil { 360 | return roleResp, errors.Wrap(err, "Error unmarshalling space roles") 361 | } 362 | return roleResp, nil 363 | } 364 | 365 | func (c *Client) handleSpaceResp(resp *http.Response) (Space, error) { 366 | body, err := ioutil.ReadAll(resp.Body) 367 | defer resp.Body.Close() 368 | if err != nil { 369 | return Space{}, err 370 | } 371 | var spaceResource SpaceResource 372 | err = json.Unmarshal(body, &spaceResource) 373 | if err != nil { 374 | return Space{}, err 375 | } 376 | space := spaceResource.Entity 377 | space.Guid = spaceResource.Meta.Guid 378 | space.c = c 379 | return space, nil 380 | } 381 | -------------------------------------------------------------------------------- /orgs_test.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestListOrgs(t *testing.T) { 10 | Convey("List Org", t, func() { 11 | mocks := []MockRoute{ 12 | {"GET", "/v2/organizations", listOrgsPayload, "", 200, ""}, 13 | {"GET", "/v2/orgsPage2", listOrgsPayloadPage2, "", 200, ""}, 14 | } 15 | setupMultiple(mocks, t) 16 | defer teardown() 17 | c := &Config{ 18 | ApiAddress: server.URL, 19 | Token: "foobar", 20 | } 21 | client, err := NewClient(c) 22 | So(err, ShouldBeNil) 23 | 24 | orgs, err := client.ListOrgs() 25 | So(err, ShouldBeNil) 26 | 27 | So(len(orgs), ShouldEqual, 4) 28 | So(orgs[0].Guid, ShouldEqual, "a537761f-9d93-4b30-af17-3d73dbca181b") 29 | So(orgs[0].Name, ShouldEqual, "demo") 30 | }) 31 | } 32 | 33 | func TestGetOrgByGuid(t *testing.T) { 34 | Convey("List Org", t, func() { 35 | setup(MockRoute{"GET", "/v2/organizations/1c0e6074-777f-450e-9abc-c42f39d9b75b", orgByGuidPayload, "", 200, ""}, t) 36 | defer teardown() 37 | c := &Config{ 38 | ApiAddress: server.URL, 39 | Token: "foobar", 40 | } 41 | client, err := NewClient(c) 42 | So(err, ShouldBeNil) 43 | 44 | org, err := client.GetOrgByGuid("1c0e6074-777f-450e-9abc-c42f39d9b75b") 45 | So(err, ShouldBeNil) 46 | 47 | So(org.Guid, ShouldEqual, "1c0e6074-777f-450e-9abc-c42f39d9b75b") 48 | So(org.Name, ShouldEqual, "name-1716") 49 | }) 50 | } 51 | 52 | func TestOrgSpaces(t *testing.T) { 53 | Convey("Get spaces by org", t, func() { 54 | setup(MockRoute{"GET", "/v2/organizations/foo/spaces", orgSpacesPayload, "", 200, ""}, t) 55 | defer teardown() 56 | c := &Config{ 57 | ApiAddress: server.URL, 58 | Token: "foobar", 59 | } 60 | client, err := NewClient(c) 61 | So(err, ShouldBeNil) 62 | 63 | spaces, err := client.OrgSpaces("foo") 64 | So(err, ShouldBeNil) 65 | 66 | So(len(spaces), ShouldEqual, 1) 67 | So(spaces[0].Guid, ShouldEqual, "b8aff561-175d-45e8-b1e7-67e2aedb03b6") 68 | So(spaces[0].Name, ShouldEqual, "test") 69 | }) 70 | } 71 | 72 | func TestOrgSummary(t *testing.T) { 73 | Convey("Get org summary", t, func() { 74 | setup(MockRoute{"GET", "/v2/organizations/06dcedd4-1f24-49a6-adc1-cce9131a1b2c/summary", orgSummaryPayload, "", 200, ""}, t) 75 | defer teardown() 76 | c := &Config{ 77 | ApiAddress: server.URL, 78 | Token: "foobar", 79 | } 80 | client, err := NewClient(c) 81 | So(err, ShouldBeNil) 82 | 83 | org := &Org{ 84 | Guid: "06dcedd4-1f24-49a6-adc1-cce9131a1b2c", 85 | c: client, 86 | } 87 | summary, err := org.Summary() 88 | So(err, ShouldBeNil) 89 | 90 | So(summary.Guid, ShouldEqual, "06dcedd4-1f24-49a6-adc1-cce9131a1b2c") 91 | So(summary.Name, ShouldEqual, "system") 92 | So(summary.Status, ShouldEqual, "active") 93 | 94 | spaces := summary.Spaces 95 | So(len(spaces), ShouldEqual, 1) 96 | So(spaces[0].Guid, ShouldEqual, "494d8b64-8181-4183-a6d3-6279db8fec6e") 97 | So(spaces[0].Name, ShouldEqual, "test") 98 | So(spaces[0].ServiceCount, ShouldEqual, 1) 99 | So(spaces[0].AppCount, ShouldEqual, 2) 100 | So(spaces[0].MemDevTotal, ShouldEqual, 32) 101 | So(spaces[0].MemProdTotal, ShouldEqual, 64) 102 | }) 103 | } 104 | 105 | func TestOrgQuota(t *testing.T) { 106 | Convey("Get org quota", t, func() { 107 | setup(MockRoute{"GET", "/v2/quota_definitions/a537761f-9d93-4b30-af17-3d73dbca181b", orgQuotaPayload, "", 200, ""}, t) 108 | defer teardown() 109 | c := &Config{ 110 | ApiAddress: server.URL, 111 | Token: "foobar", 112 | } 113 | client, err := NewClient(c) 114 | So(err, ShouldBeNil) 115 | 116 | org := &Org{ 117 | QuotaDefinitionGuid: "a537761f-9d93-4b30-af17-3d73dbca181b", 118 | c: client, 119 | } 120 | orgQuota, err := org.Quota() 121 | So(err, ShouldBeNil) 122 | 123 | So(orgQuota.Guid, ShouldEqual, "a537761f-9d93-4b30-af17-3d73dbca181b") 124 | So(orgQuota.Name, ShouldEqual, "test-2") 125 | So(orgQuota.NonBasicServicesAllowed, ShouldEqual, false) 126 | So(orgQuota.TotalServices, ShouldEqual, 10) 127 | So(orgQuota.TotalRoutes, ShouldEqual, 20) 128 | So(orgQuota.TotalPrivateDomains, ShouldEqual, 30) 129 | So(orgQuota.MemoryLimit, ShouldEqual, 40) 130 | So(orgQuota.TrialDBAllowed, ShouldEqual, true) 131 | So(orgQuota.InstanceMemoryLimit, ShouldEqual, 50) 132 | So(orgQuota.AppInstanceLimit, ShouldEqual, 60) 133 | So(orgQuota.AppTaskLimit, ShouldEqual, 70) 134 | So(orgQuota.TotalServiceKeys, ShouldEqual, 80) 135 | So(orgQuota.TotalReservedRoutePorts, ShouldEqual, 90) 136 | }) 137 | } 138 | 139 | func TestCreateOrg(t *testing.T) { 140 | Convey("Create org", t, func() { 141 | setup(MockRoute{"POST", "/v2/organizations", createOrgPayload, "", 201, ""}, t) 142 | defer teardown() 143 | c := &Config{ 144 | ApiAddress: server.URL, 145 | Token: "foobar", 146 | } 147 | client, err := NewClient(c) 148 | So(err, ShouldBeNil) 149 | 150 | org, err := client.CreateOrg(OrgRequest{Name: "my-org"}) 151 | So(err, ShouldBeNil) 152 | So(org.Guid, ShouldEqual, "22b3b0a0-6511-47e5-8f7a-93bbd2ff446e") 153 | }) 154 | } 155 | 156 | func TestDeleteOrg(t *testing.T) { 157 | Convey("Delete org", t, func() { 158 | setup(MockRoute{"DELETE", "/v2/organizations/a537761f-9d93-4b30-af17-3d73dbca181b", "", "", 204, ""}, t) 159 | defer teardown() 160 | c := &Config{ 161 | ApiAddress: server.URL, 162 | Token: "foobar", 163 | } 164 | client, err := NewClient(c) 165 | So(err, ShouldBeNil) 166 | 167 | err = client.DeleteOrg("a537761f-9d93-4b30-af17-3d73dbca181b") 168 | So(err, ShouldBeNil) 169 | }) 170 | } 171 | 172 | func TestAssociateManager(t *testing.T) { 173 | Convey("Associate manager", t, func() { 174 | setup(MockRoute{"PUT", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/managers/user-guid", associateOrgManagerPayload, "", 201, ""}, t) 175 | defer teardown() 176 | c := &Config{ 177 | ApiAddress: server.URL, 178 | Token: "foobar", 179 | } 180 | client, err := NewClient(c) 181 | So(err, ShouldBeNil) 182 | 183 | org := &Org{ 184 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 185 | c: client, 186 | } 187 | 188 | newOrg, err := org.AssociateManager("user-guid") 189 | So(err, ShouldBeNil) 190 | So(newOrg.Guid, ShouldEqual, "bc7b4caf-f4b8-4d85-b126-0729b9351e56") 191 | }) 192 | } 193 | 194 | func TestAssociateAuditor(t *testing.T) { 195 | Convey("Associate auditor", t, func() { 196 | setup(MockRoute{"PUT", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/auditors/user-guid", associateOrgAuditorPayload, "", 201, ""}, t) 197 | defer teardown() 198 | c := &Config{ 199 | ApiAddress: server.URL, 200 | Token: "foobar", 201 | } 202 | client, err := NewClient(c) 203 | So(err, ShouldBeNil) 204 | 205 | org := &Org{ 206 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 207 | c: client, 208 | } 209 | 210 | newOrg, err := org.AssociateAuditor("user-guid") 211 | So(err, ShouldBeNil) 212 | So(newOrg.Guid, ShouldEqual, "bc7b4caf-f4b8-4d85-b126-0729b9351e56") 213 | }) 214 | } 215 | 216 | func TestAssociateUser(t *testing.T) { 217 | Convey("Associate user", t, func() { 218 | setup(MockRoute{"PUT", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/users/user-guid", associateOrgUserPayload, "", 201, ""}, t) 219 | defer teardown() 220 | c := &Config{ 221 | ApiAddress: server.URL, 222 | Token: "foobar", 223 | } 224 | client, err := NewClient(c) 225 | So(err, ShouldBeNil) 226 | 227 | org := &Org{ 228 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 229 | c: client, 230 | } 231 | 232 | newOrg, err := org.AssociateUser("user-guid") 233 | So(err, ShouldBeNil) 234 | So(newOrg.Guid, ShouldEqual, "bc7b4caf-f4b8-4d85-b126-0729b9351e56") 235 | }) 236 | } 237 | 238 | func TestAssociateManagerByUsername(t *testing.T) { 239 | Convey("Associate manager by username", t, func() { 240 | setup(MockRoute{"PUT", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/managers", associateOrgManagerPayload, "", 201, ""}, t) 241 | defer teardown() 242 | c := &Config{ 243 | ApiAddress: server.URL, 244 | Token: "foobar", 245 | } 246 | client, err := NewClient(c) 247 | So(err, ShouldBeNil) 248 | 249 | org := &Org{ 250 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 251 | c: client, 252 | } 253 | 254 | newOrg, err := org.AssociateManagerByUsername("user-name") 255 | So(err, ShouldBeNil) 256 | So(newOrg.Guid, ShouldEqual, "bc7b4caf-f4b8-4d85-b126-0729b9351e56") 257 | }) 258 | } 259 | 260 | func TestAssociateAuditorByUsername(t *testing.T) { 261 | Convey("Associate auditor by username", t, func() { 262 | setup(MockRoute{"PUT", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/auditors", associateOrgAuditorPayload, "", 201, ""}, t) 263 | defer teardown() 264 | c := &Config{ 265 | ApiAddress: server.URL, 266 | Token: "foobar", 267 | } 268 | client, err := NewClient(c) 269 | So(err, ShouldBeNil) 270 | 271 | org := &Org{ 272 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 273 | c: client, 274 | } 275 | 276 | newOrg, err := org.AssociateAuditorByUsername("user-name") 277 | So(err, ShouldBeNil) 278 | So(newOrg.Guid, ShouldEqual, "bc7b4caf-f4b8-4d85-b126-0729b9351e56") 279 | }) 280 | } 281 | 282 | func TestAssociateUserByUsername(t *testing.T) { 283 | Convey("Associate user by username", t, func() { 284 | setup(MockRoute{"PUT", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/users", associateOrgUserPayload, "", 201, ""}, t) 285 | defer teardown() 286 | c := &Config{ 287 | ApiAddress: server.URL, 288 | Token: "foobar", 289 | } 290 | client, err := NewClient(c) 291 | So(err, ShouldBeNil) 292 | 293 | org := &Org{ 294 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 295 | c: client, 296 | } 297 | 298 | newOrg, err := org.AssociateUserByUsername("user-name") 299 | So(err, ShouldBeNil) 300 | So(newOrg.Guid, ShouldEqual, "bc7b4caf-f4b8-4d85-b126-0729b9351e56") 301 | }) 302 | } 303 | 304 | func TestRemoveManager(t *testing.T) { 305 | Convey("Remove manager", t, func() { 306 | setup(MockRoute{"DELETE", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/managers/user-guid", "", "", 204, ""}, t) 307 | defer teardown() 308 | c := &Config{ 309 | ApiAddress: server.URL, 310 | Token: "foobar", 311 | } 312 | client, err := NewClient(c) 313 | So(err, ShouldBeNil) 314 | 315 | org := &Org{ 316 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 317 | c: client, 318 | } 319 | 320 | err = org.RemoveManager("user-guid") 321 | So(err, ShouldBeNil) 322 | }) 323 | } 324 | 325 | func TestRemoveAuditor(t *testing.T) { 326 | Convey("Remove auditor", t, func() { 327 | setup(MockRoute{"DELETE", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/auditors/user-guid", "", "", 204, ""}, t) 328 | defer teardown() 329 | c := &Config{ 330 | ApiAddress: server.URL, 331 | Token: "foobar", 332 | } 333 | client, err := NewClient(c) 334 | So(err, ShouldBeNil) 335 | 336 | org := &Org{ 337 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 338 | c: client, 339 | } 340 | 341 | err = org.RemoveAuditor("user-guid") 342 | So(err, ShouldBeNil) 343 | }) 344 | } 345 | 346 | func TestRemoveUser(t *testing.T) { 347 | Convey("Remove user", t, func() { 348 | setup(MockRoute{"DELETE", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/users/user-guid", "", "", 204, ""}, t) 349 | defer teardown() 350 | c := &Config{ 351 | ApiAddress: server.URL, 352 | Token: "foobar", 353 | } 354 | client, err := NewClient(c) 355 | So(err, ShouldBeNil) 356 | 357 | org := &Org{ 358 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 359 | c: client, 360 | } 361 | 362 | err = org.RemoveUser("user-guid") 363 | So(err, ShouldBeNil) 364 | }) 365 | } 366 | 367 | func TestRemoveManagerByUsername(t *testing.T) { 368 | Convey("Remove manager by username", t, func() { 369 | setup(MockRoute{"DELETE", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/managers", "", "", 204, ""}, t) 370 | defer teardown() 371 | c := &Config{ 372 | ApiAddress: server.URL, 373 | Token: "foobar", 374 | } 375 | client, err := NewClient(c) 376 | So(err, ShouldBeNil) 377 | 378 | org := &Org{ 379 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 380 | c: client, 381 | } 382 | 383 | err = org.RemoveManagerByUsername("user-name") 384 | So(err, ShouldBeNil) 385 | }) 386 | } 387 | 388 | func TestRemoveAuditorByUsername(t *testing.T) { 389 | Convey("Remove auditor by username", t, func() { 390 | setup(MockRoute{"DELETE", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/auditors", "", "", 204, ""}, t) 391 | defer teardown() 392 | c := &Config{ 393 | ApiAddress: server.URL, 394 | Token: "foobar", 395 | } 396 | client, err := NewClient(c) 397 | So(err, ShouldBeNil) 398 | 399 | org := &Org{ 400 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 401 | c: client, 402 | } 403 | 404 | err = org.RemoveAuditorByUsername("user-name") 405 | So(err, ShouldBeNil) 406 | }) 407 | } 408 | 409 | func TestRemoveUserByUsername(t *testing.T) { 410 | Convey("Remove user by username", t, func() { 411 | setup(MockRoute{"DELETE", "/v2/organizations/bc7b4caf-f4b8-4d85-b126-0729b9351e56/users", "", "", 204, ""}, t) 412 | defer teardown() 413 | c := &Config{ 414 | ApiAddress: server.URL, 415 | Token: "foobar", 416 | } 417 | client, err := NewClient(c) 418 | So(err, ShouldBeNil) 419 | 420 | org := &Org{ 421 | Guid: "bc7b4caf-f4b8-4d85-b126-0729b9351e56", 422 | c: client, 423 | } 424 | 425 | err = org.RemoveUserByUsername("user-name") 426 | So(err, ShouldBeNil) 427 | }) 428 | } 429 | -------------------------------------------------------------------------------- /orgs.go: -------------------------------------------------------------------------------- 1 | package cfclient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type OrgResponse struct { 16 | Count int `json:"total_results"` 17 | Pages int `json:"total_pages"` 18 | NextUrl string `json:"next_url"` 19 | Resources []OrgResource `json:"resources"` 20 | } 21 | 22 | type OrgResource struct { 23 | Meta Meta `json:"metadata"` 24 | Entity Org `json:"entity"` 25 | } 26 | 27 | type Org struct { 28 | Guid string `json:"guid"` 29 | Name string `json:"name"` 30 | QuotaDefinitionGuid string `json:"quota_definition_guid"` 31 | c *Client 32 | } 33 | 34 | type OrgSummary struct { 35 | Guid string `json:"guid"` 36 | Name string `json:"name"` 37 | Status string `json:"status"` 38 | Spaces []OrgSummarySpaces `json:"spaces"` 39 | } 40 | 41 | type OrgSummarySpaces struct { 42 | Guid string `json:"guid"` 43 | Name string `json:"name"` 44 | ServiceCount int `json:"service_count"` 45 | AppCount int `json:"app_count"` 46 | MemDevTotal int `json:"mem_dev_total"` 47 | MemProdTotal int `json:"mem_prod_total"` 48 | } 49 | 50 | type OrgRequest struct { 51 | Name string `json:"name"` 52 | Status string `json:"status,omitempty"` 53 | QuotaDefinitionGuid string `json:"quota_definition_guid,omitempty"` 54 | } 55 | 56 | func (c *Client) ListOrgsByQuery(query url.Values) ([]Org, error) { 57 | var orgs []Org 58 | requestUrl := "/v2/organizations?" + query.Encode() 59 | for { 60 | orgResp, err := c.getOrgResponse(requestUrl) 61 | if err != nil { 62 | return []Org{}, err 63 | } 64 | for _, org := range orgResp.Resources { 65 | org.Entity.Guid = org.Meta.Guid 66 | org.Entity.c = c 67 | orgs = append(orgs, org.Entity) 68 | } 69 | requestUrl = orgResp.NextUrl 70 | if requestUrl == "" { 71 | break 72 | } 73 | } 74 | return orgs, nil 75 | } 76 | 77 | func (c *Client) ListOrgs() ([]Org, error) { 78 | return c.ListOrgsByQuery(nil) 79 | } 80 | 81 | func (c *Client) GetOrgByName(name string) (Org, error) { 82 | var org Org 83 | q := url.Values{} 84 | q.Set("q", "name:"+name) 85 | orgs, err := c.ListOrgsByQuery(q) 86 | if err != nil { 87 | return org, err 88 | } 89 | if len(orgs) == 0 { 90 | return org, fmt.Errorf("Unable to find org %s", name) 91 | } 92 | return orgs[0], nil 93 | } 94 | 95 | func (c *Client) GetOrgByGuid(guid string) (Org, error) { 96 | var orgRes OrgResource 97 | r := c.NewRequest("GET", "/v2/organizations/"+guid) 98 | resp, err := c.DoRequest(r) 99 | if err != nil { 100 | return Org{}, err 101 | } 102 | body, err := ioutil.ReadAll(resp.Body) 103 | defer resp.Body.Close() 104 | if err != nil { 105 | return Org{}, err 106 | } 107 | err = json.Unmarshal(body, &orgRes) 108 | if err != nil { 109 | return Org{}, err 110 | } 111 | orgRes.Entity.Guid = orgRes.Meta.Guid 112 | orgRes.Entity.c = c 113 | return orgRes.Entity, nil 114 | } 115 | 116 | func (c *Client) getOrgResponse(requestUrl string) (OrgResponse, error) { 117 | var orgResp OrgResponse 118 | r := c.NewRequest("GET", requestUrl) 119 | resp, err := c.DoRequest(r) 120 | if err != nil { 121 | return OrgResponse{}, errors.Wrap(err, "Error requesting orgs") 122 | } 123 | resBody, err := ioutil.ReadAll(resp.Body) 124 | defer resp.Body.Close() 125 | if err != nil { 126 | return OrgResponse{}, errors.Wrap(err, "Error reading org request") 127 | } 128 | err = json.Unmarshal(resBody, &orgResp) 129 | if err != nil { 130 | return OrgResponse{}, errors.Wrap(err, "Error unmarshalling org") 131 | } 132 | return orgResp, nil 133 | } 134 | 135 | func (c *Client) OrgSpaces(guid string) ([]Space, error) { 136 | var spaces []Space 137 | var spaceResp SpaceResponse 138 | path := fmt.Sprintf("/v2/organizations/%s/spaces", guid) 139 | r := c.NewRequest("GET", path) 140 | resp, err := c.DoRequest(r) 141 | if err != nil { 142 | return nil, errors.Wrap(err, "Error requesting space") 143 | } 144 | resBody, err := ioutil.ReadAll(resp.Body) 145 | if err != nil { 146 | log.Printf("Error reading space request %v", resBody) 147 | } 148 | 149 | err = json.Unmarshal(resBody, &spaceResp) 150 | if err != nil { 151 | return nil, errors.Wrap(err, "Error space organization") 152 | } 153 | for _, space := range spaceResp.Resources { 154 | space.Entity.Guid = space.Meta.Guid 155 | space.Entity.c = c 156 | spaces = append(spaces, space.Entity) 157 | } 158 | 159 | return spaces, nil 160 | } 161 | 162 | func (o *Org) Summary() (OrgSummary, error) { 163 | var orgSummary OrgSummary 164 | requestUrl := fmt.Sprintf("/v2/organizations/%s/summary", o.Guid) 165 | r := o.c.NewRequest("GET", requestUrl) 166 | resp, err := o.c.DoRequest(r) 167 | if err != nil { 168 | return OrgSummary{}, errors.Wrap(err, "Error requesting org summary") 169 | } 170 | resBody, err := ioutil.ReadAll(resp.Body) 171 | defer resp.Body.Close() 172 | if err != nil { 173 | return OrgSummary{}, errors.Wrap(err, "Error reading org summary body") 174 | } 175 | err = json.Unmarshal(resBody, &orgSummary) 176 | if err != nil { 177 | return OrgSummary{}, errors.Wrap(err, "Error unmarshalling org summary") 178 | } 179 | return orgSummary, nil 180 | } 181 | 182 | func (o *Org) Quota() (*OrgQuota, error) { 183 | var orgQuota *OrgQuota 184 | var orgQuotaResource OrgQuotasResource 185 | if o.QuotaDefinitionGuid == "" { 186 | return nil, nil 187 | } 188 | requestUrl := fmt.Sprintf("/v2/quota_definitions/%s", o.QuotaDefinitionGuid) 189 | r := o.c.NewRequest("GET", requestUrl) 190 | resp, err := o.c.DoRequest(r) 191 | if err != nil { 192 | return &OrgQuota{}, errors.Wrap(err, "Error requesting org quota") 193 | } 194 | resBody, err := ioutil.ReadAll(resp.Body) 195 | defer resp.Body.Close() 196 | if err != nil { 197 | return &OrgQuota{}, errors.Wrap(err, "Error reading org quota body") 198 | } 199 | err = json.Unmarshal(resBody, &orgQuotaResource) 200 | if err != nil { 201 | return &OrgQuota{}, errors.Wrap(err, "Error unmarshalling org quota") 202 | } 203 | orgQuota = &orgQuotaResource.Entity 204 | orgQuota.Guid = orgQuotaResource.Meta.Guid 205 | orgQuota.c = o.c 206 | return orgQuota, nil 207 | } 208 | 209 | func (c *Client) AssociateOrgManager(orgGUID, userGUID string) (Org, error) { 210 | org := Org{Guid: orgGUID, c: c} 211 | return org.AssociateManager(userGUID) 212 | } 213 | 214 | func (c *Client) AssociateOrgManagerByUsername(orgGUID, name string) (Org, error) { 215 | org := Org{Guid: orgGUID, c: c} 216 | return org.AssociateManagerByUsername(name) 217 | } 218 | 219 | func (c *Client) AssociateOrgUser(orgGUID, userGUID string) (Org, error) { 220 | org := Org{Guid: orgGUID, c: c} 221 | return org.AssociateUser(userGUID) 222 | } 223 | 224 | func (c *Client) AssociateOrgAuditor(orgGUID, userGUID string) (Org, error) { 225 | org := Org{Guid: orgGUID, c: c} 226 | return org.AssociateAuditor(userGUID) 227 | } 228 | 229 | func (c *Client) AssociateOrgUserByUsername(orgGUID, name string) (Org, error) { 230 | org := Org{Guid: orgGUID, c: c} 231 | return org.AssociateUserByUsername(name) 232 | } 233 | 234 | func (c *Client) AssociateOrgAuditorByUsername(orgGUID, name string) (Org, error) { 235 | org := Org{Guid: orgGUID, c: c} 236 | return org.AssociateAuditorByUsername(name) 237 | } 238 | 239 | func (c *Client) RemoveOrgManager(orgGUID, userGUID string) error { 240 | org := Org{Guid: orgGUID, c: c} 241 | return org.RemoveManager(userGUID) 242 | } 243 | 244 | func (c *Client) RemoveOrgManagerByUsername(orgGUID, name string) error { 245 | org := Org{Guid: orgGUID, c: c} 246 | return org.RemoveManagerByUsername(name) 247 | } 248 | 249 | func (c *Client) RemoveOrgUser(orgGUID, userGUID string) error { 250 | org := Org{Guid: orgGUID, c: c} 251 | return org.RemoveUser(userGUID) 252 | } 253 | 254 | func (c *Client) RemoveOrgAuditor(orgGUID, userGUID string) error { 255 | org := Org{Guid: orgGUID, c: c} 256 | return org.RemoveAuditor(userGUID) 257 | } 258 | 259 | func (c *Client) RemoveOrgUserByUsername(orgGUID, name string) error { 260 | org := Org{Guid: orgGUID, c: c} 261 | return org.RemoveUserByUsername(name) 262 | } 263 | 264 | func (c *Client) RemoveOrgAuditorByUsername(orgGUID, name string) error { 265 | org := Org{Guid: orgGUID, c: c} 266 | return org.RemoveAuditorByUsername(name) 267 | } 268 | 269 | func (o *Org) AssociateManager(userGUID string) (Org, error) { 270 | requestUrl := fmt.Sprintf("/v2/organizations/%s/managers/%s", o.Guid, userGUID) 271 | r := o.c.NewRequest("PUT", requestUrl) 272 | resp, err := o.c.DoRequest(r) 273 | if err != nil { 274 | return Org{}, err 275 | } 276 | if resp.StatusCode != http.StatusCreated { 277 | return Org{}, errors.Wrapf(err, "Error associating manager %s, response code: %d", userGUID, resp.StatusCode) 278 | } 279 | return o.c.handleOrgResp(resp) 280 | } 281 | 282 | func (o *Org) AssociateManagerByUsername(name string) (Org, error) { 283 | requestUrl := fmt.Sprintf("/v2/organizations/%s/managers", o.Guid) 284 | buf := bytes.NewBuffer(nil) 285 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 286 | if err != nil { 287 | return Org{}, err 288 | } 289 | r := o.c.NewRequestWithBody("PUT", requestUrl, buf) 290 | resp, err := o.c.DoRequest(r) 291 | if err != nil { 292 | return Org{}, err 293 | } 294 | if resp.StatusCode != http.StatusCreated { 295 | return Org{}, errors.Wrapf(err, "Error associating manager %s, response code: %d", name, resp.StatusCode) 296 | } 297 | return o.c.handleOrgResp(resp) 298 | } 299 | 300 | func (o *Org) AssociateUser(userGUID string) (Org, error) { 301 | requestUrl := fmt.Sprintf("/v2/organizations/%s/users/%s", o.Guid, userGUID) 302 | r := o.c.NewRequest("PUT", requestUrl) 303 | resp, err := o.c.DoRequest(r) 304 | if err != nil { 305 | return Org{}, err 306 | } 307 | if resp.StatusCode != http.StatusCreated { 308 | return Org{}, errors.Wrapf(err, "Error associating user %s, response code: %d", userGUID, resp.StatusCode) 309 | } 310 | return o.c.handleOrgResp(resp) 311 | } 312 | 313 | func (o *Org) AssociateAuditor(userGUID string) (Org, error) { 314 | requestUrl := fmt.Sprintf("/v2/organizations/%s/auditors/%s", o.Guid, userGUID) 315 | r := o.c.NewRequest("PUT", requestUrl) 316 | resp, err := o.c.DoRequest(r) 317 | if err != nil { 318 | return Org{}, err 319 | } 320 | if resp.StatusCode != http.StatusCreated { 321 | return Org{}, errors.Wrapf(err, "Error associating auditor %s, response code: %d", userGUID, resp.StatusCode) 322 | } 323 | return o.c.handleOrgResp(resp) 324 | } 325 | 326 | func (o *Org) AssociateUserByUsername(name string) (Org, error) { 327 | requestUrl := fmt.Sprintf("/v2/organizations/%s/users", o.Guid) 328 | buf := bytes.NewBuffer(nil) 329 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 330 | if err != nil { 331 | return Org{}, err 332 | } 333 | r := o.c.NewRequestWithBody("PUT", requestUrl, buf) 334 | resp, err := o.c.DoRequest(r) 335 | if err != nil { 336 | return Org{}, err 337 | } 338 | if resp.StatusCode != http.StatusCreated { 339 | return Org{}, errors.Wrapf(err, "Error associating user %s, response code: %d", name, resp.StatusCode) 340 | } 341 | return o.c.handleOrgResp(resp) 342 | } 343 | 344 | func (o *Org) AssociateAuditorByUsername(name string) (Org, error) { 345 | requestUrl := fmt.Sprintf("/v2/organizations/%s/auditors", o.Guid) 346 | buf := bytes.NewBuffer(nil) 347 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 348 | if err != nil { 349 | return Org{}, err 350 | } 351 | r := o.c.NewRequestWithBody("PUT", requestUrl, buf) 352 | resp, err := o.c.DoRequest(r) 353 | if err != nil { 354 | return Org{}, err 355 | } 356 | if resp.StatusCode != http.StatusCreated { 357 | return Org{}, errors.Wrapf(err, "Error associating auditor %s, response code: %d", name, resp.StatusCode) 358 | } 359 | return o.c.handleOrgResp(resp) 360 | } 361 | 362 | func (o *Org) RemoveManager(userGUID string) error { 363 | requestUrl := fmt.Sprintf("/v2/organizations/%s/managers/%s", o.Guid, userGUID) 364 | r := o.c.NewRequest("DELETE", requestUrl) 365 | resp, err := o.c.DoRequest(r) 366 | if err != nil { 367 | return err 368 | } 369 | if resp.StatusCode != http.StatusNoContent { 370 | return errors.Wrapf(err, "Error removing manager %s, response code: %d", userGUID, resp.StatusCode) 371 | } 372 | return nil 373 | } 374 | 375 | func (o *Org) RemoveManagerByUsername(name string) error { 376 | requestUrl := fmt.Sprintf("/v2/organizations/%s/managers", o.Guid) 377 | buf := bytes.NewBuffer(nil) 378 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 379 | if err != nil { 380 | return err 381 | } 382 | r := o.c.NewRequestWithBody("DELETE", requestUrl, buf) 383 | resp, err := o.c.DoRequest(r) 384 | if err != nil { 385 | return err 386 | } 387 | if resp.StatusCode != http.StatusNoContent { 388 | return errors.Wrapf(err, "Error removing manager %s, response code: %d", name, resp.StatusCode) 389 | } 390 | return nil 391 | } 392 | 393 | func (o *Org) RemoveUser(userGUID string) error { 394 | requestUrl := fmt.Sprintf("/v2/organizations/%s/users/%s", o.Guid, userGUID) 395 | r := o.c.NewRequest("DELETE", requestUrl) 396 | resp, err := o.c.DoRequest(r) 397 | if err != nil { 398 | return err 399 | } 400 | if resp.StatusCode != http.StatusNoContent { 401 | return errors.Wrapf(err, "Error removing user %s, response code: %d", userGUID, resp.StatusCode) 402 | } 403 | return nil 404 | } 405 | 406 | func (o *Org) RemoveAuditor(userGUID string) error { 407 | requestUrl := fmt.Sprintf("/v2/organizations/%s/auditors/%s", o.Guid, userGUID) 408 | r := o.c.NewRequest("DELETE", requestUrl) 409 | resp, err := o.c.DoRequest(r) 410 | if err != nil { 411 | return err 412 | } 413 | if resp.StatusCode != http.StatusNoContent { 414 | return errors.Wrapf(err, "Error removing auditor %s, response code: %d", userGUID, resp.StatusCode) 415 | } 416 | return nil 417 | } 418 | 419 | func (o *Org) RemoveUserByUsername(name string) error { 420 | requestUrl := fmt.Sprintf("/v2/organizations/%s/users", o.Guid) 421 | buf := bytes.NewBuffer(nil) 422 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 423 | if err != nil { 424 | return err 425 | } 426 | r := o.c.NewRequestWithBody("DELETE", requestUrl, buf) 427 | resp, err := o.c.DoRequest(r) 428 | if err != nil { 429 | return err 430 | } 431 | if resp.StatusCode != http.StatusNoContent { 432 | return errors.Wrapf(err, "Error removing user %s, response code: %d", name, resp.StatusCode) 433 | } 434 | return nil 435 | } 436 | 437 | func (o *Org) RemoveAuditorByUsername(name string) error { 438 | requestUrl := fmt.Sprintf("/v2/organizations/%s/auditors", o.Guid) 439 | buf := bytes.NewBuffer(nil) 440 | err := json.NewEncoder(buf).Encode(map[string]string{"username": name}) 441 | if err != nil { 442 | return err 443 | } 444 | r := o.c.NewRequestWithBody("DELETE", requestUrl, buf) 445 | resp, err := o.c.DoRequest(r) 446 | if err != nil { 447 | return err 448 | } 449 | if resp.StatusCode != http.StatusNoContent { 450 | return errors.Wrapf(err, "Error removing auditor %s, response code: %d", name, resp.StatusCode) 451 | } 452 | return nil 453 | } 454 | 455 | func (c *Client) CreateOrg(req OrgRequest) (Org, error) { 456 | buf := bytes.NewBuffer(nil) 457 | err := json.NewEncoder(buf).Encode(req) 458 | if err != nil { 459 | return Org{}, err 460 | } 461 | r := c.NewRequestWithBody("POST", "/v2/organizations", buf) 462 | resp, err := c.DoRequest(r) 463 | if err != nil { 464 | return Org{}, err 465 | } 466 | if resp.StatusCode != http.StatusCreated { 467 | return Org{}, errors.Wrapf(err, "Error creating organization, response code: %d", resp.StatusCode) 468 | } 469 | return c.handleOrgResp(resp) 470 | } 471 | 472 | func (c *Client) DeleteOrg(guid string) error { 473 | resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/organizations/%s", guid))) 474 | if err != nil { 475 | return err 476 | } 477 | if resp.StatusCode != http.StatusNoContent { 478 | return errors.Wrapf(err, "Error deleting organization %s, response code: %d", guid, resp.StatusCode) 479 | } 480 | return nil 481 | } 482 | 483 | func (c *Client) handleOrgResp(resp *http.Response) (Org, error) { 484 | body, err := ioutil.ReadAll(resp.Body) 485 | defer resp.Body.Close() 486 | if err != nil { 487 | return Org{}, err 488 | } 489 | var orgResource OrgResource 490 | err = json.Unmarshal(body, &orgResource) 491 | if err != nil { 492 | return Org{}, err 493 | } 494 | org := orgResource.Entity 495 | org.Guid = orgResource.Meta.Guid 496 | org.c = c 497 | return org, nil 498 | } 499 | --------------------------------------------------------------------------------