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

mongodb / mongodb-atlas-cli / 24446377957

15 Apr 2026 09:19AM UTC coverage: 22.449% (-41.3%) from 63.758%
24446377957

push

github

GitHub
build(deps): bump the cloud-providers group with 3 updates (#4533)

8972 of 39967 relevant lines covered (22.45%)

0.25 hits per line

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

11.48
/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 {
2✔
87
                        return "", nil, fmt.Errorf(`the specified version "%s" is invalid, it needs to follow the rules of Semantic Versioning`, versionValue)
1✔
88
                }
1✔
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 {
×
98
        // Get the manifest from the plugin directory
×
99
        manifest, err := plugin.GetManifestFromPluginDirectory(pluginDirectoryPath)
×
100
        if err != nil {
×
101
                return err
×
102
        }
×
103

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

109
        // make sure that there is exactly one plugin with the same name
110
        pluginCount := 0
×
111
        for _, p := range opts.getValidPlugins() {
×
112
                if manifest.Name == p.Name {
×
113
                        pluginCount++
×
114
                }
×
115
        }
116
        if pluginCount != 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]()
×
122
        for _, cmd := range opts.existingCommands {
×
123
                // only add command to existing commands map if it is not part of the plugin we want to update
×
124
                if sourcePluginName, ok := cmd.Annotations[sourcePluginName]; !ok || sourcePluginName != manifest.Name {
×
125
                        existingCommandsSet.Add(cmd.Name())
×
126
                }
×
127
        }
128
        if manifest.HasDuplicateCommand(existingCommandsSet) {
×
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
×
133
}
134

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

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

148
        // When signatureID and pubKeyID are 0, the signature check is skipped.
149
        if opts.skipSignatureVerification {
×
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)
×
156
        if err != nil {
×
157
                return err
×
158
        }
×
159
        defer rc.Close()
×
160

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

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

×
176
        // validate the extracted plugin files
×
177
        // if plugin is invalid, delete all of its files
×
178
        err = opts.validatePlugin(tempPluginDirectoryPath)
×
179
        if err != nil {
×
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"
×
185
        err = os.Rename(existingPlugin.PluginDirectoryPath, oldPluginDirectoryPath)
×
186
        if err != nil {
×
187
                return err
×
188
        }
×
189
        defer os.RemoveAll(oldPluginDirectoryPath)
×
190

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

205
        return nil
×
206
}
207

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

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

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

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

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

244
                // create GithubAsset and use it to update to update plugin
245
                githubAsset, err := createGithubAssetFromPlugin(existingPlugin, opts.pluginUpdateVersion)
×
246
                if err != nil {
×
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) {
×
252
                        return fmt.Errorf("the specified version %s is not greater than the currently installed version %s", githubAsset.version.String(), existingPlugin.Version.String())
×
253
                }
×
254

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

263
        return nil
×
264
}
265

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

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

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

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

×
320
        return cmd
×
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