diff --git a/GNUmakefile b/GNUmakefile index 2dce0cea1..cf6b211b6 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -8,11 +8,10 @@ build: fmtcheck test: fmtcheck - go test $(TEST) -timeout=30s -parallel=4 + go test $(TEST) -timeout=30s -parallel=4 -cover fmt: @echo "==> Fixing source code with gofmt..." - gofmt -s -w ./main.go gofmt -s -w ./$(PKG_NAME) fmtcheck: diff --git a/README.md b/README.md index f712196ee..6fb830553 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -# go-client-mongodb-atlas +# go-client-mongodb-atlas [![Build Status](https://travis-ci.org/mongodb/go-client-mongodb-atlas.svg?branch=master)](https://travis-ci.org/mongodb/go-client-mongodb-atlas) + +A Go HTTP client for the [MongoDB Atlas API](https://docs.atlas.mongodb.com/api/). + + +## Contribution and Development + +``` +make tools +make test +``` \ No newline at end of file diff --git a/mongodbatlas/containers.go b/mongodbatlas/containers.go new file mode 100644 index 000000000..b44a10ce6 --- /dev/null +++ b/mongodbatlas/containers.go @@ -0,0 +1,173 @@ +package mongodbatlas + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +const containersPath = "groups/%s/containers" + +//ContainersService is an interface for interfacing with the Network Peering Containers +// endpoints of the MongoDB Atlas API. +//See more: https://docs.atlas.mongodb.com/reference/api/vpc/ +type ContainersService interface { + List(context.Context, string, *ListOptions) ([]Container, *Response, error) + Get(context.Context, string, string) (*Container, *Response, error) + Create(context.Context, string, *Container) (*Container, *Response, error) + Update(context.Context, string, string, *Container) (*Container, *Response, error) + Delete(context.Context, string, string) (*Response, error) +} + +//ContainersServiceOp handles communication with the Network Peering Container related methods +// of the MongoDB Atlas API +type ContainersServiceOp struct { + client *Client +} + +var _ ContainersService = &ContainersServiceOp{} + +// Container represents MongoDB network peering containter. +type Container struct { + AtlasCIDRBlock string `json:"atlasCidrBlock,omitempty"` + AzureSubscriptionID string `json:"azureSubscriptionId,omitempty"` + GCPProjectID string `json:"gcpProjectId,omitempty"` + ID string `json:"id,omitempty"` + NetworkName string `json:"networkName,omitempty"` + ProviderName string `json:"providerName,omitempty"` + Provisioned *bool `json:"provisioned,omitempty"` + Region string `json:"region,omitempty"` + RegionName string `json:"regionName,omitempty"` + VNetName string `json:"vnetName,omitempty"` + VPCID string `json:"vpcId,omitempty"` +} + +// containersResponse is the response from the ContainersService.List. +type containersResponse struct { + Links []*Link `json:"links,omitempty"` + Results []Container `json:"results,omitempty"` + TotalCount int `json:"totalCount,omitempty"` +} + +//List all containers in the project associated to {GROUP-ID}. +//See more: https://docs.atlas.mongodb.com/reference/api/vpc-get-containers-list/ +func (s *ContainersServiceOp) List(ctx context.Context, groupID string, listOptions *ListOptions) ([]Container, *Response, error) { + path := fmt.Sprintf(containersPath, groupID) + + //Add query params from listOptions + path, err := setListOptions(path, listOptions) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(containersResponse) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Results, resp, nil +} + +//Get gets the network peering container specified to {CONTAINER-ID} from the project associated to {GROUP-ID}. +//See more: https://docs.atlas.mongodb.com/reference/api/vpc-get-container/ +func (s *ContainersServiceOp) Get(ctx context.Context, groupID string, containerID string) (*Container, *Response, error) { + if containerID == "" { + return nil, nil, NewArgError("perrID", "must be set") + } + + basePath := fmt.Sprintf(containersPath, groupID) + escapedEntry := url.PathEscape(containerID) + path := fmt.Sprintf("%s/%s", basePath, escapedEntry) + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(Container) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, err +} + +//Add a network peering container to the project associated to {GROUP-ID}. +//See more: https://docs.atlas.mongodb.com/reference/api/vpc-create-container/ +func (s *ContainersServiceOp) Create(ctx context.Context, groupID string, createRequest *Container) (*Container, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + path := fmt.Sprintf(containersPath, groupID) + + req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(Container) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, err +} + +//Update a network peering container in the project associated to {GROUP-ID} +//See more: https://docs.atlas.mongodb.com/reference/api/vpc-update-container/ +func (s *ContainersServiceOp) Update(ctx context.Context, groupID string, containerID string, updateRequest *Container) (*Container, *Response, error) { + if updateRequest == nil { + return nil, nil, NewArgError("updateRequest", "cannot be nil") + } + + basePath := fmt.Sprintf(containersPath, groupID) + path := fmt.Sprintf("%s/%s", basePath, containerID) + + req, err := s.client.NewRequest(ctx, http.MethodPatch, path, updateRequest) + if err != nil { + return nil, nil, err + } + + root := new(Container) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root, resp, err +} + +//Delete the network peering container specified to {CONTAINER-ID} from the project associated to {GROUP-ID}. +// See more: https://docs.atlas.mongodb.com/reference/api/vpc-delete-one-container/ +func (s *ContainersServiceOp) Delete(ctx context.Context, groupID string, containerID string) (*Response, error) { + if containerID == "" { + return nil, NewArgError("containerID", "must be set") + } + + basePath := fmt.Sprintf(containersPath, groupID) + escapedEntry := url.PathEscape(containerID) + path := fmt.Sprintf("%s/%s", basePath, escapedEntry) + + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + + return resp, err +} diff --git a/mongodbatlas/containers_test.go b/mongodbatlas/containers_test.go new file mode 100644 index 000000000..371c23978 --- /dev/null +++ b/mongodbatlas/containers_test.go @@ -0,0 +1,316 @@ +package mongodbatlas + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/go-test/deep" + "github.com/mwielbut/pointy" +) + +func TestContainers_ListContainers(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/groups/1/containers", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{ + "results": [ + { + "atlasCidrBlock": "10.8.0.0/18", + "id": "1112269b3bf99403840e8934", + "gcpProjectId": "my-sample-project-191923", + "networkName": "test1", + "providerName": "GCP", + "provisioned": true + }, + { + "atlasCidrBlock" : "10.8.0.0/21", + "id" : "1112269b3bf99403840e8934", + "providerName" : "AWS", + "provisioned" : true, + "regionName" : "US_EAST_1", + "vpcId" : "vpc-zz0zzzzz" + } + ], + "totalCount": 2 + }`) + }) + + containers, _, err := client.Containers.List(ctx, "1", nil) + if err != nil { + t.Errorf("Containers.List returned error: %v", err) + } + + GCPContainer := Container{ + AtlasCIDRBlock: "10.8.0.0/18", + ID: "1112269b3bf99403840e8934", + GCPProjectID: "my-sample-project-191923", + NetworkName: "test1", + ProviderName: "GCP", + Provisioned: pointy.Bool(true), + } + + AWSContainer := Container{ + AtlasCIDRBlock: "10.8.0.0/21", + ID: "1112269b3bf99403840e8934", + ProviderName: "AWS", + Provisioned: pointy.Bool(true), + RegionName: "US_EAST_1", + VPCID: "vpc-zz0zzzzz", + } + + expected := []Container{GCPContainer, AWSContainer} + if !reflect.DeepEqual(containers, expected) { + t.Errorf("Containers.List\n got=%#v\nwant=%#v", containers, expected) + } +} + +func TestContainers_ListContainersMultiplePages(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/groups/1/containers", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + + dr := containersResponse{ + Results: []Container{ + { + AtlasCIDRBlock: "10.8.0.0/18", + ID: "1112269b3bf99403840e8934", + GCPProjectID: "my-sample-project-191923", + NetworkName: "test1", + ProviderName: "GCP", + Provisioned: pointy.Bool(true), + }, + { + AtlasCIDRBlock: "10.8.0.0/21", + ID: "1112269b3bf99403840e8934", + ProviderName: "AWS", + Provisioned: pointy.Bool(true), + RegionName: "US_EAST_1", + VPCID: "vpc-zz0zzzzz", + }, + }, + Links: []*Link{ + {Href: "http://example.com/api/atlas/v1.0/groups/1/containers?pageNum=2&itemsPerPage=2", Rel: "self"}, + {Href: "http://example.com/api/atlas/v1.0/groups/1/containers?pageNum=2&itemsPerPage=2", Rel: "previous"}, + }, + } + + b, err := json.Marshal(dr) + if err != nil { + t.Fatal(err) + } + fmt.Fprint(w, string(b)) + }) + + _, resp, err := client.Containers.List(ctx, "1", nil) + if err != nil { + t.Fatal(err) + } + + checkCurrentPage(t, resp, 2) +} + +func TestContainers_RetrievePageByNumber(t *testing.T) { + setup() + defer teardown() + + jBlob := ` + { + "links": [ + { + "href": "http://example.com/api/atlas/v1.0/groups/1/containers?pageNum=1&itemsPerPage=1", + "rel": "previous" + }, + { + "href": "http://example.com/api/atlas/v1.0/groups/1/containers?pageNum=2&itemsPerPage=1", + "rel": "self" + }, + { + "href": "http://example.com/api/atlas/v1.0/groups/1/containers?itemsPerPage=3&pageNum=2", + "rel": "next" + } + ], + "results": [ + { + "atlasCidrBlock": "10.8.0.0/18", + "id": "1112269b3bf99403840e8934", + "gcpProjectId": "my-sample-project-191923", + "networkName": "test1", + "providerName": "GCP", + "provisioned": true + } + ], + "totalCount": 3 + }` + + mux.HandleFunc("/groups/1/containers", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, jBlob) + }) + + opt := &ListOptions{PageNum: 2} + _, resp, err := client.Containers.List(ctx, "1", opt) + + if err != nil { + t.Fatal(err) + } + + checkCurrentPage(t, resp, 2) +} + +func TestContainers_Create(t *testing.T) { + setup() + defer teardown() + + groupID := "1" + + createRequest := &Container{ + AtlasCIDRBlock: "10.8.0.0/18", + ProviderName: "GCP", + } + + mux.HandleFunc(fmt.Sprintf("/groups/%s/containers", groupID), func(w http.ResponseWriter, r *http.Request) { + expected := map[string]interface{}{ + "atlasCidrBlock": "10.8.0.0/18", + "providerName": "GCP", + } + + jsonBlob := ` + { + "atlasCidrBlock": "10.8.0.0/18", + "id": "1112269b3bf99403840e8934", + "gcpProjectId": "my-sample-project-191923", + "networkName": "test1", + "providerName": "GCP", + "provisioned": true + } + ` + + var v map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + if diff := deep.Equal(v, expected); diff != nil { + t.Errorf("Containers.Create Request Body = %v", diff) + } + + if !reflect.DeepEqual(v, expected) { + t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) + } + + fmt.Fprintf(w, jsonBlob) + }) + + container, _, err := client.Containers.Create(ctx, groupID, createRequest) + if err != nil { + t.Errorf("Containers.Create returned error: %v", err) + } + + expectedCID := "1112269b3bf99403840e8934" + expectedGCPID := "my-sample-project-191923" + + if cID := container.ID; cID != expectedCID { + t.Errorf("expected containerId '%s', received '%s'", expectedCID, cID) + } + + if id := container.GCPProjectID; id != expectedGCPID { + t.Errorf("expected gcpProjectId '%s', received '%s'", expectedGCPID, id) + } + + if isProvisioned := container.Provisioned; !*isProvisioned { + t.Errorf("expected provisioned '%t', received '%t'", true, *isProvisioned) + } + +} + +func TestContainers_Update(t *testing.T) { + setup() + defer teardown() + + groupID := "1" + containerID := "1112269b3bf99403840e8934" + + updateRequest := &Container{ + AtlasCIDRBlock: "10.8.0.0/18", + GCPProjectID: "my-sample-project-191923", + ProviderName: "GCP", + } + + mux.HandleFunc(fmt.Sprintf("/groups/%s/containers/%s", groupID, containerID), func(w http.ResponseWriter, r *http.Request) { + expected := map[string]interface{}{ + "atlasCidrBlock": "10.8.0.0/18", + "gcpProjectId": "my-sample-project-191923", + "providerName": "GCP", + } + + jsonBlob := ` + { + "atlasCidrBlock": "10.8.0.0/18", + "id": "1112269b3bf99403840e8934", + "gcpProjectId": "my-sample-project-191923", + "networkName": "test1", + "providerName": "GCP", + "provisioned": true + } + ` + + var v map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&v) + if err != nil { + t.Fatalf("decode json: %v", err) + } + + if diff := deep.Equal(v, expected); diff != nil { + t.Errorf("Containers.Update Request Body = %v", diff) + } + if !reflect.DeepEqual(v, expected) { + t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) + } + + fmt.Fprintf(w, jsonBlob) + }) + + container, _, err := client.Containers.Update(ctx, groupID, containerID, updateRequest) + if err != nil { + t.Errorf("Containers.Update returned error: %v", err) + } + + if cID := container.ID; cID != containerID { + t.Errorf("expected containerID '%s', received '%s'", containerID, cID) + } + + expectedGCPID := "my-sample-project-191923" + + if id := container.GCPProjectID; id != expectedGCPID { + t.Errorf("expected gcpProjectId '%s', received '%s'", expectedGCPID, id) + } + + if isProvisioned := container.Provisioned; !*isProvisioned { + t.Errorf("expected provisioned '%t', received '%t'", true, *isProvisioned) + } + +} + +func TestContainers_Delete(t *testing.T) { + setup() + defer teardown() + + groupID := "1" + id := "1112222b3bf99403840e8934" + + mux.HandleFunc(fmt.Sprintf("/groups/%s/containers/%s", groupID, id), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.Containers.Delete(ctx, groupID, id) + if err != nil { + t.Errorf("Container.Delete returned error: %v", err) + } +} diff --git a/mongodbatlas/mongodbatlas.go b/mongodbatlas/mongodbatlas.go index f4f8ced83..0f08a6617 100644 --- a/mongodbatlas/mongodbatlas.go +++ b/mongodbatlas/mongodbatlas.go @@ -39,6 +39,7 @@ type Client struct { APIKeys APIKeysService CloudProviderSnapshotRestoreJobs CloudProviderSnapshotRestoreJobsService Peers PeersService + Containers ContainersService onRequestCompleted RequestCompletionCallback } @@ -138,6 +139,7 @@ func NewClient(httpClient *http.Client) *Client { c.APIKeys = &APIKeysServiceOp{client: c} c.CloudProviderSnapshotRestoreJobs = &CloudProviderSnapshotRestoreJobsServiceOp{client: c} c.Peers = &PeersServiceOp{client: c} + c.Containers = &ContainersServiceOp{client: c} return c } diff --git a/mongodbatlas/project_ip_whitelist_test.go b/mongodbatlas/project_ip_whitelist_test.go index d262a1428..eec415915 100644 --- a/mongodbatlas/project_ip_whitelist_test.go +++ b/mongodbatlas/project_ip_whitelist_test.go @@ -118,7 +118,7 @@ func TestProjectIPWhitelist_Create(t *testing.T) { groupID := "1" createRequest := []*ProjectIPWhitelist{ - &ProjectIPWhitelist{ + { GroupID: groupID, CIDRBlock: "0.0.0.1/12", }, @@ -126,7 +126,7 @@ func TestProjectIPWhitelist_Create(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/groups/%s/whitelist", groupID), func(w http.ResponseWriter, r *http.Request) { expected := []map[string]interface{}{ - map[string]interface{}{ + { "cidrBlock": "0.0.0.1/12", "groupId": groupID, }, @@ -225,13 +225,13 @@ func TestProjectIPWhitelist_Update(t *testing.T) { groupID := "1" ipAddress := "0.0.0.1" - createRequest := []*ProjectIPWhitelist{&ProjectIPWhitelist{ + createRequest := []*ProjectIPWhitelist{{ GroupID: groupID, IPAddress: ipAddress, }} mux.HandleFunc(fmt.Sprintf("/groups/%s/whitelist/%s", groupID, ipAddress), func(w http.ResponseWriter, r *http.Request) { - expected := []map[string]interface{}{map[string]interface{}{ + expected := []map[string]interface{}{{ "ipAddress": ipAddress, "groupId": groupID, }}