Skip to content

Commit 94057bb

Browse files
CLOUDP-325300: [AtlasCLI] Support preview and upcoming stability levels for L1 commands (#3984)
1 parent 3af2fbc commit 94057bb

39 files changed

+2697
-1140
lines changed

internal/api/commands.go

Lines changed: 1221 additions & 981 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/api/executor_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@ func TestExecutorHappyPathNoLogging(t *testing.T) {
5858
RequestParameters: api.RequestParameters{
5959
URL: "/test/url",
6060
},
61-
Versions: []api.Version{{
62-
Version: "1991-05-17",
61+
Versions: []api.CommandVersion{{
62+
Version: api.NewStableVersion(1991, 5, 17),
6363
RequestContentType: "json",
6464
ResponseContentTypes: []string{"json"},
6565
}},
6666
},
6767
ContentType: "json",
6868
Format: "json",
6969
Parameters: nil,
70-
Version: "1991-05-17",
70+
Version: api.NewStableVersion(1991, 5, 17),
7171
}
7272
response, err := executor.ExecuteCommand(t.Context(), commandRequest)
7373

@@ -120,16 +120,16 @@ func TestExecutorHappyPathDebugLogging(t *testing.T) {
120120
RequestParameters: api.RequestParameters{
121121
URL: "/test/url",
122122
},
123-
Versions: []api.Version{{
124-
Version: "1991-05-17",
123+
Versions: []api.CommandVersion{{
124+
Version: api.NewStableVersion(1991, 5, 17),
125125
RequestContentType: "json",
126126
ResponseContentTypes: []string{"json"},
127127
}},
128128
},
129129
ContentType: "json",
130130
Format: "json",
131131
Parameters: nil,
132-
Version: "1991-05-17",
132+
Version: api.NewStableVersion(1991, 5, 17),
133133
}
134134
response, err := executor.ExecuteCommand(t.Context(), commandRequest)
135135

internal/api/httprequest.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,19 +145,19 @@ func buildQueryParameters(commandQueryParameters []shared_api.Parameter, paramet
145145
}
146146

147147
// select a version from a list of versions, throws an error if no match is found.
148-
func selectVersion(versionString string, versions []shared_api.Version) (*shared_api.Version, error) {
148+
func selectVersion(selectedVersion shared_api.Version, versions []shared_api.CommandVersion) (*shared_api.CommandVersion, error) {
149149
for _, version := range versions {
150-
if version.Version == versionString {
150+
if version.Version.Equal(selectedVersion) {
151151
return &version, nil
152152
}
153153
}
154154

155-
return nil, fmt.Errorf("version '%s' not found", versionString)
155+
return nil, fmt.Errorf("version '%s' not found", selectedVersion)
156156
}
157157

158158
// generate the accept header using the given format string
159159
// try to find the content type in the list of response content types, if not found set the type to json.
160-
func acceptHeader(version *shared_api.Version, requestedContentType string) (string, error) {
160+
func acceptHeader(version *shared_api.CommandVersion, requestedContentType string) (string, error) {
161161
contentType := ""
162162
supportedTypes := make([]string, 0)
163163

@@ -176,7 +176,7 @@ func acceptHeader(version *shared_api.Version, requestedContentType string) (str
176176
return fmt.Sprintf("application/vnd.atlas.%s+%s", version.Version, contentType), nil
177177
}
178178

179-
func contentType(version *shared_api.Version) *string {
179+
func contentType(version *shared_api.CommandVersion) *string {
180180
if version.RequestContentType != "" {
181181
contentType := fmt.Sprintf("application/vnd.atlas.%s+%s", version.Version, version.RequestContentType)
182182
return &contentType

internal/api/httprequest_test.go

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func TestConvertToHttpRequest(t *testing.T) {
212212
"envelope": {"true"},
213213
"metrics": {"metric1", "metric2"},
214214
},
215-
Version: "2023-11-15",
215+
Version: shared_api.NewStableVersion(2023, 11, 15),
216216
},
217217
shouldFail: false,
218218
expectedURL: "https://base_url/api/atlas/v2/groups/abcdef1234/clusters/cluster-01/view-42/metrics/pageviews/collStats/measurements?envelope=true&metrics=metric1&metrics=metric2",
@@ -232,7 +232,7 @@ func TestConvertToHttpRequest(t *testing.T) {
232232
"groupId": {"0ff00ff00ff0"},
233233
"pretty": {"true"},
234234
},
235-
Version: "2024-08-05",
235+
Version: shared_api.NewStableVersion(2024, 8, 5),
236236
},
237237
shouldFail: false,
238238
expectedURL: "http://another_base/api/atlas/v2/groups/0ff00ff00ff0/clusters?pretty=true",
@@ -252,7 +252,7 @@ func TestConvertToHttpRequest(t *testing.T) {
252252
"groupId": {"0ff00ff00ff0"},
253253
"pretty": {"true"},
254254
},
255-
Version: "2024-08-05",
255+
Version: shared_api.NewStableVersion(2024, 8, 5),
256256
},
257257
shouldFail: true,
258258
expectedURL: "http://another_base/api/atlas/v2/groups/0ff00ff00ff0/clusters?pretty=true",
@@ -271,7 +271,7 @@ func TestConvertToHttpRequest(t *testing.T) {
271271
Parameters: map[string][]string{
272272
"pretty": {"true"},
273273
},
274-
Version: "2024-08-05",
274+
Version: shared_api.NewStableVersion(2024, 8, 5),
275275
},
276276
shouldFail: true,
277277
},
@@ -286,10 +286,50 @@ func TestConvertToHttpRequest(t *testing.T) {
286286
"groupId": {"0ff00ff00ff0"},
287287
"pretty": {"true"},
288288
},
289-
Version: "1991-05-17",
289+
Version: shared_api.NewStableVersion(1991, 5, 17),
290290
},
291291
shouldFail: true,
292292
},
293+
{
294+
name: "valid preview post request (createClusterCommand - Preview)",
295+
baseURL: "http://another_base",
296+
request: CommandRequest{
297+
Command: createClusterCommand,
298+
Content: strings.NewReader(`{"very_pretty_json":true}`),
299+
ContentType: "json",
300+
Parameters: map[string][]string{
301+
"groupId": {"0ff00ff00ff0"},
302+
"pretty": {"true"},
303+
},
304+
Version: shared_api.NewPreviewVersion(),
305+
},
306+
shouldFail: false,
307+
expectedURL: "http://another_base/api/atlas/v2/groups/0ff00ff00ff0/clusters?pretty=true",
308+
expectedHTTPVerb: http.MethodPost,
309+
expectedHTTPAcceptHeader: "application/vnd.atlas.preview+json",
310+
expectedHTTPContentTypeHeader: "application/vnd.atlas.preview+json",
311+
expectedHTTPBody: `{"very_pretty_json":true}`,
312+
},
313+
{
314+
name: "valid upcoming post request (createClusterCommand - Upcoming)",
315+
baseURL: "http://another_base",
316+
request: CommandRequest{
317+
Command: createClusterCommand,
318+
Content: strings.NewReader(`{"very_pretty_json":true}`),
319+
ContentType: "json",
320+
Parameters: map[string][]string{
321+
"groupId": {"0ff00ff00ff0"},
322+
"pretty": {"true"},
323+
},
324+
Version: shared_api.NewUpcomingVersion(2025, 1, 1),
325+
},
326+
shouldFail: false,
327+
expectedURL: "http://another_base/api/atlas/v2/groups/0ff00ff00ff0/clusters?pretty=true",
328+
expectedHTTPVerb: http.MethodPost,
329+
expectedHTTPAcceptHeader: "application/vnd.atlas.2025-01-01.upcoming+json",
330+
expectedHTTPContentTypeHeader: "application/vnd.atlas.2025-01-01.upcoming+json",
331+
expectedHTTPBody: `{"very_pretty_json":true}`,
332+
},
293333
}
294334

295335
for _, tt := range tests {
@@ -421,9 +461,9 @@ NOTE: Groups and projects are synonymous terms. Your group id is the same as you
421461
},
422462
Verb: http.MethodGet,
423463
},
424-
Versions: []shared_api.Version{
464+
Versions: []shared_api.CommandVersion{
425465
{
426-
Version: `2023-11-15`,
466+
Version: shared_api.NewStableVersion(2023, 11, 15),
427467
RequestContentType: ``,
428468
ResponseContentTypes: []string{
429469
`json`,
@@ -473,30 +513,44 @@ NOTE: Groups and projects are synonymous terms. Your group id is the same as you
473513
},
474514
Verb: http.MethodPost,
475515
},
476-
Versions: []shared_api.Version{
516+
Versions: []shared_api.CommandVersion{
517+
{
518+
Version: shared_api.NewStableVersion(2023, 1, 1),
519+
RequestContentType: `json`,
520+
ResponseContentTypes: []string{
521+
`json`,
522+
},
523+
},
524+
{
525+
Version: shared_api.NewStableVersion(2023, 2, 1),
526+
RequestContentType: `json`,
527+
ResponseContentTypes: []string{
528+
`json`,
529+
},
530+
},
477531
{
478-
Version: `2023-01-01`,
532+
Version: shared_api.NewStableVersion(2024, 8, 5),
479533
RequestContentType: `json`,
480534
ResponseContentTypes: []string{
481535
`json`,
482536
},
483537
},
484538
{
485-
Version: `2023-02-01`,
539+
Version: shared_api.NewStableVersion(2024, 10, 23),
486540
RequestContentType: `json`,
487541
ResponseContentTypes: []string{
488542
`json`,
489543
},
490544
},
491545
{
492-
Version: `2024-08-05`,
546+
Version: shared_api.NewPreviewVersion(),
493547
RequestContentType: `json`,
494548
ResponseContentTypes: []string{
495549
`json`,
496550
},
497551
},
498552
{
499-
Version: `2024-10-23`,
553+
Version: shared_api.NewUpcomingVersion(2025, 1, 1),
500554
RequestContentType: `json`,
501555
ResponseContentTypes: []string{
502556
`json`,

internal/api/interface.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type CommandRequest struct {
5151
ContentType string
5252
Format string
5353
Parameters map[string][]string
54-
Version string
54+
Version api.Version
5555
}
5656

5757
type CommandResponse struct {

internal/cli/api/api.go

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ func convertAPIToCobraCommand(command shared_api.Command) (*cobra.Command, error
137137
// This can happen when the profile contains a default version which is not supported for a specific endpoint
138138
ensureVersionIsSupported(command, &version)
139139

140+
// Print a warning if the version is a preview version
141+
printPreviewWarning(command, &version)
142+
140143
// Detect if stdout is being piped (atlas api myTag myOperationId > output.json)
141144
isPiped, err := IsStdOutPiped()
142145
if err != nil {
@@ -173,8 +176,15 @@ func convertAPIToCobraCommand(command shared_api.Command) (*cobra.Command, error
173176
return err
174177
}
175178

179+
// Convert the version string into an api version
180+
apiVersion, err := shared_api.ParseVersion(version)
181+
if err != nil {
182+
// This should never happen, the version is already validated in the prerun function
183+
return err
184+
}
185+
176186
// Convert the api command + cobra command into a api command request
177-
commandRequest, err := NewCommandRequestFromCobraCommand(cmd, command, content, format, version)
187+
commandRequest, err := NewCommandRequestFromCobraCommand(cmd, command, content, format, apiVersion)
178188
if err != nil {
179189
return err
180190
}
@@ -414,7 +424,7 @@ func defaultAPIVersion(command shared_api.Command) (string, error) {
414424
}
415425

416426
lastVersion := command.Versions[nVersions-1]
417-
return lastVersion.Version, nil
427+
return lastVersion.Version.String(), nil
418428
}
419429

420430
func remindUserToPinVersion(cmd *cobra.Command) {
@@ -433,22 +443,63 @@ func remindUserToPinVersion(cmd *cobra.Command) {
433443
}
434444
}
435445

436-
func ensureVersionIsSupported(apiCommand shared_api.Command, version *string) {
437-
for _, commandVersion := range apiCommand.Versions {
438-
if commandVersion.Version == *version {
439-
return
446+
func ensureVersionIsSupported(apiCommand shared_api.Command, versionString *string) {
447+
version, err := shared_api.ParseVersion(*versionString)
448+
449+
// If the version is valid, check if it's supported
450+
if err == nil {
451+
for _, commandVersion := range apiCommand.Versions {
452+
if commandVersion.Version.Equal(version) {
453+
return
454+
}
440455
}
441456
}
442457

443458
// if we get here it means that the picked version is not supported
444459
defaultVersion, err := defaultAPIVersion(apiCommand)
445460
// if we fail to get a version (which should never happen), then quit
446461
if err != nil {
462+
fmt.Fprintf(os.Stderr, "error in 'ensureVersionIsSupported': default version has an invalid format '%s'\n", *versionString)
447463
return
448464
}
449465

450-
fmt.Fprintf(os.Stderr, "warning: version '%s' is not supported for this endpoint, using default API version '%s'; consider pinning a version to ensure consisentcy when updating the CLI\n", *version, defaultVersion)
451-
*version = defaultVersion
466+
fmt.Fprintf(os.Stderr, "warning: version '%s' is not supported for this endpoint, using default API version '%s'; consider pinning a version to ensure consisentcy when updating the CLI\n", *versionString, defaultVersion)
467+
*versionString = defaultVersion
468+
}
469+
470+
func printPreviewWarning(apiCommand shared_api.Command, versionString *string) {
471+
version, err := shared_api.ParseVersion(*versionString)
472+
473+
// If the version is invalid return, this should never happen
474+
if err != nil {
475+
fmt.Fprintf(os.Stderr, "error in 'printPreviewWarning': received an invalid version '%s'\n", *versionString)
476+
return
477+
}
478+
479+
// If the version is not a preview version, return
480+
if version.StabilityLevel() != shared_api.StabilityLevelPreview {
481+
return
482+
}
483+
484+
// Find the version in the command versions
485+
var commandVersion *shared_api.CommandVersion
486+
for _, cv := range apiCommand.Versions {
487+
if cv.Version.Equal(version) {
488+
commandVersion = &cv
489+
break
490+
}
491+
}
492+
493+
// If the version is not found, return (should also never happen)
494+
if commandVersion == nil {
495+
return
496+
}
497+
498+
if commandVersion.PublicPreview {
499+
fmt.Fprintf(os.Stderr, "warning: you've selected a public preview version of the endpoint, this version is subject to breaking changes.\n")
500+
} else {
501+
fmt.Fprintf(os.Stderr, "warning: you've selected a private preview version of the endpoint, this version might not be available for your account and is subject to breaking changes.\n")
502+
}
452503
}
453504

454505
func needsFileFlag(apiCommand shared_api.Command) bool {
@@ -474,7 +525,7 @@ func addVersionFlag(cmd *cobra.Command, apiCommand shared_api.Command, version *
474525
// Create a unique list of all supported versions
475526
versions := make(map[string]struct{}, 0)
476527
for _, version := range apiCommand.Versions {
477-
versions[version.Version] = struct{}{}
528+
versions[version.Version.String()] = struct{}{}
478529
}
479530

480531
// Convert the keys of the map into a list

internal/cli/api/command_request.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"github.com/spf13/pflag"
2424
)
2525

26-
func NewCommandRequestFromCobraCommand(cobraCommand *cobra.Command, apiCommand shared_api.Command, content io.Reader, format string, version string) (*api.CommandRequest, error) {
26+
func NewCommandRequestFromCobraCommand(cobraCommand *cobra.Command, apiCommand shared_api.Command, content io.Reader, format string, version shared_api.Version) (*api.CommandRequest, error) {
2727
return &api.CommandRequest{
2828
Command: apiCommand,
2929
Content: content,

tools/cmd/api-generator/commands.go.tmpl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,13 @@ var Commands = shared_api.GroupedAndSortedCommands{
7070
},
7171
Verb: {{ .RequestParameters.Verb }},
7272
},
73-
Versions: []shared_api.Version{
73+
Versions: []shared_api.CommandVersion{
7474
{{- range .Versions }}
7575
{
76-
Version: `{{ .Version }}`,
76+
Version: {{ createVersion .Version }},
77+
{{- if .PublicPreview }}
78+
PublicPreview: true,
79+
{{- end}}
7780
RequestContentType: `{{ .RequestContentType }}`,
7881
ResponseContentTypes: []string{
7982
{{- range .ResponseContentTypes }}
@@ -86,7 +89,7 @@ var Commands = shared_api.GroupedAndSortedCommands{
8689
Watcher: &shared_api.WatcherProperties{
8790
Get: shared_api.WatcherGetProperties{
8891
OperationID: `{{ .Watcher.Get.OperationID }}`,
89-
Version: `{{ .Watcher.Get.Version }}`,
92+
Version: {{ createVersion .Watcher.Get.Version }},
9093
Params: map[string]string{
9194
{{- range $k, $v := .Watcher.Get.Params }}
9295
`{{ $k }}`: `{{ $v }}`,

0 commit comments

Comments
 (0)