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

mongodb / mongodb-atlas-cli / 25848964073

14 May 2026 07:59AM UTC coverage: 22.479% (-41.3%) from 63.771%
25848964073

push

github

web-flow
build(deps): bump test-summary/action from 2.4 to 2.6 (#4576)

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

8987 of 39979 relevant lines covered (22.48%)

0.25 hits per line

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

13.04
/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/filepath"
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 pluginTargetDirectory(existingPluginPath, newDirectoryName string) string {
1✔
56
        return filepath.Join(filepath.Dir(existingPluginPath), newDirectoryName)
1✔
57
}
1✔
58

59
func printPluginUpdateWarning(p *plugin.Plugin, err error) {
×
60
        _, _ = log.Warningf("could not update plugin %s because: %v\n", p.Name, err)
×
61
}
×
62

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

71
        matches := regex.FindStringSubmatch(arg)
1✔
72
        if matches == nil {
2✔
73
                return "", nil, errUpdateArgInvalid
1✔
74
        }
1✔
75

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

85
        var version *semver.Version
1✔
86

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

96
        return groupMap["pluginValue"], version, nil
1✔
97
}
98

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

108
        err = validateManifest(manifest)
×
109
        if err != nil {
×
110
                return err
×
111
        }
×
112

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

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

136
        return nil
×
137
}
138

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

146
        // find correct assetID, signatureID and pubKeyID using system requirements
147
        assetID, signatureID, pubKeyID, err := githubAssetRelease.getIDs(assets)
×
148
        if err != nil {
×
149
                return err
×
150
        }
×
151

152
        // When signatureID and pubKeyID are 0, the signature check is skipped.
153
        if opts.skipSignatureVerification {
×
154
                signatureID = 0
×
155
                pubKeyID = 0
×
156
        }
×
157

158
        // download plugin asset archive file and save it as ReadCloser
159
        rc, err := githubAssetRelease.getPluginAssetsAsReadCloser(opts.ghClient, assetID, signatureID, pubKeyID)
×
160
        if err != nil {
×
161
                return err
×
162
        }
×
163
        defer rc.Close()
×
164

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

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

×
180
        // validate the extracted plugin files
×
181
        // if plugin is invalid, delete all of its files
×
182
        err = opts.validatePlugin(tempPluginDirectoryPath)
×
183
        if err != nil {
×
184
                return err
×
185
        }
×
186

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

×
195
        // rename temp plugin directory to actual name in the same parent directory as the existing plugin,
×
196
        // so plugins installed in ATLAS_CLI_EXTRA_PLUGIN_DIRECTORY are updated in place.
×
197
        pluginDirectoryPath := pluginTargetDirectory(existingPlugin.PluginDirectoryPath, githubAssetRelease.getPluginDirectoryName())
×
198
        err = os.Rename(tempPluginDirectoryPath, pluginDirectoryPath)
×
199
        if err != nil {
×
200
                err = os.Rename(oldPluginDirectoryPath, existingPlugin.PluginDirectoryPath)
×
201
                return err
×
202
        }
×
203

204
        return nil
×
205
}
206

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

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

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

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

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

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

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

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

262
        return nil
×
263
}
264

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

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

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

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

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