1// Copyright 2021 MongoDB Inc
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package mongodbatlas
16
17import (
18	"context"
19	"fmt"
20	"net/http"
21	"net/url"
22)
23
24const clustersPath = "groups/%s/clusters"
25
26// ClustersService is an interface for interfacing with the Clusters
27// endpoints of the MongoDB Atlas API.
28// See more: https://docs.atlas.mongodb.com/reference/api/clusters/
29type ClustersService interface {
30	List(context.Context, string, *ListOptions) ([]Cluster, *Response, error)
31	Get(context.Context, string, string) (*Cluster, *Response, error)
32	Create(context.Context, string, *Cluster) (*Cluster, *Response, error)
33	Update(context.Context, string, string, *Cluster) (*Cluster, *Response, error)
34	Delete(context.Context, string, string) (*Response, error)
35	UpdateProcessArgs(context.Context, string, string, *ProcessArgs) (*ProcessArgs, *Response, error)
36	GetProcessArgs(context.Context, string, string) (*ProcessArgs, *Response, error)
37}
38
39// ClustersServiceOp handles communication with the Cluster related methods
40// of the MongoDB Atlas API
41type ClustersServiceOp service
42
43var _ ClustersService = &ClustersServiceOp{}
44
45// AutoScaling configures your cluster to automatically scale its storage
46type AutoScaling struct {
47	AutoIndexingEnabled *bool    `json:"autoIndexingEnabled,omitempty"` // Autopilot mode is only available if you are enrolled in the Auto Pilot Early Access program.
48	Compute             *Compute `json:"compute,omitempty"`
49	DiskGBEnabled       *bool    `json:"diskGBEnabled,omitempty"`
50}
51
52// Compute Specifies whether the cluster automatically scales its cluster tier and whether the cluster can scale down.
53type Compute struct {
54	Enabled          *bool  `json:"enabled,omitempty"`
55	ScaleDownEnabled *bool  `json:"scaleDownEnabled,omitempty"`
56	MinInstanceSize  string `json:"minInstanceSize,omitempty"`
57	MaxInstanceSize  string `json:"maxInstanceSize,omitempty"`
58}
59
60// BiConnector specifies BI Connector for Atlas configuration on this cluster
61type BiConnector struct {
62	Enabled        *bool  `json:"enabled,omitempty"`
63	ReadPreference string `json:"readPreference,omitempty"`
64}
65
66// ProviderSettings configuration for the provisioned servers on which MongoDB runs. The available options are specific to the cloud service provider.
67type ProviderSettings struct {
68	BackingProviderName string       `json:"backingProviderName,omitempty"`
69	DiskIOPS            *int64       `json:"diskIOPS,omitempty"`
70	DiskTypeName        string       `json:"diskTypeName,omitempty"`
71	EncryptEBSVolume    *bool        `json:"encryptEBSVolume,omitempty"`
72	InstanceSizeName    string       `json:"instanceSizeName,omitempty"`
73	ProviderName        string       `json:"providerName,omitempty"`
74	RegionName          string       `json:"regionName,omitempty"`
75	VolumeType          string       `json:"volumeType,omitempty"`
76	AutoScaling         *AutoScaling `json:"autoScaling,omitempty"`
77}
78
79// RegionsConfig describes the region’s priority in elections and the number and type of MongoDB nodes Atlas deploys to the region.
80type RegionsConfig struct {
81	AnalyticsNodes *int64 `json:"analyticsNodes,omitempty"`
82	ElectableNodes *int64 `json:"electableNodes,omitempty"`
83	Priority       *int64 `json:"priority,omitempty"`
84	ReadOnlyNodes  *int64 `json:"readOnlyNodes,omitempty"`
85}
86
87// ReplicationSpec represents a configuration for cluster regions
88type ReplicationSpec struct {
89	ID            string                   `json:"id,omitempty"`
90	NumShards     *int64                   `json:"numShards,omitempty"`
91	ZoneName      string                   `json:"zoneName,omitempty"`
92	RegionsConfig map[string]RegionsConfig `json:"regionsConfig,omitempty"`
93}
94
95// PrivateEndpoint connection strings. Each object describes the connection strings
96// you can use to connect to this cluster through a private endpoint.
97// Atlas returns this parameter only if you deployed a private endpoint to all regions
98// to which you deployed this cluster's nodes.
99type PrivateEndpoint struct {
100	ConnectionString    string     `json:"connectionString,omitempty"`
101	Endpoints           []Endpoint `json:"endpoints,omitempty"`
102	SRVConnectionString string     `json:"srvConnectionString,omitempty"`
103	Type                string     `json:"type,omitempty"`
104}
105
106// Endpoint through which you connect to Atlas
107type Endpoint struct {
108	EndpointID   string `json:"endpointId,omitempty"`
109	ProviderName string `json:"providerName,omitempty"`
110	Region       string `json:"region,omitempty"`
111}
112
113// ConnectionStrings configuration for applications use to connect to this cluster
114type ConnectionStrings struct {
115	Standard          string            `json:"standard,omitempty"`
116	StandardSrv       string            `json:"standardSrv,omitempty"`
117	PrivateEndpoint   []PrivateEndpoint `json:"privateEndpoint,omitempty"`
118	AwsPrivateLink    map[string]string `json:"awsPrivateLink,omitempty"`    // Deprecated: Use connectionStrings.PrivateEndpoint[n].ConnectionString
119	AwsPrivateLinkSrv map[string]string `json:"awsPrivateLinkSrv,omitempty"` // Deprecated: Use ConnectionStrings.privateEndpoint[n].SRVConnectionString
120	Private           string            `json:"private,omitempty"`
121	PrivateSrv        string            `json:"privateSrv,omitempty"`
122}
123
124// Cluster represents MongoDB cluster.
125type Cluster struct {
126	AutoScaling              *AutoScaling             `json:"autoScaling,omitempty"`
127	BackupEnabled            *bool                    `json:"backupEnabled,omitempty"` // Deprecated: Use ProviderBackupEnabled instead
128	BiConnector              *BiConnector             `json:"biConnector,omitempty"`
129	ClusterType              string                   `json:"clusterType,omitempty"`
130	DiskSizeGB               *float64                 `json:"diskSizeGB,omitempty"`
131	EncryptionAtRestProvider string                   `json:"encryptionAtRestProvider,omitempty"`
132	Labels                   []Label                  `json:"labels,omitempty"`
133	ID                       string                   `json:"id,omitempty"`
134	GroupID                  string                   `json:"groupId,omitempty"`
135	MongoDBVersion           string                   `json:"mongoDBVersion,omitempty"`
136	MongoDBMajorVersion      string                   `json:"mongoDBMajorVersion,omitempty"`
137	MongoURI                 string                   `json:"mongoURI,omitempty"`
138	MongoURIUpdated          string                   `json:"mongoURIUpdated,omitempty"`
139	MongoURIWithOptions      string                   `json:"mongoURIWithOptions,omitempty"`
140	Name                     string                   `json:"name,omitempty"`
141	NumShards                *int64                   `json:"numShards,omitempty"`
142	Paused                   *bool                    `json:"paused,omitempty"`
143	PitEnabled               *bool                    `json:"pitEnabled,omitempty"`
144	ProviderBackupEnabled    *bool                    `json:"providerBackupEnabled,omitempty"`
145	ProviderSettings         *ProviderSettings        `json:"providerSettings,omitempty"`
146	ReplicationFactor        *int64                   `json:"replicationFactor,omitempty"`
147	ReplicationSpec          map[string]RegionsConfig `json:"replicationSpec,omitempty"`
148	ReplicationSpecs         []ReplicationSpec        `json:"replicationSpecs,omitempty"`
149	SrvAddress               string                   `json:"srvAddress,omitempty"`
150	StateName                string                   `json:"stateName,omitempty"`
151	ConnectionStrings        *ConnectionStrings       `json:"connectionStrings,omitempty"`
152}
153
154// ProcessArgs represents the advanced configuration options for the cluster
155type ProcessArgs struct {
156	FailIndexKeyTooLong              *bool  `json:"failIndexKeyTooLong,omitempty"`
157	JavascriptEnabled                *bool  `json:"javascriptEnabled,omitempty"`
158	MinimumEnabledTLSProtocol        string `json:"minimumEnabledTlsProtocol,omitempty"`
159	NoTableScan                      *bool  `json:"noTableScan,omitempty"`
160	OplogSizeMB                      *int64 `json:"oplogSizeMB,omitempty"`
161	SampleSizeBIConnector            *int64 `json:"sampleSizeBIConnector,omitempty"`
162	SampleRefreshIntervalBIConnector *int64 `json:"sampleRefreshIntervalBIConnector,omitempty"`
163}
164
165// clustersResponse is the response from the ClustersService.List.
166type clustersResponse struct {
167	Links      []*Link   `json:"links,omitempty"`
168	Results    []Cluster `json:"results,omitempty"`
169	TotalCount int       `json:"totalCount,omitempty"`
170}
171
172// DefaultDiskSizeGB represents the Tier and the default disk size for each one
173// it can be use like: DefaultDiskSizeGB["AWS"]["M10"]
174var DefaultDiskSizeGB = map[string]map[string]float64{
175	"TENANT": {
176		"M2": 2,
177		"M5": 5,
178	},
179	"AWS": {
180		"M10":       10,
181		"M20":       20,
182		"M30":       40,
183		"M40":       80,
184		"R40":       80,
185		"M40_NVME":  380,
186		"M50":       160,
187		"R50":       160,
188		"M50_NVME":  760,
189		"M60":       320,
190		"R60":       320,
191		"M60_NVME":  1600,
192		"M80":       750,
193		"R80":       750,
194		"M80_NVME":  1600,
195		"M140":      1000,
196		"M200":      1500,
197		"R200":      1500,
198		"M200_NVME": 3100,
199		"M300":      2000,
200		"R300":      2000,
201		"R400":      3000,
202		"M400_NVME": 4000,
203	},
204	"GCP": {
205		"M10":  10,
206		"M20":  20,
207		"M30":  40,
208		"M40":  80,
209		"M50":  160,
210		"M60":  320,
211		"M80":  750,
212		"M200": 1500,
213		"M300": 2200,
214	},
215	"AZURE": {
216		"M10":  32,
217		"M20":  32,
218		"M30":  32,
219		"M40":  128,
220		"M50":  128,
221		"M60":  128,
222		"M80":  256,
223		"M200": 256,
224	},
225}
226
227// List all clusters in the project associated to {GROUP-ID}.
228// See more: https://docs.atlas.mongodb.com/reference/api/clusters-get-all/
229func (s *ClustersServiceOp) List(ctx context.Context, groupID string, listOptions *ListOptions) ([]Cluster, *Response, error) {
230	path := fmt.Sprintf(clustersPath, groupID)
231
232	// Add query params from listOptions
233	path, err := setListOptions(path, listOptions)
234	if err != nil {
235		return nil, nil, err
236	}
237
238	req, err := s.Client.NewRequest(ctx, http.MethodGet, path, nil)
239	if err != nil {
240		return nil, nil, err
241	}
242
243	root := new(clustersResponse)
244	resp, err := s.Client.Do(ctx, req, root)
245	if err != nil {
246		return nil, resp, err
247	}
248
249	if l := root.Links; l != nil {
250		resp.Links = l
251	}
252
253	return root.Results, resp, nil
254}
255
256// Get gets the cluster specified to {ClUSTER-NAME} from the project associated to {GROUP-ID}.
257// See more: https://docs.atlas.mongodb.com/reference/api/clusters-get-one/
258func (s *ClustersServiceOp) Get(ctx context.Context, groupID, clusterName string) (*Cluster, *Response, error) {
259	if err := checkClusterNameParam(clusterName); err != nil {
260		return nil, nil, err
261	}
262
263	basePath := fmt.Sprintf(clustersPath, groupID)
264	escapedEntry := url.PathEscape(clusterName)
265	path := fmt.Sprintf("%s/%s", basePath, escapedEntry)
266
267	req, err := s.Client.NewRequest(ctx, http.MethodGet, path, nil)
268	if err != nil {
269		return nil, nil, err
270	}
271
272	root := new(Cluster)
273	resp, err := s.Client.Do(ctx, req, root)
274	if err != nil {
275		return nil, resp, err
276	}
277
278	return root, resp, err
279}
280
281// Create adds a cluster to the project associated to {GROUP-ID}.
282// See more: https://docs.atlas.mongodb.com/reference/api/clusters-create-one/
283func (s *ClustersServiceOp) Create(ctx context.Context, groupID string, createRequest *Cluster) (*Cluster, *Response, error) {
284	if createRequest == nil {
285		return nil, nil, NewArgError("createRequest", "cannot be nil")
286	}
287
288	path := fmt.Sprintf(clustersPath, groupID)
289
290	req, err := s.Client.NewRequest(ctx, http.MethodPost, path, createRequest)
291	if err != nil {
292		return nil, nil, err
293	}
294
295	root := new(Cluster)
296	resp, err := s.Client.Do(ctx, req, root)
297	if err != nil {
298		return nil, resp, err
299	}
300
301	return root, resp, err
302}
303
304// Update a cluster in the project associated to {GROUP-ID}
305// See more: https://docs.atlas.mongodb.com/reference/api/clusters-modify-one/
306func (s *ClustersServiceOp) Update(ctx context.Context, groupID, clusterName string, updateRequest *Cluster) (*Cluster, *Response, error) {
307	if updateRequest == nil {
308		return nil, nil, NewArgError("updateRequest", "cannot be nil")
309	}
310
311	basePath := fmt.Sprintf(clustersPath, groupID)
312	path := fmt.Sprintf("%s/%s", basePath, clusterName)
313
314	req, err := s.Client.NewRequest(ctx, http.MethodPatch, path, updateRequest)
315	if err != nil {
316		return nil, nil, err
317	}
318
319	root := new(Cluster)
320	resp, err := s.Client.Do(ctx, req, root)
321	if err != nil {
322		return nil, resp, err
323	}
324
325	return root, resp, err
326}
327
328// Delete the cluster specified to {CLUSTER-NAME} from the project associated to {GROUP-ID}.
329// See more: https://docs.atlas.mongodb.com/reference/api/clusters-delete-one/
330func (s *ClustersServiceOp) Delete(ctx context.Context, groupID, clusterName string) (*Response, error) {
331	if clusterName == "" {
332		return nil, NewArgError("clusterName", "must be set")
333	}
334
335	basePath := fmt.Sprintf(clustersPath, groupID)
336	escapedEntry := url.PathEscape(clusterName)
337	path := fmt.Sprintf("%s/%s", basePath, escapedEntry)
338
339	req, err := s.Client.NewRequest(ctx, http.MethodDelete, path, nil)
340	if err != nil {
341		return nil, err
342	}
343
344	resp, err := s.Client.Do(ctx, req, nil)
345
346	return resp, err
347}
348
349// UpdateProcessArgs Modifies Advanced Configuration Options for One Cluster
350// See more: https://docs.atlas.mongodb.com/reference/api/clusters-modify-advanced-configuration-options/
351func (s *ClustersServiceOp) UpdateProcessArgs(ctx context.Context, groupID, clusterName string, updateRequest *ProcessArgs) (*ProcessArgs, *Response, error) {
352	if updateRequest == nil {
353		return nil, nil, NewArgError("updateRequest", "cannot be nil")
354	}
355
356	basePath := fmt.Sprintf(clustersPath, groupID)
357	path := fmt.Sprintf("%s/%s/processArgs", basePath, clusterName)
358
359	req, err := s.Client.NewRequest(ctx, http.MethodPatch, path, updateRequest)
360	if err != nil {
361		return nil, nil, err
362	}
363
364	root := new(ProcessArgs)
365	resp, err := s.Client.Do(ctx, req, root)
366	if err != nil {
367		return nil, resp, err
368	}
369
370	return root, resp, err
371}
372
373// GetProcessArgs gets the Advanced Configuration Options for One Cluster
374// See more: https://docs.atlas.mongodb.com/reference/api/clusters-get-advanced-configuration-options/#get-advanced-configuration-options-for-one-cluster
375func (s *ClustersServiceOp) GetProcessArgs(ctx context.Context, groupID, clusterName string) (*ProcessArgs, *Response, error) {
376	if err := checkClusterNameParam(clusterName); err != nil {
377		return nil, nil, err
378	}
379
380	basePath := fmt.Sprintf(clustersPath, groupID)
381	escapedEntry := url.PathEscape(clusterName)
382	path := fmt.Sprintf("%s/%s/processArgs", basePath, escapedEntry)
383
384	req, err := s.Client.NewRequest(ctx, http.MethodGet, path, nil)
385	if err != nil {
386		return nil, nil, err
387	}
388
389	root := new(ProcessArgs)
390	resp, err := s.Client.Do(ctx, req, root)
391	if err != nil {
392		return nil, resp, err
393	}
394
395	return root, resp, err
396}
397
398func checkClusterNameParam(clusterName string) error {
399	if clusterName == "" {
400		return NewArgError("name", "must be set")
401	}
402	return nil
403}
404