diff --git a/README.md b/README.md index 357a274f..84788de5 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,30 @@ # go-cfclient -[![build workflow](https://github.com/cloudfoundry-community/go-cfclient/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/cloudfoundry-community/go-cfclient/actions/workflows/build.yml) -[![GoDoc](https://godoc.org/github.com/cloudfoundry-community/go-cfclient?status.svg)](http://godoc.org/github.com/cloudfoundry-community/go-cfclient) -[![Report card](https://goreportcard.com/badge/github.com/cloudfoundry-community/go-cfclient)](https://goreportcard.com/report/github.com/cloudfoundry-community/go-cfclient) - ## Overview +`cfclient` is a package to assist you in writing apps that need to interact with the [Cloud Foundry](http://cloudfoundry.org) +v2 cloud controller API. -`cfclient` is a package to assist you in writing apps that need to interact with [Cloud Foundry](http://cloudfoundry.org). -It provides functions and structures to retrieve and update - +## v2 go-cfclient deprecated +The v2 version of the client and corresponding v2 cloud controller (CC) API is deprecated. Please start using the v3 version of this +client and CC API. This v2 branch is only kept around to support critical bug fixes. -## Usage +## Upgrading the v2 go-cfclient +If you're currently using an old version of the go-cfclient and need to upgrade to the latest version that still +supports the v2 CC API, then you'll need to go get the "new" v2 module. -``` -go get github.com/cloudfoundry-community/go-cfclient +```shell +$ go get -u github.com/cloudfoundry-community/go-cfclient/v2 ``` -NOTE: Currently this project is not versioning its releases and so breaking changes might be introduced. -Whilst hopefully notifications of breaking changes are made via commit messages, ideally your project will use a local -vendoring system to lock in a version of `go-cfclient` that is known to work for you. -This will allow you to control the timing and maintenance of upgrades to newer versions of this library. +Update your go import statements as necessary in your go source files, then finally: +```shell +$ go mod tidy +``` +## Usage +``` +go get github.com/cloudfoundry-community/go-cfclient/v2 +``` Some example code: ```go @@ -29,7 +33,7 @@ package main import ( "fmt" - "github.com/cloudfoundry-community/go-cfclient" + "github.com/cloudfoundry-community/go-cfclient/v2" ) func main() { @@ -108,6 +112,3 @@ To do this, simply use Go to regenerate the code: make generate ``` -## Contributing - -Pull requests welcome. Please ensure you run all the unit tests, go fmt the code, and golangci-lint via `make all` diff --git a/client.go b/client.go index 8c9d8c38..951fb0c4 100644 --- a/client.go +++ b/client.go @@ -370,6 +370,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) { } func (c *Client) handleError(resp *http.Response) (*http.Response, error) { + defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -379,7 +380,6 @@ func (c *Client) handleError(resp *http.Response) (*http.Response, error) { Body: body, } } - defer resp.Body.Close() // Unmarshal V2 error response if strings.HasPrefix(resp.Request.URL.Path, "/v2/") { diff --git a/go.mod b/go.mod index 8ad31060..50fd79ff 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/cloudfoundry-community/go-cfclient +module github.com/cloudfoundry-community/go-cfclient/v2 go 1.15 diff --git a/payloads_test.go b/payloads_test.go index e1b8ffcd..9df1d80c 100644 --- a/payloads_test.go +++ b/payloads_test.go @@ -2283,6 +2283,154 @@ const listSecGroupsPayloadPage2 = `{ ] }` +const listSpaceStagingSecGroupsPayload = `{ + "total_results": 28, + "total_pages": 1, + "prev_url": null, + "next_url": "/v2/security_groupsPage2", + "resources": [ + { + "metadata": { + "guid": "af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c", + "url": "/v2/security_groups/af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c", + "created_at": "2015-12-04T11:15:55Z", + "updated_at": null + }, + "entity": { + "name": "secgroup-test", + "rules": [ + { + "destination": "1.1.1.1", + "ports": "443,4443", + "protocol": "tcp" + }, + { + "destination": "1.2.3.4", + "ports": "1111", + "protocol": "udp" + } + ], + "running_default": true, + "staging_default": true, + "staging_spaces_url": "/v2/security_groups/af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c/staging_spaces", + "spaces": [] + } + } + ] +}` + +const listSpaceStagingSecGroupsPayloadPage2 = `{ +"total_results": 28, + "total_pages": 1, + "prev_url": null, + "next_url": null, + "resources": [ + { + "metadata": { + "guid": "f9ad202b-76dd-44ec-b7c2-fd2417a561e8", + "url": "/v2/security_groups/f9ad202b-76dd-44ec-b7c2-fd2417a561e8", + "created_at": "2015-12-04T11:15:55Z", + "updated_at": null + }, + "entity": { + "name": "secgroup-test2", + "rules": [ + { + "destination": "2.2.2.2", + "ports": "2222", + "protocol": "udp" + }, + { + "destination": "4.3.2.1", + "ports": "443,4443", + "protocol": "tcp" + } + ], + "running_default": false, + "staging_default": false, + "staging_spaces_url": "/v2/security_groups/f9ad202b-76dd-44ec-b7c2-fd2417a561e8/staging_spaces", + "staging_spaces": [ + { + "metadata": { + "guid": "e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4", + "url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4", + "created_at": "2014-10-27T10:49:37Z", + "updated_at": "2015-01-21T15:30:52Z" + }, + "entity": { + "name": "space-test", + "organization_guid": "82338ba1-bc08-4576-aad1-9a5b4693b386", + "space_quota_definition_guid": null, + "allow_ssh": true, + "organization_url": "/v2/organizations/82338ba1-bc08-4576-aad1-9a5b4693b386", + "developers_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/developers", + "managers_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/managers", + "auditors_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/auditors", + "apps_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/apps", + "routes_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/routes", + "domains_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/domains", + "service_instances_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/service_instances", + "app_events_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/app_events", + "events_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/events", + "staging_security_groups_url": "/v2/spaces/e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4/staging_security_groups" + } + }, + { + "metadata": { + "guid": "a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333", + "url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333", + "created_at": "2014-10-27T10:49:37Z", + "updated_at": "2015-01-21T15:30:52Z" + }, + "entity": { + "name": "space-test2", + "organization_guid": "82338ba1-bc08-4576-aad1-9a5b4693b386", + "space_quota_definition_guid": null, + "allow_ssh": true, + "organization_url": "/v2/organizations/82338ba1-bc08-4576-aad1-9a5b4693b386", + "developers_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/developers", + "managers_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/managers", + "auditors_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/auditors", + "apps_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/apps", + "routes_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/routes", + "domains_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/domains", + "service_instances_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/service_instances", + "app_events_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/app_events", + "events_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/events", + "staging_security_groups_url": "/v2/spaces/a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333/staging_security_groups" + } + }, + { + "metadata": { + "guid": "c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1", + "url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1", + "created_at": "2014-10-27T10:49:37Z", + "updated_at": "2015-01-21T15:30:52Z" + }, + "entity": { + "name": "space-test3", + "organization_guid": "82338ba1-bc08-4576-aad1-9a5b4693b386", + "space_quota_definition_guid": null, + "allow_ssh": true, + "organization_url": "/v2/organizations/82338ba1-bc08-4576-aad1-9a5b4693b386", + "developers_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/developers", + "managers_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/managers", + "auditors_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/auditors", + "apps_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/apps", + "routes_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/routes", + "domains_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/domains", + "service_instances_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/service_instances", + "app_events_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/app_events", + "events_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/events", + "staging_security_groups_url": "/v2/spaces/c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1/staging_security_groups" + } + } + ] + } + } + ] +}` + const listRunningSecGroupsPayload = `{ "total_results": 1, "total_pages": 1, diff --git a/secgroups.go b/secgroups.go index 5d6db1e6..564e595c 100644 --- a/secgroups.go +++ b/secgroups.go @@ -462,6 +462,25 @@ func (c *Client) UnbindSecGroup(secGUID, spaceGUID string) error { return nil } +/* +UnbindStagingSecGroupToSpace contact the CF endpoint to dis-associate a space with a security group for life-cycle staging. +It is the reverse of BindStagingSecGroupToSpace and comparable to UnbindSecGroup which is for life-cycle running. +secGUID: identifies the security group to remove a space from +spaceGUID: identifies the space to dissociate from the security group +*/ +func (c *Client) UnbindStagingSecGroupToSpace(secGUID, spaceGUID string) error { + // Perform the DELETE and check for errors + resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s/staging_spaces/%s", secGUID, spaceGUID))) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("CF API returned with status code %d", resp.StatusCode) + } + return nil +} + // Reads most security group response bodies into a SecGroup object func respBodyToSecGroup(body io.ReadCloser, c *Client) (*SecGroup, error) { // get the json from the response body diff --git a/secgroups_test.go b/secgroups_test.go index 051fd472..37a12956 100644 --- a/secgroups_test.go +++ b/secgroups_test.go @@ -254,3 +254,22 @@ func TestUnbindStagingSecGroups(t *testing.T) { So(err, ShouldBeNil) }) } + +func TestUnbindStagingSecGroupToSpace(t *testing.T) { + Convey("Unbind Staging Sec Group from Space", t, func() { + mocks := []MockRoute{ + {"DELETE", "/v2/security_groups/0cbfd9ee-697f-4b52-81b8-e54b6c77e352/staging_spaces/c3cd2163-d66a-47ec-b3e8-e4189bd52f09", []string{""}, "", 204, "", nil}, + } + setupMultiple(mocks, t) + defer teardown() + c := &Config{ + ApiAddress: server.URL, + Token: "foobar", + } + client, err := NewClient(c) + So(err, ShouldBeNil) + + err = client.UnbindStagingSecGroupToSpace("0cbfd9ee-697f-4b52-81b8-e54b6c77e352", "c3cd2163-d66a-47ec-b3e8-e4189bd52f09") + So(err, ShouldBeNil) + }) +} diff --git a/spaces.go b/spaces.go index c85e5b00..b2184070 100644 --- a/spaces.go +++ b/spaces.go @@ -615,6 +615,55 @@ func (s *Space) ListSecGroups() (secGroups []SecGroup, err error) { return secGroups, nil } +func (c *Client) ListSpaceStagingSecGroups(spaceGUID string) (secGroups []SecGroup, err error) { + space := Space{Guid: spaceGUID, c: c} + return space.ListStagingSecGroups() +} + +func (s *Space) ListStagingSecGroups() (secGroups []SecGroup, err error) { + requestURL := fmt.Sprintf("/v2/spaces/%s/staging_security_groups?inline-relations-depth=1", s.Guid) + for requestURL != "" { + var secGroupResp SecGroupResponse + r := s.c.NewRequest("GET", requestURL) + resp, err := s.c.DoRequest(r) + + if err != nil { + return nil, errors.Wrap(err, "Error requesting staging sec groups") + } + defer resp.Body.Close() + resBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "Error reading staging sec group response body") + } + + err = json.Unmarshal(resBody, &secGroupResp) + if err != nil { + return nil, errors.Wrap(err, "Error unmarshalling staging sec group") + } + + for _, secGroup := range secGroupResp.Resources { + secGroup.Entity.Guid = secGroup.Meta.Guid + secGroup.Entity.c = s.c + for i, space := range secGroup.Entity.StagingSpacesData { + space.Entity.Guid = space.Meta.Guid + secGroup.Entity.StagingSpacesData[i] = space + } + if len(secGroup.Entity.StagingSpacesData) == 0 { + spaces, err := secGroup.Entity.ListSpaceResources() + if err != nil { + return nil, err + } + secGroup.Entity.StagingSpacesData = append(secGroup.Entity.StagingSpacesData, spaces...) + } + secGroups = append(secGroups, secGroup.Entity) + } + + requestURL = secGroupResp.NextUrl + resp.Body.Close() + } + return secGroups, nil +} + func (s *Space) GetServiceOfferings() (ServiceOfferingResponse, error) { var response ServiceOfferingResponse requestURL := fmt.Sprintf("/v2/spaces/%s/services", s.Guid) diff --git a/spaces_test.go b/spaces_test.go index 11b4a198..7a5ca8e3 100644 --- a/spaces_test.go +++ b/spaces_test.go @@ -100,6 +100,57 @@ func TestListSpaceSecGroups(t *testing.T) { }) } +func TestListSpaceStagingSecGroups(t *testing.T) { + Convey("List Space Staging SecGroups", t, func() { + mocks := []MockRoute{ + {"GET", "/v2/spaces/8efd7c5c-d83c-4786-b399-b7bd548839e1/staging_security_groups", []string{listSpaceStagingSecGroupsPayload}, "", 200, "inline-relations-depth=1", nil}, + {"GET", "/v2/security_groupsPage2", []string{listSpaceStagingSecGroupsPayloadPage2}, "", 200, "", nil}, + {"GET", "/v2/security_groups/af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c/staging_spaces", []string{emptyResources}, "", 200, "", nil}, + } + setupMultiple(mocks, t) + defer teardown() + c := &Config{ + ApiAddress: server.URL, + Token: "foobar", + } + client, err := NewClient(c) + So(err, ShouldBeNil) + + secGroups, err := client.ListSpaceStagingSecGroups("8efd7c5c-d83c-4786-b399-b7bd548839e1") + So(err, ShouldBeNil) + + So(len(secGroups), ShouldEqual, 2) + So(secGroups[0].Guid, ShouldEqual, "af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c") + So(secGroups[0].Name, ShouldEqual, "secgroup-test") + So(secGroups[0].Running, ShouldEqual, true) + So(secGroups[0].Staging, ShouldEqual, true) + So(secGroups[0].Rules[0].Protocol, ShouldEqual, "tcp") + So(secGroups[0].Rules[0].Ports, ShouldEqual, "443,4443") + So(secGroups[0].Rules[0].Destination, ShouldEqual, "1.1.1.1") + So(secGroups[0].Rules[1].Protocol, ShouldEqual, "udp") + So(secGroups[0].Rules[1].Ports, ShouldEqual, "1111") + So(secGroups[0].Rules[1].Destination, ShouldEqual, "1.2.3.4") + So(secGroups[0].StagingSpacesURL, ShouldEqual, "/v2/security_groups/af15c29a-6bde-4a9b-8cdf-43aa0d4b7e3c/staging_spaces") + So(secGroups[0].StagingSpacesData, ShouldBeEmpty) + So(secGroups[1].Guid, ShouldEqual, "f9ad202b-76dd-44ec-b7c2-fd2417a561e8") + So(secGroups[1].Name, ShouldEqual, "secgroup-test2") + So(secGroups[1].Running, ShouldEqual, false) + So(secGroups[1].Staging, ShouldEqual, false) + So(secGroups[1].Rules[0].Protocol, ShouldEqual, "udp") + So(secGroups[1].Rules[0].Ports, ShouldEqual, "2222") + So(secGroups[1].Rules[0].Destination, ShouldEqual, "2.2.2.2") + So(secGroups[1].Rules[1].Protocol, ShouldEqual, "tcp") + So(secGroups[1].Rules[1].Ports, ShouldEqual, "443,4443") + So(secGroups[1].Rules[1].Destination, ShouldEqual, "4.3.2.1") + So(secGroups[1].StagingSpacesData[0].Entity.Guid, ShouldEqual, "e0a0d1bf-ad74-4b3c-8f4a-0c33859a54e4") + So(secGroups[1].StagingSpacesData[0].Entity.Name, ShouldEqual, "space-test") + So(secGroups[1].StagingSpacesData[1].Entity.Guid, ShouldEqual, "a2a0d1bf-ad74-4b3c-8f4a-0c33859a5333") + So(secGroups[1].StagingSpacesData[1].Entity.Name, ShouldEqual, "space-test2") + So(secGroups[1].StagingSpacesData[2].Entity.Guid, ShouldEqual, "c7a0d1bf-ad74-4b3c-8f4a-0c33859adsa1") + So(secGroups[1].StagingSpacesData[2].Entity.Name, ShouldEqual, "space-test3") + }) +} + func TestListSpaceManagers(t *testing.T) { Convey("ListSpaceManagers()", t, func() { setup(MockRoute{"GET", "/v2/spaces/foo/managers", []string{listSpacePeoplePayload}, "", 200, "", nil}, t)