Skip to content

Commit 64f2e7c

Browse files
authored
CLOUDP-297408 Apply cli specific extensions into generated commands (#3609)
1 parent 7c5e709 commit 64f2e7c

File tree

4 files changed

+215
-62
lines changed

4 files changed

+215
-62
lines changed

internal/cli/api/api.go

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ func convertAPIToCobraCommand(command api.Command) (*cobra.Command, error) {
100100
}
101101

102102
cmd := &cobra.Command{
103-
Use: commandName,
104-
Short: shortDescription,
105-
Long: longDescription,
103+
Use: commandName,
104+
Aliases: command.Aliases,
105+
Short: shortDescription,
106+
Long: longDescription,
106107
PreRunE: func(cmd *cobra.Command, _ []string) error {
107108
// Go through all commands that have not been touched/modified by the user and try to populate them from the users profile
108109
// Common usecases:
@@ -225,16 +226,8 @@ func convertAPIToCobraCommand(command api.Command) (*cobra.Command, error) {
225226

226227
func addParameters(cmd *cobra.Command, parameters []api.Parameter) error {
227228
for _, parameter := range parameters {
228-
if cmd.Flag(parameter.Name) != nil {
229-
// this should never happen, the api command generation tool should cover this
230-
return fmt.Errorf("there is already a parameter with that name, name='%s'", parameter.Name)
231-
}
232-
233-
parameterFlag := parameterToPFlag(parameter)
234-
cmd.Flags().AddFlag(parameterFlag)
235-
236-
if parameter.Required {
237-
_ = cmd.MarkFlagRequired(parameterFlag.Name)
229+
if err := addFlag(cmd, parameter); err != nil {
230+
return err
238231
}
239232
}
240233

internal/cli/api/flags.go

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,61 +15,60 @@
1515
package api
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"strings"
2021

2122
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/api"
22-
"github.com/spf13/pflag"
23+
"github.com/spf13/cobra"
2324
)
2425

25-
func parameterToPFlag(parameter api.Parameter) *pflag.Flag {
26+
var (
27+
errUnsupportedType = errors.New("unsupported parameter type")
28+
errFlagAlreadyExists = errors.New("parameter already exists")
29+
)
30+
31+
func addFlag(cmd *cobra.Command, parameter api.Parameter) error {
2632
name := parameter.Name
33+
34+
if cmd.Flag(name) != nil {
35+
// this should never happen, the api command generation tool should cover this
36+
return fmt.Errorf("%w: %s", errFlagAlreadyExists, name)
37+
}
38+
2739
shortDescription := flagDescription(parameter.Description)
2840

2941
if parameter.Type.IsArray {
3042
switch parameter.Type.Type {
3143
case api.TypeString:
32-
return createFlag(func(f *pflag.FlagSet) {
33-
f.StringArray(name, make([]string, 0), shortDescription)
34-
})
44+
cmd.Flags().StringArrayP(name, parameter.Short, nil, shortDescription)
3545
case api.TypeInt:
36-
return createFlag(func(f *pflag.FlagSet) {
37-
f.Int32Slice(name, make([]int32, 0), shortDescription)
38-
})
46+
cmd.Flags().Int32SliceP(name, parameter.Short, nil, shortDescription)
3947
case api.TypeBool:
40-
return createFlag(func(f *pflag.FlagSet) {
41-
f.BoolSlice(name, make([]bool, 0), shortDescription)
42-
})
48+
cmd.Flags().BoolSliceP(name, parameter.Short, nil, shortDescription)
49+
default:
50+
return fmt.Errorf("%w: %s", errUnsupportedType, parameter.Type.Type)
4351
}
4452
} else {
4553
switch parameter.Type.Type {
4654
case api.TypeString:
47-
return createFlag(func(f *pflag.FlagSet) {
48-
f.String(name, "", shortDescription)
49-
})
55+
cmd.Flags().StringP(name, parameter.Short, "", shortDescription)
5056
case api.TypeInt:
51-
return createFlag(func(f *pflag.FlagSet) {
52-
f.Int(name, 0, shortDescription)
53-
})
57+
cmd.Flags().IntP(name, parameter.Short, 0, shortDescription)
5458
case api.TypeBool:
55-
return createFlag(func(f *pflag.FlagSet) {
56-
f.Bool(name, false, shortDescription)
57-
})
59+
cmd.Flags().BoolP(name, parameter.Short, false, shortDescription)
60+
default:
61+
return fmt.Errorf("%w: %s", errUnsupportedType, parameter.Type.Type)
5862
}
5963
}
6064

61-
// should never happen, can only happen if someone adds a new api.ParameterConcreteType
62-
panic(fmt.Sprintf("unsupported parameter type: %s", parameter.Type.Type))
63-
}
65+
if parameter.Required {
66+
if err := cmd.MarkFlagRequired(name); err != nil {
67+
return err
68+
}
69+
}
6470

65-
func createFlag(factory func(f *pflag.FlagSet)) *pflag.Flag {
66-
flagSet := pflag.NewFlagSet("temp", pflag.ContinueOnError)
67-
factory(flagSet)
68-
var output *pflag.Flag
69-
flagSet.VisitAll(func(f *pflag.Flag) {
70-
output = f
71-
})
72-
return output
71+
return nil
7372
}
7473

7574
func flagDescription(description string) string {

tools/api-generator/convert.go

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ func specToCommands(spec *openapi3.T) (api.GroupedAndSortedCommands, error) {
3838
if err != nil {
3939
return nil, fmt.Errorf("failed to convert operation to command: %w", err)
4040
}
41+
if command == nil {
42+
continue
43+
}
4144

4245
if len(operation.Tags) != 1 {
4346
return nil, fmt.Errorf("expect every operation to have exactly 1 tag, got: %v", len(operation.Tags))
@@ -75,7 +78,40 @@ func specToCommands(spec *openapi3.T) (api.GroupedAndSortedCommands, error) {
7578
return sortedGroups, nil
7679
}
7780

81+
func extractExtensionsFromOperation(operation *openapi3.Operation) (bool, string, []string) {
82+
skip := false
83+
operationID := operation.OperationID
84+
var aliases []string
85+
86+
if extensions, okExtensions := operation.Extensions["x-xgen-atlascli"].(map[string]any); okExtensions && extensions != nil {
87+
if extSkip, okSkip := extensions["skip"].(bool); okSkip && extSkip {
88+
skip = extSkip
89+
}
90+
91+
if extAliases, okExtAliases := extensions["command-aliases"].([]any); okExtAliases && extAliases != nil {
92+
for _, alias := range extAliases {
93+
if sAlias, ok := alias.(string); ok && sAlias != "" {
94+
aliases = append(aliases, sAlias)
95+
}
96+
}
97+
}
98+
99+
if overrides := extractOverrides(operation.Extensions); overrides != nil {
100+
if overriddenOperationID, ok := overrides["operationId"].(string); ok && overriddenOperationID != "" {
101+
operationID = overriddenOperationID
102+
}
103+
}
104+
}
105+
106+
return skip, operationID, aliases
107+
}
108+
78109
func operationToCommand(path, verb string, operation *openapi3.Operation) (*api.Command, error) {
110+
skip, operationID, aliases := extractExtensionsFromOperation(operation)
111+
if skip {
112+
return nil, nil
113+
}
114+
79115
httpVerb, err := api.ToHTTPVerb(verb)
80116
if err != nil {
81117
return nil, err
@@ -96,24 +132,6 @@ func operationToCommand(path, verb string, operation *openapi3.Operation) (*api.
96132
return nil, fmt.Errorf("failed to clean description: %w", err)
97133
}
98134

99-
operationID := operation.OperationID
100-
if overrides := extractOverrides(operation.Extensions); overrides != nil {
101-
if overriddenOperationID, ok := overrides["operationId"].(string); ok && overriddenOperationID != "" {
102-
operationID = overriddenOperationID
103-
}
104-
}
105-
106-
var aliases []string
107-
if extensions, okExtensions := operation.Extensions["x-xgen-atlascli"].(map[string]any); okExtensions && extensions != nil {
108-
if extAliases, okExtAliases := extensions["command-aliases"].([]any); okExtAliases && extAliases != nil {
109-
for _, alias := range extAliases {
110-
if sAlias, ok := alias.(string); ok && sAlias != "" {
111-
aliases = append(aliases, sAlias)
112-
}
113-
}
114-
}
115-
}
116-
117135
command := api.Command{
118136
OperationID: operationID,
119137
Aliases: aliases,

tools/api-generator/fixtures/06-spec-with-cli-specific-extensions.yaml

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,50 @@ tags:
3131
name: Clusters
3232
paths:
3333
/api/atlas/v2/groups/{groupId}/clusters:
34+
get:
35+
description: Returns the details for all clusters in the specific project to which you have access. Clusters contain a group of hosts that maintain the same data set. The response includes clusters with asymmetrically-sized shards. To use this resource, the requesting API Key must have the Project Read Only role. This feature is not available for serverless clusters.
36+
operationId: listClusters
37+
x-xgen-atlascli:
38+
skip: true
39+
parameters:
40+
- $ref: '#/components/parameters/envelope'
41+
- $ref: '#/components/parameters/groupId'
42+
- $ref: '#/components/parameters/includeCount'
43+
- $ref: '#/components/parameters/itemsPerPage'
44+
- $ref: '#/components/parameters/pageNum'
45+
- $ref: '#/components/parameters/pretty'
46+
- description: Flag that indicates whether to return Clusters with retain backups.
47+
in: query
48+
name: includeDeletedWithRetainedBackups
49+
schema:
50+
default: false
51+
type: boolean
52+
responses:
53+
'200':
54+
content:
55+
application/vnd.atlas.2023-01-01+json:
56+
schema:
57+
$ref: '#/components/schemas/PaginatedLegacyClusterView'
58+
x-sunset: '2025-06-01'
59+
x-xgen-version: '2023-01-01'
60+
application/vnd.atlas.2023-02-01+json:
61+
schema:
62+
$ref: '#/components/schemas/PaginatedAdvancedClusterDescriptionView'
63+
x-sunset: '2026-03-01'
64+
x-xgen-version: '2023-02-01'
65+
application/vnd.atlas.2024-08-05+json:
66+
schema:
67+
$ref: '#/components/schemas/PaginatedClusterDescription20240805'
68+
x-xgen-version: '2024-08-05'
69+
description: OK
70+
'401':
71+
$ref: '#/components/responses/unauthorized'
72+
'500':
73+
$ref: '#/components/responses/internalServerError'
74+
summary: Return All Clusters in One Project
75+
tags:
76+
- Clusters
77+
x-xgen-owner-team: Atlas Dedicated
3478
post:
3579
description: Creates one cluster in the specified project. Clusters contain a group of hosts that maintain the same data set. This resource can create clusters with asymmetrically-sized shards. Each project supports up to 25 database deployments. To use this resource, the requesting API Key must have the Project Owner role. This feature is not available for serverless clusters.
3680
x-xgen-atlascli:
@@ -263,6 +307,33 @@ components:
263307
minLength: 24
264308
pattern: ^([a-f0-9]{24})$
265309
type: string
310+
includeCount:
311+
description: Flag that indicates whether the response returns the total number of items (**totalCount**) in the response.
312+
in: query
313+
name: includeCount
314+
schema:
315+
default: true
316+
example: true
317+
type: boolean
318+
itemsPerPage:
319+
description: Number of items that the response returns per page.
320+
in: query
321+
name: itemsPerPage
322+
schema:
323+
default: 100
324+
example: 100
325+
maximum: 500
326+
minimum: 1
327+
type: integer
328+
pageNum:
329+
description: Number of the page that displays the current set of the total objects that the response returns.
330+
in: query
331+
name: pageNum
332+
schema:
333+
default: 1
334+
example: 1
335+
minimum: 1
336+
type: integer
266337
pretty:
267338
description: Flag that indicates whether the response body should be in the <a href="https://en.wikipedia.org/wiki/Prettyprint" target="_blank" rel="noopener noreferrer">prettyprint</a> format.
268339
in: query
@@ -6252,6 +6323,78 @@ components:
62526323
uniqueItems: true
62536324
readOnly: true
62546325
type: object
6326+
PaginatedAdvancedClusterDescriptionView:
6327+
properties:
6328+
links:
6329+
description: List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships.
6330+
externalDocs:
6331+
description: Web Linking Specification (RFC 5988)
6332+
url: https://datatracker.ietf.org/doc/html/rfc5988
6333+
items:
6334+
$ref: '#/components/schemas/Link'
6335+
readOnly: true
6336+
type: array
6337+
results:
6338+
description: List of returned documents that MongoDB Cloud provides when completing this request.
6339+
items:
6340+
$ref: '#/components/schemas/AdvancedClusterDescription'
6341+
readOnly: true
6342+
type: array
6343+
totalCount:
6344+
description: Total number of documents available. MongoDB Cloud omits this value if `includeCount` is set to `false`.
6345+
format: int32
6346+
minimum: 0
6347+
readOnly: true
6348+
type: integer
6349+
type: object
6350+
PaginatedClusterDescription20240805:
6351+
properties:
6352+
links:
6353+
description: List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships.
6354+
externalDocs:
6355+
description: Web Linking Specification (RFC 5988)
6356+
url: https://datatracker.ietf.org/doc/html/rfc5988
6357+
items:
6358+
$ref: '#/components/schemas/Link'
6359+
readOnly: true
6360+
type: array
6361+
results:
6362+
description: List of returned documents that MongoDB Cloud provides when completing this request.
6363+
items:
6364+
$ref: '#/components/schemas/ClusterDescription20240805'
6365+
readOnly: true
6366+
type: array
6367+
totalCount:
6368+
description: Total number of documents available. MongoDB Cloud omits this value if `includeCount` is set to `false`.
6369+
format: int32
6370+
minimum: 0
6371+
readOnly: true
6372+
type: integer
6373+
type: object
6374+
PaginatedLegacyClusterView:
6375+
properties:
6376+
links:
6377+
description: List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships.
6378+
externalDocs:
6379+
description: Web Linking Specification (RFC 5988)
6380+
url: https://datatracker.ietf.org/doc/html/rfc5988
6381+
items:
6382+
$ref: '#/components/schemas/Link'
6383+
readOnly: true
6384+
type: array
6385+
results:
6386+
description: List of returned documents that MongoDB Cloud provides when completing this request.
6387+
items:
6388+
$ref: '#/components/schemas/LegacyAtlasCluster'
6389+
readOnly: true
6390+
type: array
6391+
totalCount:
6392+
description: Total number of documents available. MongoDB Cloud omits this value if `includeCount` is set to `false`.
6393+
format: int32
6394+
minimum: 0
6395+
readOnly: true
6396+
type: integer
6397+
type: object
62556398
PeriodicCpsSnapshotSource:
62566399
allOf:
62576400
- $ref: '#/components/schemas/IngestionSource'

0 commit comments

Comments
 (0)