• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb / mongodb-atlas-cli / 28581285639

02 Jul 2026 09:53AM UTC coverage: 63.044% (-1.3%) from 64.305%
28581285639

push

github

web-flow
build(deps): bump the cloud-providers group with 3 updates (#4631)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: apix-bot[bot] <168195273+apix-bot[bot]@users.noreply.github.com>

21128 of 33513 relevant lines covered (63.04%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.71
/internal/cli/api/api.go
1
// Copyright 2024 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

15
package api
16

17
import (
18
        "bytes"
19
        "errors"
20
        "fmt"
21
        "io"
22
        "os"
23
        "slices"
24
        "strings"
25
        "time"
26

27
        "github.com/iancoleman/strcase"
28
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/api"
29
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli"
30
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/flag"
31
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/log"
32
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/usage"
33
        shared_api "github.com/mongodb/mongodb-atlas-cli/atlascli/tools/shared/api"
34
        "github.com/spf13/cobra"
35
        "github.com/spf13/pflag"
36
)
37

38
var (
39
        ErrFailedToSetUntouchedFlags         = errors.New("failed to set untouched flags")
40
        ErrServerReturnedAnErrorResponseCode = errors.New("server returned an error response code")
41
        ErrAPICommandsHasNoVersions          = errors.New("api command has no versions")
42
        ErrFormattingOutput                  = errors.New("error formatting output")
43
        ErrRunningWatcher                    = errors.New("error while running watcher")
44
        BinaryOutputTypes                    = []string{"gzip"}
45
)
46

47
func Builder() *cobra.Command {
1✔
48
        apiCmd := createRootAPICommand()
1✔
49

1✔
50
        for _, tag := range api.Commands {
2✔
51
                tagCmd := createAPICommandGroupToCobraCommand(tag)
1✔
52

1✔
53
                for _, command := range tag.Commands {
2✔
54
                        cobraCommand, err := convertAPIToCobraCommand(command)
1✔
55
                        // err should always be nil, errors happening here should be covered by the generate api commands tool
1✔
56
                        // if err != nil there is a bug in the converter tool
1✔
57
                        if err != nil {
1✔
58
                                _, _ = log.Warningf("failed to add command for operationId: %s, err: %s", command.OperationID, err)
×
59
                                continue
×
60
                        }
61

62
                        tagCmd.AddCommand(cobraCommand)
1✔
63
                }
64

65
                apiCmd.AddCommand(tagCmd)
1✔
66
        }
67

68
        return apiCmd
1✔
69
}
70

71
func createRootAPICommand() *cobra.Command {
1✔
72
        rootCmd := &cobra.Command{
1✔
73
                Use:   "api",
1✔
74
                Short: `Access all features of the Atlas Administration API through the Atlas CLI by using the 'atlas api <tag> <operationId>' command.`,
1✔
75
                Long: `This feature streamlines script development by letting you interact directly with any Atlas Administration API endpoint through the Atlas CLI.
1✔
76

1✔
77
For more information on
1✔
78
- Atlas Administration API see: https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/
1✔
79
- Getting started with the Atlas Administration API: https://www.mongodb.com/docs/atlas/configure-api-access/#std-label-atlas-admin-api-access`,
1✔
80
        }
1✔
81

1✔
82
        rootCmd.SetHelpTemplate(cli.APICommandHelpTemplate)
1✔
83

1✔
84
        return rootCmd
1✔
85
}
1✔
86

87
func createAPICommandGroupToCobraCommand(group shared_api.Group) *cobra.Command {
1✔
88
        groupName := strcase.ToLowerCamel(group.Name)
1✔
89
        shortDescription, longDescription := splitShortAndLongDescription(group.Description)
1✔
90

1✔
91
        return &cobra.Command{
1✔
92
                Use:   groupName,
1✔
93
                Short: shortDescription,
1✔
94
                Long:  longDescription,
1✔
95
        }
1✔
96
}
1✔
97

98
//nolint:gocyclo
99
func convertAPIToCobraCommand(command shared_api.Command) (*cobra.Command, error) {
1✔
100
        // command properties
1✔
101
        commandName := strcase.ToLowerCamel(command.OperationID)
1✔
102
        commandOperationID := command.OperationID
1✔
103
        commandAliases := command.Aliases
1✔
104

1✔
105
        if command.ShortOperationID != "" {
2✔
106
                // Add original operation ID to aliases
1✔
107
                commandAliases = append(commandAliases, commandName)
1✔
108
                // Use shortOperationID as the command name
1✔
109
                commandName = strcase.ToLowerCamel(command.ShortOperationID)
1✔
110
        }
1✔
111

112
        shortDescription, longDescription := splitShortAndLongDescription(command.Description)
1✔
113

1✔
114
        // flag values
1✔
115
        file := ""
1✔
116
        format := ""
1✔
117
        outputFile := ""
1✔
118
        version, err := defaultAPIVersion(command)
1✔
119
        watch := false
1✔
120
        watchTimeout := int64(0)
1✔
121
        if err != nil {
1✔
122
                return nil, err
×
123
        }
×
124

125
        cmd := &cobra.Command{
1✔
126
                Use:     commandName,
1✔
127
                Aliases: commandAliases,
1✔
128
                Short:   shortDescription,
1✔
129
                Long:    longDescription,
1✔
130
                Annotations: map[string]string{
1✔
131
                        "operationId": commandOperationID,
1✔
132
                },
1✔
133
                PreRunE: func(cmd *cobra.Command, _ []string) error {
2✔
134
                        // Go through all commands that have not been touched/modified by the user and try to populate them from the users profile
1✔
135
                        // Common usecases:
1✔
136
                        // - set orgId
1✔
137
                        // - set projectId
1✔
138
                        // - default api version
1✔
139
                        if err := setUnTouchedFlags(NewProfileFlagValueProviderForDefaultProfile(), cmd); err != nil {
1✔
140
                                return errors.Join(ErrFailedToSetUntouchedFlags, err)
×
141
                        }
×
142

143
                        // Remind the user to pin their api command to a specific version to avoid breaking changes
144
                        remindUserToPinVersion(cmd)
1✔
145

1✔
146
                        // Reset version to default if unsupported version was selected
1✔
147
                        // This can happen when the profile contains a default version which is not supported for a specific endpoint
1✔
148
                        ensureVersionIsSupported(command, &version)
1✔
149

1✔
150
                        // Print a warning if the version is a preview version
1✔
151
                        printPreviewWarning(command, &version)
1✔
152

1✔
153
                        // Print a warning if the version has a sunset date or is deprecated
1✔
154
                        printDeprecatedVersionWarning(command, &version)
1✔
155

1✔
156
                        // Detect if stdout is being piped (atlas api myTag myOperationId > output.json)
1✔
157
                        isPiped, err := IsStdOutPiped()
1✔
158
                        if err != nil {
1✔
159
                                return err
×
160
                        }
×
161

162
                        // If the selected output format is binary and stdout is not being piped, mark output as required
163
                        // This ensures that the console isn't flooded with binary contents (for example gzip contents)
164
                        if slices.Contains(BinaryOutputTypes, format) && !isPiped {
1✔
165
                                if err := cmd.MarkFlagRequired(flag.Output); err != nil {
×
166
                                        return err
×
167
                                }
×
168
                        }
169

170
                        return nil
1✔
171
                },
172
                RunE: func(cmd *cobra.Command, _ []string) error {
1✔
173
                        // Get the request input if needed
1✔
174
                        // This is needed for most PATCH/POST/PUT requests
1✔
175
                        var content io.ReadCloser
1✔
176
                        if needsFileFlag(command) {
1✔
177
                                content, err = handleInput(cmd)
×
178
                                if err != nil {
×
179
                                        return err
×
180
                                }
×
181
                                defer content.Close()
×
182
                        }
183

184
                        // Create a new executor
185
                        // This is the piece of code which knows how to execute api.Commands
186
                        formatter := api.NewFormatter()
1✔
187
                        executor, err := api.NewDefaultExecutor(formatter)
1✔
188
                        if err != nil {
1✔
189
                                return err
×
190
                        }
×
191

192
                        // Convert the version string into an api version
193
                        apiVersion, err := shared_api.ParseVersion(version)
1✔
194
                        if err != nil {
1✔
195
                                // This should never happen, the version is already validated in the prerun function
×
196
                                return err
×
197
                        }
×
198

199
                        // Convert the api command + cobra command into a api command request
200
                        commandRequest, err := NewCommandRequestFromCobraCommand(cmd, command, content, format, apiVersion)
1✔
201
                        if err != nil {
1✔
202
                                return err
×
203
                        }
×
204

205
                        // Execute the api command request
206
                        // This function will return an error if the http request failed
207
                        // When the http request returns a non-success code error will still be nil
208
                        result, err := executor.ExecuteCommand(cmd.Context(), *commandRequest)
1✔
209
                        if err != nil {
1✔
210
                                return err
×
211
                        }
×
212

213
                        // Properly free up result output
214
                        defer result.Output.Close()
1✔
215

1✔
216
                        // Output that will be send to stdout/file
1✔
217
                        responseOutput := result.Output
1✔
218

1✔
219
                        // Response body used for the watcher
1✔
220
                        var watchResponseBody []byte
1✔
221

1✔
222
                        // If the response was successful, handle --format
1✔
223
                        if result.IsSuccess {
2✔
224
                                // If we're watching, we need to cache the original output before formatting so we don't read twice from the same reader
1✔
225
                                // In case we're not watching the http output will be piped straight into the formatter, which should be a little more memory efficient
1✔
226
                                if watch {
1✔
227
                                        responseBytes, err := io.ReadAll(result.Output)
×
228
                                        if err != nil {
×
229
                                                return errors.Join(errors.New("failed to read output"), err)
×
230
                                        }
×
231
                                        watchResponseBody = responseBytes
×
232

×
233
                                        // Create a new reader for the formatter
×
234
                                        responseOutput = io.NopCloser(bytes.NewReader(responseBytes))
×
235
                                }
236

237
                                formattedOutput, err := formatter.Format(format, responseOutput)
1✔
238
                                if err != nil {
1✔
239
                                        return errors.Join(ErrFormattingOutput, err)
×
240
                                }
×
241

242
                                responseOutput = formattedOutput
1✔
243
                        }
244

245
                        // Determine where to write the
246
                        output, err := getOutputWriteCloser(outputFile)
1✔
247
                        if err != nil {
1✔
248
                                return err
×
249
                        }
×
250
                        // Properly free up output
251
                        defer output.Close()
1✔
252

1✔
253
                        // Write the output
1✔
254
                        _, err = io.Copy(output, responseOutput)
1✔
255
                        if err != nil {
1✔
256
                                return err
×
257
                        }
×
258

259
                        // In case the http status code was non-success
260
                        // Return an error, this causes the CLI to exit with a non-zero exit code while still running all telemetry code
261
                        if !result.IsSuccess {
1✔
262
                                return ErrServerReturnedAnErrorResponseCode
×
263
                        }
×
264

265
                        // In case watcher is set we wait for the watcher to succeed before we exit the program
266
                        if watch {
1✔
267
                                // Create a new watcher
×
268
                                watcher, err := NewWatcher(executor, commandRequest.Parameters, watchResponseBody, *command.Watcher)
×
269
                                if err != nil {
×
270
                                        return err
×
271
                                }
×
272

273
                                // Wait until we're in the desired state or until an error occures when watching
274
                                if err := watcher.Wait(cmd.Context(), time.Duration(watchTimeout)); err != nil {
×
275
                                        return errors.Join(ErrRunningWatcher, err)
×
276
                                }
×
277
                        }
278

279
                        return nil
1✔
280
                },
281
        }
282

283
        // Mark command as preview so the breaking change validator skips it
284
        if isPreviewOnly(command) {
2✔
285
                cmd.Annotations["preview"] = "true"
1✔
286
        }
1✔
287

288
        // Add deprecation message if all versions are sunset
289
        addDeprecationMessageIfNeeded(cmd, command)
1✔
290

1✔
291
        // Common flags
1✔
292
        addWatchFlagIfNeeded(cmd, command, &watch, &watchTimeout)
1✔
293
        addVersionFlag(cmd, command, &version)
1✔
294

1✔
295
        if needsFileFlag(command) {
2✔
296
                cmd.Flags().StringVar(&file, flag.File, "", "path to your API request file. Leave empty to use standard input instead - you must provide one or the other, but not both.")
1✔
297
        }
1✔
298

299
        // Add output flags:
300
        // - `--output`: desired output format, translates to ContentType. Can also be a go template
301
        // - `--output-file`: file where we want to write the output to
302
        if err := addOutputFlags(cmd, command, &format, &outputFile); err != nil {
1✔
303
                return nil, err
×
304
        }
×
305

306
        // Add URL parameters as flags
307
        if err := addParameters(cmd, command.RequestParameters.URLParameters); err != nil {
1✔
308
                return nil, err
×
309
        }
×
310

311
        // Add query parameters as flags
312
        if err := addParameters(cmd, command.RequestParameters.QueryParameters); err != nil {
1✔
313
                return nil, err
×
314
        }
×
315

316
        // Handle parameter aliases
317
        cmd.Flags().SetNormalizeFunc(normalizeFlagFunc(command))
1✔
318

1✔
319
        return cmd, nil
1✔
320
}
321

322
func normalizeFlagFunc(command shared_api.Command) func(f *pflag.FlagSet, name string) pflag.NormalizedName {
1✔
323
        return func(_ *pflag.FlagSet, name string) pflag.NormalizedName {
2✔
324
                name = normalizeFlagName(command.RequestParameters.QueryParameters, name)
1✔
325
                name = normalizeFlagName(command.RequestParameters.URLParameters, name)
1✔
326

1✔
327
                return pflag.NormalizedName(name)
1✔
328
        }
1✔
329
}
330

331
func normalizeFlagName(parameters []shared_api.Parameter, name string) string {
1✔
332
        for _, parameter := range parameters {
2✔
333
                if slices.Contains(parameter.Aliases, name) {
2✔
334
                        return parameter.Name
1✔
335
                }
1✔
336
        }
337

338
        return name
1✔
339
}
340

341
func addParameters(cmd *cobra.Command, parameters []shared_api.Parameter) error {
1✔
342
        for _, parameter := range parameters {
2✔
343
                if err := addFlag(cmd, parameter); err != nil {
1✔
344
                        return err
×
345
                }
×
346
        }
347

348
        return nil
1✔
349
}
350

351
func setUnTouchedFlags(flagValueProvider FlagValueProvider, cmd *cobra.Command) error {
1✔
352
        var visitErr error
1✔
353
        cmd.NonInheritedFlags().VisitAll(func(f *pflag.Flag) {
2✔
354
                // There is no VisitAll which accepts an error
1✔
355
                // because of this we set visitErr when an error occures
1✔
356
                // if visitErr != nil we stop processing other flags
1✔
357
                if visitErr != nil {
1✔
358
                        return
×
359
                }
×
360

361
                // Only update flags thave have been un-touched by the user
362
                if !f.Changed {
2✔
363
                        value, err := flagValueProvider.ValueForFlag(f.Name)
1✔
364
                        if err != nil {
1✔
365
                                visitErr = err
×
366
                        }
×
367

368
                        // If we get a value back from the FlagValueProvider:
369
                        // - Set the value
370
                        // - Mark the flag as changed, this is needed to mark required flags as set
371
                        if value != nil {
2✔
372
                                if err := f.Value.Set(*value); err != nil {
1✔
373
                                        visitErr = err
×
374
                                        return
×
375
                                }
×
376

377
                                f.Changed = true
1✔
378
                        }
379
                }
380
        })
381

382
        return visitErr
1✔
383
}
384

385
func handleInput(cmd *cobra.Command) (io.ReadCloser, error) {
×
386
        isPiped, err := IsStdInPiped()
×
387
        if err != nil {
×
388
                return nil, err
×
389
        }
×
390

391
        // If not piped, get the file flag
392
        filePath, err := cmd.Flags().GetString(flag.File)
×
393
        if err != nil {
×
394
                return nil, fmt.Errorf("error getting file flag: %w", err)
×
395
        }
×
396

397
        if isPiped {
×
398
                if filePath != "" {
×
399
                        return nil, errors.New("cannot use --file flag and also input from standard input")
×
400
                }
×
401
                // Use stdin as the input
402
                return os.Stdin, nil
×
403
        }
404

405
        // Require file flag if not piped
406
        if filePath == "" {
×
407
                return nil, errors.New("--file flag is required when not using piped input")
×
408
        }
×
409

410
        // Open the file
411
        file, err := os.Open(filePath)
×
412
        if err != nil {
×
413
                return nil, fmt.Errorf("error opening file %s: %w", filePath, err)
×
414
        }
×
415

416
        return file, nil
×
417
}
418

419
func IsStdInPiped() (bool, error) {
×
420
        return isPiped(os.Stdin)
×
421
}
×
422

423
func IsStdOutPiped() (bool, error) {
1✔
424
        return isPiped(os.Stdout)
1✔
425
}
1✔
426

427
func isPiped(file *os.File) (bool, error) {
1✔
428
        // Check if data is being piped to stdin
1✔
429
        info, err := file.Stat()
1✔
430
        if err != nil {
1✔
431
                return false, fmt.Errorf("isPiped, error checking: %w", err)
×
432
        }
×
433

434
        // Check if there's data in stdin (piped input)
435
        isPiped := (info.Mode() & os.ModeCharDevice) == 0
1✔
436

1✔
437
        return isPiped, nil
1✔
438
}
439

440
func isPreviewOnly(command shared_api.Command) bool {
1✔
441
        for _, v := range command.Versions {
2✔
442
                if v.Version.StabilityLevel() != shared_api.StabilityLevelPreview {
2✔
443
                        return false
1✔
444
                }
1✔
445
        }
446
        return len(command.Versions) > 0
1✔
447
}
448

449
func defaultAPIVersion(command shared_api.Command) (string, error) {
1✔
450
        // Command versions are sorted by the generation tool
1✔
451
        nVersions := len(command.Versions)
1✔
452
        if nVersions == 0 {
1✔
453
                return "", ErrAPICommandsHasNoVersions
×
454
        }
×
455

456
        lastVersion := command.Versions[nVersions-1]
1✔
457
        return lastVersion.Version.String(), nil
1✔
458
}
459

460
func remindUserToPinVersion(cmd *cobra.Command) {
1✔
461
        versionFlag := cmd.Flag(flag.Version)
1✔
462
        // if we fail to get the version flag (which should never happen), then quit
1✔
463
        if versionFlag == nil {
1✔
464
                return
×
465
        }
×
466

467
        // check if the version flag is still in it's default state:
468
        // - not set by the user
469
        // - not set using api_version on the users profile
470
        // in that case, print a warning
471
        if !versionFlag.Changed {
2✔
472
                fmt.Fprintf(os.Stderr, "warning: using default API version '%s'; consider pinning a version to ensure consisentcy when updating the CLI\n", versionFlag.Value.String())
1✔
473
        }
1✔
474
}
475

476
func ensureVersionIsSupported(apiCommand shared_api.Command, versionString *string) {
1✔
477
        version, err := shared_api.ParseVersion(*versionString)
1✔
478

1✔
479
        // If the version is valid, check if it's supported
1✔
480
        if err == nil {
2✔
481
                for _, commandVersion := range apiCommand.Versions {
2✔
482
                        if commandVersion.Version.Equal(version) {
2✔
483
                                return
1✔
484
                        }
1✔
485
                }
486
        }
487

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

496
        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)
1✔
497
        *versionString = defaultVersion
1✔
498
}
499

500
func printPreviewWarning(apiCommand shared_api.Command, versionString *string) {
1✔
501
        version, err := shared_api.ParseVersion(*versionString)
1✔
502

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

509
        // If the version is not a preview version, return
510
        if version.StabilityLevel() != shared_api.StabilityLevelPreview {
2✔
511
                return
1✔
512
        }
1✔
513

514
        // Find the version in the command versions
515
        var commandVersion *shared_api.CommandVersion
×
516
        for _, cv := range apiCommand.Versions {
×
517
                if cv.Version.Equal(version) {
×
518
                        commandVersion = &cv
×
519
                        break
×
520
                }
521
        }
522

523
        // If the version is not found, return (should also never happen)
524
        if commandVersion == nil {
×
525
                return
×
526
        }
×
527

528
        if commandVersion.PublicPreview {
×
529
                fmt.Fprintf(os.Stderr, "warning: you've selected a public preview version of the endpoint, this version is subject to breaking changes.\n")
×
530
        } else {
×
531
                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")
×
532
        }
×
533
}
534

535
// printDeprecatedVersionWarning is a convenience wrapper that calls printDeprecatedVersionWarningWithTime with the current time.
536
func printDeprecatedVersionWarning(apiCommand shared_api.Command, versionString *string) {
1✔
537
        printDeprecatedVersionWarningWithTime(apiCommand, versionString, time.Now())
1✔
538
}
1✔
539

540
// printDeprecatedVersionWarningWithTime prints a warning if the version is deprecated or has a sunset date.
541
// only warn if the command is not fully deprecated, assume that if all versions are deprecated,
542
// then the command will be marked as deprecated in Cobra.
543
// The time parameter `now` allows injection of static date for deterministic testing.
544
func printDeprecatedVersionWarningWithTime(apiCommand shared_api.Command, versionString *string, now time.Time) {
1✔
545
        if allVersionsDeprecated(apiCommand) {
1✔
546
                return
×
547
        }
×
548

549
        version, err := shared_api.ParseVersion(*versionString)
1✔
550
        if err != nil {
1✔
551
                return
×
552
        }
×
553

554
        // Find the version in the command versions
555
        var commandVersion *shared_api.CommandVersion
1✔
556
        for i := range apiCommand.Versions {
2✔
557
                if apiCommand.Versions[i].Version.Equal(version) {
2✔
558
                        commandVersion = &apiCommand.Versions[i]
1✔
559
                        break
1✔
560
                }
561
        }
562

563
        // If the version is not found or is not deprecated and has no sunset date, return
564
        if commandVersion == nil {
1✔
565
                return
×
566
        }
×
567

568
        // Print a warning or error if the version has a sunset date
569
        if commandVersion.Sunset != nil {
1✔
570
                sunsetDate := commandVersion.Sunset.Format("2006-01-02")
×
571
                // if date is in the past, warn the user that it will not work
×
572
                if commandVersion.Sunset.Before(now) {
×
573
                        fmt.Fprintf(os.Stderr, "error: version '%s' is deprecated for this command and has already been sunset since %s. Consider upgrading to a newer version if available.\n", *versionString, sunsetDate)
×
574
                        return
×
575
                }
×
576
                fmt.Fprintf(os.Stderr, "warning: version '%s' is deprecated for this command and will be sunset on %s. Consider upgrading to a newer version if available.\n", *versionString, sunsetDate)
×
577
                return
×
578
        }
579

580
        // Find the latest command version
581
        latestCommandVersion, err := defaultAPIVersion(apiCommand)
1✔
582
        if err != nil || latestCommandVersion == commandVersion.Version.String() {
2✔
583
                latestCommandVersion = ""
1✔
584
        }
1✔
585

586
        // Print a warning if the version is deprecated but is not sunset
587
        if commandVersion.Deprecated {
1✔
588
                fmt.Fprintf(os.Stderr, "warning: version '%s' is deprecated. ", *versionString)
×
589
                if latestCommandVersion != "" {
×
590
                        fmt.Fprintf(os.Stderr, "Consider upgrading to a newer version: %s.", latestCommandVersion)
×
591
                }
×
592
                fmt.Fprintf(os.Stderr, "\n")
×
593
                return
×
594
        }
595
}
596

597
func needsFileFlag(apiCommand shared_api.Command) bool {
1✔
598
        for _, version := range apiCommand.Versions {
2✔
599
                if version.RequestContentType != "" {
2✔
600
                        return true
1✔
601
                }
1✔
602
        }
603

604
        return false
1✔
605
}
606

607
func addWatchFlagIfNeeded(cmd *cobra.Command, apiCommand shared_api.Command, watch *bool, watchTimeout *int64) {
1✔
608
        if apiCommand.Watcher == nil || apiCommand.Watcher.Get.OperationID == "" {
2✔
609
                return
1✔
610
        }
1✔
611

612
        cmd.Flags().BoolVarP(watch, flag.EnableWatch, flag.EnableWatchShort, false, usage.EnableWatchDefault)
1✔
613
        cmd.Flags().Int64Var(watchTimeout, flag.WatchTimeout, 0, usage.WatchTimeout)
1✔
614
}
615

616
func addVersionFlag(cmd *cobra.Command, apiCommand shared_api.Command, version *string) {
1✔
617
        // Create a unique list of all supported versions
1✔
618
        versions := make(map[string]struct{}, 0)
1✔
619
        for _, version := range apiCommand.Versions {
2✔
620
                versions[version.Version.String()] = struct{}{}
1✔
621
        }
1✔
622

623
        // Convert the keys of the map into a list
624
        supportedVersionsVersions := make([]string, 0, len(versions))
1✔
625
        for version := range versions {
2✔
626
                supportedVersionsVersions = append(supportedVersionsVersions, `"`+version+`"`)
1✔
627
        }
1✔
628

629
        // Sort the list
630
        slices.Sort(supportedVersionsVersions)
1✔
631

1✔
632
        // Convert the list to a string
1✔
633
        supportedVersionsVersionsString := strings.Join(supportedVersionsVersions, ", ")
1✔
634

1✔
635
        cmd.Flags().StringVar(version, flag.Version, *version, fmt.Sprintf("API version to use when calling the Atlas API endpoints [options: %s]. If not set by the user, defaults to the latest version or the profile's api_version config value if set.", supportedVersionsVersionsString))
1✔
636
}
637

638
func addOutputFlags(cmd *cobra.Command, apiCommand shared_api.Command, format *string, outputFile *string) error {
1✔
639
        // Get the list of supported content types for the apiCommand
1✔
640
        supportedContentTypesList := getContentTypes(&apiCommand)
1✔
641

1✔
642
        // If there's only one content type, set the format to that
1✔
643
        numSupportedContentTypes := len(supportedContentTypesList)
1✔
644
        if numSupportedContentTypes == 1 {
2✔
645
                *format = supportedContentTypesList[0]
1✔
646
        }
1✔
647

648
        // If the content type has json, also add go-template as an option to the --format help
649
        containsJSON := slices.Contains(supportedContentTypesList, "json")
1✔
650

1✔
651
        // Place quotes around every supported content type
1✔
652
        for i, value := range supportedContentTypesList {
2✔
653
                supportedContentTypesList[i] = `"` + value + `"`
1✔
654
        }
1✔
655

656
        // Add go-template, we add it here because we don't want go-template to be between quotes
657
        if containsJSON {
2✔
658
                supportedContentTypesList = append(supportedContentTypesList, "go-template")
1✔
659
        }
1✔
660

661
        // Generate a list of supported content types and add it to --help for --format
662
        // Example ['csv', 'json', go-template]
663
        supportedContentTypesString := strings.Join(supportedContentTypesList, ", ")
1✔
664

1✔
665
        // Set the flags
1✔
666
        cmd.Flags().StringVarP(format, flag.Output, flag.OutputShort, *format, fmt.Sprintf("preferred api format, can be [%s]", supportedContentTypesString))
1✔
667
        cmd.Flags().StringVar(outputFile, flag.OutputFile, "", "file to write the api output to. This flag is required when the output of an endpoint is binary (ex: gzip) and the command is not piped (ex: atlas command > out.zip)")
1✔
668

1✔
669
        // If there's multiple content types, mark --format as required
1✔
670
        if numSupportedContentTypes > 1 {
2✔
671
                if err := cmd.MarkFlagRequired(flag.Output); err != nil {
1✔
672
                        return err
×
673
                }
×
674
        }
675

676
        return nil
1✔
677
}
678

679
func getContentTypes(apiCommand *shared_api.Command) []string {
1✔
680
        // Create a unique list of all supported content types
1✔
681
        // First create a map to convert 2 nested lists into a map
1✔
682
        supportedContentTypes := make(map[string]struct{}, 0)
1✔
683
        for _, version := range apiCommand.Versions {
2✔
684
                for _, contentType := range version.ResponseContentTypes {
2✔
685
                        supportedContentTypes[contentType] = struct{}{}
1✔
686
                }
1✔
687
        }
688

689
        // Convert the keys of the map into a list
690
        supportedContentTypesList := make([]string, 0, len(supportedContentTypes))
1✔
691
        for contentType := range supportedContentTypes {
2✔
692
                supportedContentTypesList = append(supportedContentTypesList, contentType)
1✔
693
        }
1✔
694

695
        // Sort the list
696
        slices.Sort(supportedContentTypesList)
1✔
697

1✔
698
        return supportedContentTypesList
1✔
699
}
700

701
func getOutputWriteCloser(outputFile string) (io.WriteCloser, error) {
1✔
702
        // If an output file is specified, create/open the file and return the writer
1✔
703
        if outputFile != "" {
1✔
704
                //nolint: mnd
×
705
                file, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
×
706
                if err != nil {
×
707
                        return nil, err
×
708
                }
×
709
                return file, nil
×
710
        }
711

712
        // Return stdout by default
713
        return os.Stdout, nil
1✔
714
}
715

716
// addDeprecationMessageIfNeeded adds a deprecation message to the command if the command is deprecated.
717
func addDeprecationMessageIfNeeded(cmd *cobra.Command, apiCommand shared_api.Command) {
1✔
718
        allVersionsAreDeprecated := allVersionsDeprecated(apiCommand)
1✔
719
        // if the command is not fully deprecated, then we don't need to add a deprecation message, there will be a warning for each version.
1✔
720
        if !allVersionsAreDeprecated {
2✔
721
                return
1✔
722
        }
1✔
723

724
        cmd.Deprecated = "all of the available endpoint versions have been deprecated."
1✔
725

1✔
726
        if len(apiCommand.Versions) == 1 && apiCommand.Versions[0].Sunset != nil {
2✔
727
                version := apiCommand.Versions[0]
1✔
728
                sunsetDate := version.Sunset.Format("2006-01-02")
1✔
729
                cmd.Deprecated += fmt.Sprintf(" The API endpoint version %s will no longer be available after the sunset date of %s.\n", version.Version.String(), sunsetDate)
1✔
730
                return
1✔
731
        }
1✔
732

733
        for _, version := range apiCommand.Versions {
2✔
734
                if version.Sunset != nil {
1✔
735
                        sunsetDate := version.Sunset.Format("2006-01-02")
×
736
                        cmd.Deprecated += fmt.Sprintf(" The API endpoint version %s will no longer be available after the sunset date of %s.", version.Version.String(), sunsetDate)
×
737
                        break
×
738
                }
739
        }
740

741
        cmd.Deprecated += "\n"
1✔
742
}
743

744
// allVersionsDeprecated checks if all the versions are deprecated.
745
// we classify the command as deprecated if all the versions have a sunset date or all the versions are deprecated.
746
func allVersionsDeprecated(apiCommand shared_api.Command) bool {
1✔
747
        for _, version := range apiCommand.Versions {
2✔
748
                if version.Sunset == nil && !version.Deprecated {
2✔
749
                        return false
1✔
750
                }
1✔
751
        }
752

753
        return true
1✔
754
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc