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

mongodb / mongodb-atlas-cli / 16670272475

01 Aug 2025 08:27AM UTC coverage: 57.953% (-7.1%) from 65.017%
16670272475

Pull #4071

github

fmenezes
lint
Pull Request #4071: chore: remove unit tag from tests

23613 of 40745 relevant lines covered (57.95%)

2.74 hits per line

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

73.68
/internal/cli/plugin/update.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 plugin
16

17
import (
18
        "context"
19
        "errors"
20
        "fmt"
21
        "os"
22
        "path"
23
        "regexp"
24
        "strings"
25

26
        "github.com/Masterminds/semver/v3"
27
        "github.com/google/go-github/v61/github"
28
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli"
29
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli/require"
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/plugin"
33
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/set"
34
        "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/usage"
35
        "github.com/spf13/cobra"
36
)
37

38
var (
39
        errTooManyArguments        = errors.New(`either the "--all" flag or the plugin identifier can be provided, but not both`)
40
        errNotEnoughArguments      = errors.New(`either the "--all" flag or the plugin identifier needs to be provided`)
41
        errPluginHasNoGithubValues = errors.New(`specified plugin does not contain any Github values in its manifest.yaml file. This issue may have occurred because the plugin was added manually instead of using the "plugin install" command`)
42
        errUpdateArgInvalid        = errors.New(`the format of the plugin indentifier is invalid. You can specify a plugin to update using either the "<github-owner>/<github-repository-name>" format or the plugin name`)
43
)
44

45
type UpdateOpts struct {
46
        cli.OutputOpts
47
        Opts
48
        UpdateAll                 bool
49
        pluginSpecifier           string
50
        pluginUpdateVersion       *semver.Version
51
        ghClient                  *github.Client
52
        skipSignatureVerification bool
53
}
54

55
func printPluginUpdateWarning(p *plugin.Plugin, err error) {
×
56
        _, _ = log.Warningf("could not update plugin %s because: %v\n", p.Name, err)
×
57
}
×
58

59
// extract plugin specifier and version given the input argument of the update command.
60
func extractPluginSpecifierAndVersionFromArg(arg string) (string, *semver.Version, error) {
1✔
61
        regexPattern := `^(?P<pluginValue>[^\s@]+)(@(?P<version>.+))?$`
1✔
62
        regex, err := regexp.Compile(regexPattern)
1✔
63
        if err != nil {
1✔
64
                return "", nil, fmt.Errorf("error compiling regex: %w", err)
×
65
        }
×
66

67
        matches := regex.FindStringSubmatch(arg)
1✔
68
        if matches == nil {
2✔
69
                return "", nil, errUpdateArgInvalid
1✔
70
        }
1✔
71

72
        names := regex.SubexpNames()
1✔
73
        groupMap := make(map[string]string)
1✔
74
        for i, match := range matches {
2✔
75
                if i == 0 {
2✔
76
                        continue
1✔
77
                }
78
                groupMap[names[i]] = match
1✔
79
        }
80

81
        var version *semver.Version
1✔
82

1✔
83
        if versionValue, ok := groupMap["version"]; ok && versionValue != latest && versionValue != "" {
2✔
84
                versionValue := strings.TrimPrefix(versionValue, "v")
1✔
85
                semverVersion, err := semver.NewVersion(versionValue)
1✔
86
                if err != nil {
1✔
87
                        return "", nil, fmt.Errorf(`the specified version "%s" is invalid, it needs to follow the rules of Semantic Versioning`, versionValue)
×
88
                }
×
89
                version = semverVersion
1✔
90
        }
91

92
        return groupMap["pluginValue"], version, nil
1✔
93
}
94

95
// checks if the plugin manifest is valid and that the plugin
96
// doesn't contain any commands that conflict with existing CLI commands.
97
func (opts *UpdateOpts) validatePlugin(pluginDirectoryPath string) error {
1✔
98
        // Get the manifest from the plugin directory
1✔
99
        manifest, err := plugin.GetManifestFromPluginDirectory(pluginDirectoryPath)
1✔
100
        if err != nil {
1✔
101
                return err
×
102
        }
×
103

104
        err = validateManifest(manifest)
1✔
105
        if err != nil {
1✔
106
                return err
×
107
        }
×
108

109
        // make sure that there is exactly one plugin with the same name
110
        pluginCount := 0
1✔
111
        for _, p := range opts.getValidPlugins() {
2✔
112
                if manifest.Name == p.Name {
2✔
113
                        pluginCount++
1✔
114
                }
1✔
115
        }
116
        if pluginCount != 1 {
1✔
117
                return fmt.Errorf(`there needs to be exactly 1 plugin with the name "%s", but there are %d`, manifest.Name, pluginCount)
×
118
        }
×
119

120
        // Check for duplicate commands
121
        existingCommandsSet := set.NewSet[string]()
1✔
122
        for _, cmd := range opts.existingCommands {
2✔
123
                // only add command to existing commands map if it is not part of the plugin we want to update
1✔
124
                if sourcePluginName, ok := cmd.Annotations[sourcePluginName]; !ok || sourcePluginName != manifest.Name {
2✔
125
                        existingCommandsSet.Add(cmd.Name())
1✔
126
                }
1✔
127
        }
128
        if manifest.HasDuplicateCommand(existingCommandsSet) {
1✔
129
                return fmt.Errorf(`could not load plugin "%s" because it contains a command that already exists in the AtlasCLI or another plugin`, manifest.Name)
×
130
        }
×
131

132
        return nil
1✔
133
}
134

135
func (opts *UpdateOpts) updatePlugin(ctx context.Context, githubAssetRelease *GithubAsset, existingPlugin *plugin.Plugin) error {
1✔
136
        // get all plugin assets info from github repository
1✔
137
        assets, err := githubAssetRelease.getReleaseAssets(opts.ghClient)
1✔
138
        if err != nil {
1✔
139
                return err
×
140
        }
×
141

142
        // find correct assetID, signatureID and pubKeyID using system requirements
143
        assetID, signatureID, pubKeyID, err := githubAssetRelease.getIDs(assets)
1✔
144
        if err != nil {
1✔
145
                return err
×
146
        }
×
147

148
        // When signatureID and pubKeyID are 0, the signature check is skipped.
149
        if opts.skipSignatureVerification {
1✔
150
                signatureID = 0
×
151
                pubKeyID = 0
×
152
        }
×
153

154
        // download plugin asset archive file and save it as ReadCloser
155
        rc, err := githubAssetRelease.getPluginAssetsAsReadCloser(opts.ghClient, assetID, signatureID, pubKeyID)
1✔
156
        if err != nil {
1✔
157
                return err
×
158
        }
×
159
        defer rc.Close()
1✔
160

1✔
161
        // use the ReadCloser to save the asset archive file in the default plugin directory
1✔
162
        pluginArchiveFilePath, err := saveReadCloserToPluginAssetArchiveFile(rc)
1✔
163
        if err != nil {
1✔
164
                return err
×
165
        }
×
166
        defer os.Remove(pluginArchiveFilePath) // delete archive file after update command finishes
1✔
167

1✔
168
        // try to extract content of plugin archive file and save it in default plugin directory
1✔
169
        tempPluginDirectoryName := githubAssetRelease.getPluginDirectoryName() + "_temp"
1✔
170
        tempPluginDirectoryPath, err := extractPluginAssetArchiveFile(ctx, pluginArchiveFilePath, tempPluginDirectoryName)
1✔
171
        if err != nil {
1✔
172
                return err
×
173
        }
×
174
        defer os.RemoveAll(tempPluginDirectoryPath)
1✔
175

1✔
176
        // validate the extracted plugin files
1✔
177
        // if plugin is invalid, delete all of its files
1✔
178
        err = opts.validatePlugin(tempPluginDirectoryPath)
1✔
179
        if err != nil {
1✔
180
                return err
×
181
        }
×
182

183
        // rename old plugin directory to <plugin-directory>_old so we can rollback in case something goes wrong
184
        oldPluginDirectoryPath := existingPlugin.PluginDirectoryPath + "_old"
1✔
185
        err = os.Rename(existingPlugin.PluginDirectoryPath, oldPluginDirectoryPath)
1✔
186
        if err != nil {
1✔
187
                return err
×
188
        }
×
189
        defer os.RemoveAll(oldPluginDirectoryPath)
1✔
190

1✔
191
        // rename temp plugin directory to actual name
1✔
192
        // if anything goes wrong, rollback the old version of the directory
1✔
193
        pluginsDefaultDirectory, err := plugin.GetDefaultPluginDirectory()
1✔
194
        if err != nil {
1✔
195
                err = os.Rename(oldPluginDirectoryPath, existingPlugin.PluginDirectoryPath)
×
196
                return err
×
197
        }
×
198
        pluginDirectoryPath := path.Join(pluginsDefaultDirectory, githubAssetRelease.getPluginDirectoryName())
1✔
199
        err = os.Rename(tempPluginDirectoryPath, pluginDirectoryPath)
1✔
200
        if err != nil {
1✔
201
                err = os.Rename(oldPluginDirectoryPath, existingPlugin.PluginDirectoryPath)
×
202
                return err
×
203
        }
×
204

205
        return nil
1✔
206
}
207

208
func (opts *UpdateOpts) Run(ctx context.Context) error {
1✔
209
        // if update flag is set, update all plugin, if not update only specified plugin
1✔
210
        if opts.UpdateAll {
2✔
211
                // try to create GithubAssetRelease from each plugin -  when create use it to update the plugin
1✔
212
                for _, p := range opts.getValidPlugins() {
2✔
213
                        if !p.HasGithub() {
1✔
214
                                continue
×
215
                        }
216

217
                        opts.Print(fmt.Sprintf(`Updating plugin "%s"`, p.Name))
1✔
218

1✔
219
                        // create GithubAsset and use it to update to update plugin
1✔
220
                        githubAsset, err := createGithubAssetFromPlugin(p, nil)
1✔
221
                        if err != nil {
1✔
222
                                printPluginUpdateWarning(p, err)
×
223
                                continue
×
224
                        }
225

226
                        // update using GithubAsset
227
                        err = opts.updatePlugin(ctx, githubAsset, p)
1✔
228
                        if err != nil {
1✔
229
                                printPluginUpdateWarning(p, err)
×
230
                        }
×
231
                }
232
        } else {
1✔
233
                // find existing plugin using plugin args
1✔
234
                existingPlugin, err := opts.findPluginWithArg(opts.pluginSpecifier)
1✔
235
                if err != nil {
1✔
236
                        return err
×
237
                }
×
238

239
                // make sure the plugin has Github values
240
                if !existingPlugin.HasGithub() {
1✔
241
                        return errPluginHasNoGithubValues
×
242
                }
×
243

244
                // create GithubAsset and use it to update to update plugin
245
                githubAsset, err := createGithubAssetFromPlugin(existingPlugin, opts.pluginUpdateVersion)
1✔
246
                if err != nil {
1✔
247
                        return err
×
248
                }
×
249

250
                // make sure the specified version is greater than currently installed version
251
                if githubAsset.version != nil && !githubAsset.version.GreaterThan(existingPlugin.Version) {
2✔
252
                        return fmt.Errorf("the specified version %s is not greater than the currently installed version %s", githubAsset.version.String(), existingPlugin.Version.String())
1✔
253
                }
1✔
254

255
                // update using GithubAsset
256
                opts.Print(fmt.Sprintf(`Updating plugin "%s"`, existingPlugin.Name))
1✔
257
                err = opts.updatePlugin(ctx, githubAsset, existingPlugin)
1✔
258
                if err != nil {
1✔
259
                        return err
×
260
                }
×
261
        }
262

263
        return nil
1✔
264
}
265

266
func UpdateBuilder(pluginOpts *Opts) *cobra.Command {
1✔
267
        opts := &UpdateOpts{
1✔
268
                UpdateAll: false,
1✔
269
                ghClient:  NewAuthenticatedGithubClient(),
1✔
270
        }
1✔
271
        opts.Opts = *pluginOpts
1✔
272

1✔
273
        const use = "update"
1✔
274
        cmd := &cobra.Command{
1✔
275
                Use:     use + " [plugin]",
1✔
276
                Aliases: cli.GenerateAliases(use),
1✔
277
                Annotations: map[string]string{
1✔
278
                        "pluginDesc": "Plugin identifier.",
1✔
279
                },
1✔
280
                Short: "Update Atlas CLI plugin.",
1✔
281
                Long: `Update an Atlas CLI plugin.
1✔
282
You can specify a plugin to update using either the "<github-owner>/<github-repository-name>" format or the plugin name.
1✔
283
Additionally, you can use the "--all" flag to update all plugins.
1✔
284
`,
1✔
285
                Args: require.MaximumNArgs(1),
1✔
286
                Example: `  # Update a plugin:
1✔
287
  atlas plugin update mongodb/atlas-cli-plugin-example
1✔
288
  atlas plugin update atlas-cli-plugin-example
1✔
289
  
1✔
290
  # Update all plugins
1✔
291
  atlas plugin update --all`,
1✔
292
                PreRunE: func(_ *cobra.Command, arg []string) error {
2✔
293
                        // make sure either the "--all" flag is set or the plugin identifier but not both
1✔
294
                        if opts.UpdateAll && len(arg) >= 1 {
1✔
295
                                return errTooManyArguments
×
296
                        }
×
297
                        if !opts.UpdateAll && len(arg) != 1 {
1✔
298
                                return errNotEnoughArguments
×
299
                        }
×
300
                        if !opts.UpdateAll {
2✔
301
                                // extract plugin value and version from arg
1✔
302
                                pluginSpecifier, version, err := extractPluginSpecifierAndVersionFromArg(arg[0])
1✔
303
                                if err != nil {
2✔
304
                                        return err
1✔
305
                                }
1✔
306
                                opts.pluginSpecifier = pluginSpecifier
1✔
307
                                opts.pluginUpdateVersion = version
1✔
308
                        }
309

310
                        return nil
1✔
311
                },
312
                RunE: func(cmd *cobra.Command, _ []string) error {
1✔
313
                        return opts.Run(cmd.Context())
1✔
314
                },
1✔
315
        }
316

317
        cmd.Flags().BoolVar(&opts.UpdateAll, flag.All, false, usage.UpdateAllPlugins)
1✔
318
        cmd.Flags().BoolVar(&opts.skipSignatureVerification, flag.SkipSignatureVerification, false, usage.SkipSignatureVerification)
1✔
319

1✔
320
        return cmd
1✔
321
}
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