Skip to content

CLOUDP-325300: [AtlasCLI] Support preview and upcoming stability levels for L1 commands #3984

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 14 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,202 changes: 1,221 additions & 981 deletions internal/api/commands.go

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions internal/api/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,16 @@ func TestExecutorHappyPathNoLogging(t *testing.T) {
RequestParameters: api.RequestParameters{
URL: "/test/url",
},
Versions: []api.Version{{
Version: "1991-05-17",
Versions: []api.CommandVersion{{
Version: api.NewStableVersion(1991, 5, 17),
RequestContentType: "json",
ResponseContentTypes: []string{"json"},
}},
},
ContentType: "json",
Format: "json",
Parameters: nil,
Version: "1991-05-17",
Version: api.NewStableVersion(1991, 5, 17),
}
response, err := executor.ExecuteCommand(t.Context(), commandRequest)

Expand Down Expand Up @@ -120,16 +120,16 @@ func TestExecutorHappyPathDebugLogging(t *testing.T) {
RequestParameters: api.RequestParameters{
URL: "/test/url",
},
Versions: []api.Version{{
Version: "1991-05-17",
Versions: []api.CommandVersion{{
Version: api.NewStableVersion(1991, 5, 17),
RequestContentType: "json",
ResponseContentTypes: []string{"json"},
}},
},
ContentType: "json",
Format: "json",
Parameters: nil,
Version: "1991-05-17",
Version: api.NewStableVersion(1991, 5, 17),
}
response, err := executor.ExecuteCommand(t.Context(), commandRequest)

Expand Down
10 changes: 5 additions & 5 deletions internal/api/httprequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,19 +145,19 @@ func buildQueryParameters(commandQueryParameters []shared_api.Parameter, paramet
}

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

return nil, fmt.Errorf("version '%s' not found", versionString)
return nil, fmt.Errorf("version '%s' not found", selectedVersion)
}

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

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

func contentType(version *shared_api.Version) *string {
func contentType(version *shared_api.CommandVersion) *string {
if version.RequestContentType != "" {
contentType := fmt.Sprintf("application/vnd.atlas.%s+%s", version.Version, version.RequestContentType)
return &contentType
Expand Down
78 changes: 66 additions & 12 deletions internal/api/httprequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func TestConvertToHttpRequest(t *testing.T) {
"envelope": {"true"},
"metrics": {"metric1", "metric2"},
},
Version: "2023-11-15",
Version: shared_api.NewStableVersion(2023, 11, 15),
},
shouldFail: false,
expectedURL: "https://base_url/api/atlas/v2/groups/abcdef1234/clusters/cluster-01/view-42/metrics/pageviews/collStats/measurements?envelope=true&metrics=metric1&metrics=metric2",
Expand All @@ -232,7 +232,7 @@ func TestConvertToHttpRequest(t *testing.T) {
"groupId": {"0ff00ff00ff0"},
"pretty": {"true"},
},
Version: "2024-08-05",
Version: shared_api.NewStableVersion(2024, 8, 5),
},
shouldFail: false,
expectedURL: "http://another_base/api/atlas/v2/groups/0ff00ff00ff0/clusters?pretty=true",
Expand All @@ -252,7 +252,7 @@ func TestConvertToHttpRequest(t *testing.T) {
"groupId": {"0ff00ff00ff0"},
"pretty": {"true"},
},
Version: "2024-08-05",
Version: shared_api.NewStableVersion(2024, 8, 5),
},
shouldFail: true,
expectedURL: "http://another_base/api/atlas/v2/groups/0ff00ff00ff0/clusters?pretty=true",
Expand All @@ -271,7 +271,7 @@ func TestConvertToHttpRequest(t *testing.T) {
Parameters: map[string][]string{
"pretty": {"true"},
},
Version: "2024-08-05",
Version: shared_api.NewStableVersion(2024, 8, 5),
},
shouldFail: true,
},
Expand All @@ -286,10 +286,50 @@ func TestConvertToHttpRequest(t *testing.T) {
"groupId": {"0ff00ff00ff0"},
"pretty": {"true"},
},
Version: "1991-05-17",
Version: shared_api.NewStableVersion(1991, 5, 17),
},
shouldFail: true,
},
{
name: "valid preview post request (createClusterCommand - Preview)",
baseURL: "http://another_base",
request: CommandRequest{
Command: createClusterCommand,
Content: strings.NewReader(`{"very_pretty_json":true}`),
ContentType: "json",
Parameters: map[string][]string{
"groupId": {"0ff00ff00ff0"},
"pretty": {"true"},
},
Version: shared_api.NewPreviewVersion(),
},
shouldFail: false,
expectedURL: "http://another_base/api/atlas/v2/groups/0ff00ff00ff0/clusters?pretty=true",
expectedHTTPVerb: http.MethodPost,
expectedHTTPAcceptHeader: "application/vnd.atlas.preview+json",
expectedHTTPContentTypeHeader: "application/vnd.atlas.preview+json",
expectedHTTPBody: `{"very_pretty_json":true}`,
},
{
name: "valid upcoming post request (createClusterCommand - Upcoming)",
baseURL: "http://another_base",
request: CommandRequest{
Command: createClusterCommand,
Content: strings.NewReader(`{"very_pretty_json":true}`),
ContentType: "json",
Parameters: map[string][]string{
"groupId": {"0ff00ff00ff0"},
"pretty": {"true"},
},
Version: shared_api.NewUpcomingVersion(2025, 1, 1),
},
shouldFail: false,
expectedURL: "http://another_base/api/atlas/v2/groups/0ff00ff00ff0/clusters?pretty=true",
expectedHTTPVerb: http.MethodPost,
expectedHTTPAcceptHeader: "application/vnd.atlas.2025-01-01.upcoming+json",
expectedHTTPContentTypeHeader: "application/vnd.atlas.2025-01-01.upcoming+json",
expectedHTTPBody: `{"very_pretty_json":true}`,
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -421,9 +461,9 @@ NOTE: Groups and projects are synonymous terms. Your group id is the same as you
},
Verb: http.MethodGet,
},
Versions: []shared_api.Version{
Versions: []shared_api.CommandVersion{
{
Version: `2023-11-15`,
Version: shared_api.NewStableVersion(2023, 11, 15),
RequestContentType: ``,
ResponseContentTypes: []string{
`json`,
Expand Down Expand Up @@ -473,30 +513,44 @@ NOTE: Groups and projects are synonymous terms. Your group id is the same as you
},
Verb: http.MethodPost,
},
Versions: []shared_api.Version{
Versions: []shared_api.CommandVersion{
{
Version: shared_api.NewStableVersion(2023, 1, 1),
RequestContentType: `json`,
ResponseContentTypes: []string{
`json`,
},
},
{
Version: shared_api.NewStableVersion(2023, 2, 1),
RequestContentType: `json`,
ResponseContentTypes: []string{
`json`,
},
},
{
Version: `2023-01-01`,
Version: shared_api.NewStableVersion(2024, 8, 5),
RequestContentType: `json`,
ResponseContentTypes: []string{
`json`,
},
},
{
Version: `2023-02-01`,
Version: shared_api.NewStableVersion(2024, 10, 23),
RequestContentType: `json`,
ResponseContentTypes: []string{
`json`,
},
},
{
Version: `2024-08-05`,
Version: shared_api.NewPreviewVersion(),
RequestContentType: `json`,
ResponseContentTypes: []string{
`json`,
},
},
{
Version: `2024-10-23`,
Version: shared_api.NewUpcomingVersion(2025, 1, 1),
RequestContentType: `json`,
ResponseContentTypes: []string{
`json`,
Expand Down
2 changes: 1 addition & 1 deletion internal/api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type CommandRequest struct {
ContentType string
Format string
Parameters map[string][]string
Version string
Version api.Version
}

type CommandResponse struct {
Expand Down
69 changes: 60 additions & 9 deletions internal/cli/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ func convertAPIToCobraCommand(command shared_api.Command) (*cobra.Command, error
// This can happen when the profile contains a default version which is not supported for a specific endpoint
ensureVersionIsSupported(command, &version)

// Print a warning if the version is a preview version
printPreviewWarning(command, &version)

// Detect if stdout is being piped (atlas api myTag myOperationId > output.json)
isPiped, err := IsStdOutPiped()
if err != nil {
Expand Down Expand Up @@ -173,8 +176,15 @@ func convertAPIToCobraCommand(command shared_api.Command) (*cobra.Command, error
return err
}

// Convert the version string into an api version
apiVersion, err := shared_api.ParseVersion(version)
if err != nil {
// This should never happen, the version is already validated in the prerun function
return err
}

// Convert the api command + cobra command into a api command request
commandRequest, err := NewCommandRequestFromCobraCommand(cmd, command, content, format, version)
commandRequest, err := NewCommandRequestFromCobraCommand(cmd, command, content, format, apiVersion)
if err != nil {
return err
}
Expand Down Expand Up @@ -414,7 +424,7 @@ func defaultAPIVersion(command shared_api.Command) (string, error) {
}

lastVersion := command.Versions[nVersions-1]
return lastVersion.Version, nil
return lastVersion.Version.String(), nil
}

func remindUserToPinVersion(cmd *cobra.Command) {
Expand All @@ -433,22 +443,63 @@ func remindUserToPinVersion(cmd *cobra.Command) {
}
}

func ensureVersionIsSupported(apiCommand shared_api.Command, version *string) {
for _, commandVersion := range apiCommand.Versions {
if commandVersion.Version == *version {
return
func ensureVersionIsSupported(apiCommand shared_api.Command, versionString *string) {
version, err := shared_api.ParseVersion(*versionString)

// If the version is valid, check if it's supported
if err == nil {
for _, commandVersion := range apiCommand.Versions {
if commandVersion.Version.Equal(version) {
return
}
}
}

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

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)
*version = defaultVersion
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)
*versionString = defaultVersion
}

func printPreviewWarning(apiCommand shared_api.Command, versionString *string) {
version, err := shared_api.ParseVersion(*versionString)

// If the version is invalid return, this should never happen
if err != nil {
fmt.Fprintf(os.Stderr, "error in 'printPreviewWarning': received an invalid version '%s'\n", *versionString)
return
}

// If the version is not a preview version, return
if version.StabilityLevel() != shared_api.StabilityLevelPreview {
return
}

// Find the version in the command versions
var commandVersion *shared_api.CommandVersion
for _, cv := range apiCommand.Versions {
if cv.Version.Equal(version) {
commandVersion = &cv
break
}
}

// If the version is not found, return (should also never happen)
if commandVersion == nil {
return
}

if commandVersion.PublicPreview {
fmt.Fprintf(os.Stderr, "warning: you've selected a public preview version of the endpoint, this version is subject to breaking changes.\n")
} else {
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")
}
Comment on lines +498 to +502
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super clear messages 👍

}

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

// Convert the keys of the map into a list
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/api/command_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"github.com/spf13/pflag"
)

func NewCommandRequestFromCobraCommand(cobraCommand *cobra.Command, apiCommand shared_api.Command, content io.Reader, format string, version string) (*api.CommandRequest, error) {
func NewCommandRequestFromCobraCommand(cobraCommand *cobra.Command, apiCommand shared_api.Command, content io.Reader, format string, version shared_api.Version) (*api.CommandRequest, error) {
return &api.CommandRequest{
Command: apiCommand,
Content: content,
Expand Down
9 changes: 6 additions & 3 deletions tools/cmd/api-generator/commands.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,13 @@ var Commands = shared_api.GroupedAndSortedCommands{
},
Verb: {{ .RequestParameters.Verb }},
},
Versions: []shared_api.Version{
Versions: []shared_api.CommandVersion{
{{- range .Versions }}
{
Version: `{{ .Version }}`,
Version: {{ createVersion .Version }},
{{- if .PublicPreview }}
PublicPreview: true,
{{- end}}
RequestContentType: `{{ .RequestContentType }}`,
ResponseContentTypes: []string{
{{- range .ResponseContentTypes }}
Expand All @@ -86,7 +89,7 @@ var Commands = shared_api.GroupedAndSortedCommands{
Watcher: &shared_api.WatcherProperties{
Get: shared_api.WatcherGetProperties{
OperationID: `{{ .Watcher.Get.OperationID }}`,
Version: `{{ .Watcher.Get.Version }}`,
Version: {{ createVersion .Watcher.Get.Version }},
Params: map[string]string{
{{- range $k, $v := .Watcher.Get.Params }}
`{{ $k }}`: `{{ $v }}`,
Expand Down
Loading
Loading