Skip to content

Commit 8b0b400

Browse files
CLOUDP-296203: [AtlasCLI Plugins] Add Support for installing CLI Plugins from private repos (#4009)
1 parent 62e0926 commit 8b0b400

File tree

9 files changed

+77
-35
lines changed

9 files changed

+77
-35
lines changed

build/ci/library_owners.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys": "apix-2",
1111
"github.com/bradleyjkemp/cupaloy/v2": "apix-2",
1212
"github.com/briandowns/spinner": "apix-2",
13+
"github.com/cli/go-gh/v2": "apix-2",
1314
"github.com/creack/pty": "apix-2",
1415
"github.com/denisbrodbeck/machineid": "apix-2",
1516
"github.com/evergreen-ci/shrub": "apix-2",

build/package/purls.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ pkg:golang/github.com/bodgit/[email protected]
3535
pkg:golang/github.com/bodgit/[email protected]
3636
pkg:golang/github.com/bodgit/[email protected]
3737
pkg:golang/github.com/briandowns/[email protected]
38+
pkg:golang/github.com/cli/go-gh/[email protected]
39+
pkg:golang/github.com/cli/[email protected]
3840
pkg:golang/github.com/cloudflare/[email protected]
3941
pkg:golang/github.com/denisbrodbeck/[email protected]
4042
pkg:golang/github.com/dsnet/[email protected]
@@ -65,7 +67,7 @@ pkg:golang/github.com/klauspost/[email protected]
6567
pkg:golang/github.com/kylelemons/[email protected]
6668
pkg:golang/github.com/mattn/[email protected]
6769
pkg:golang/github.com/mattn/[email protected]
68-
pkg:golang/github.com/mgutz/[email protected]20170206155736-9520e82c474b
70+
pkg:golang/github.com/mgutz/[email protected]20200706080929-d51e80ef957d
6971
pkg:golang/github.com/mholt/[email protected]
7072
pkg:golang/github.com/minio/[email protected]
7173
pkg:golang/github.com/mongodb-forks/[email protected]

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ require (
6464
require (
6565
github.com/bmatcuk/doublestar/v4 v4.0.2 // indirect
6666
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
67+
github.com/cli/safeexec v1.0.0 // indirect
6768
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
6869
github.com/google/addlicense v1.1.1 // indirect
6970
github.com/google/go-licenses/v2 v2.0.0-alpha.1 // indirect
@@ -101,6 +102,7 @@ require (
101102
github.com/bodgit/plumbing v1.3.0 // indirect
102103
github.com/bodgit/sevenzip v1.6.0 // indirect
103104
github.com/bodgit/windows v1.0.1 // indirect
105+
github.com/cli/go-gh/v2 v2.12.1
104106
github.com/cloudflare/circl v1.6.1 // indirect
105107
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
106108
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect
@@ -135,7 +137,7 @@ require (
135137
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
136138
github.com/mailru/easyjson v0.7.7 // indirect
137139
github.com/mattn/go-colorable v0.1.13 // indirect
138-
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
140+
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
139141
github.com/minio/minlz v1.0.0 // indirect
140142
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
141143
github.com/montanaflynn/stats v0.7.1 // indirect

go.sum

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
112112
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
113113
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
114114
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
115+
github.com/cli/go-gh/v2 v2.12.1 h1:SVt1/afj5FRAythyMV3WJKaUfDNsxXTIe7arZbwTWKA=
116+
github.com/cli/go-gh/v2 v2.12.1/go.mod h1:+5aXmEOJsH9fc9mBHfincDwnS02j2AIA/DsTH0Bk5uw=
117+
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
118+
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
115119
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
116120
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
117121
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
@@ -301,8 +305,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
301305
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
302306
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
303307
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
304-
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
305308
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
309+
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
310+
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
306311
github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A=
307312
github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs=
308313
github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=

internal/cli/plugin/first_class.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package plugin
1717
import (
1818
"fmt"
1919

20-
"github.com/google/go-github/v61/github"
2120
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/plugin"
2221
"github.com/spf13/cobra"
2322
)
@@ -90,16 +89,16 @@ func (fcp *FirstClassPlugin) isAlreadyInstalled(plugins *plugin.ValidatedPlugins
9089
return false
9190
}
9291

93-
func (fcp *FirstClassPlugin) runFirstClassPluginCommand(cmd *cobra.Command, args []string, ghClient *github.Client, plugins *plugin.ValidatedPlugins) error {
92+
func (fcp *FirstClassPlugin) runFirstClassPluginCommand(cmd *cobra.Command, args []string, plugins *plugin.ValidatedPlugins) error {
9493
installOpts := &InstallOpts{
9594
Opts: Opts{
9695
plugins: plugins,
9796
},
97+
ghClient: NewAuthenticatedGithubClient(),
9898
}
9999
installOpts.githubAsset = &GithubAsset{
100-
ghClient: ghClient,
101-
owner: fcp.Github.Owner,
102-
name: fcp.Github.Name,
100+
owner: fcp.Github.Owner,
101+
name: fcp.Github.Name,
103102
}
104103
installOpts.Print("Installing first class plugin " + fcp.Name)
105104

@@ -122,7 +121,6 @@ func (fcp *FirstClassPlugin) runFirstClassPluginCommand(cmd *cobra.Command, args
122121

123122
func (fcp *FirstClassPlugin) getCommands(plugins *plugin.ValidatedPlugins) []*cobra.Command {
124123
commands := make([]*cobra.Command, 0, len(fcp.Commands))
125-
ghClient := github.NewClient(nil)
126124

127125
// for every command listed in the first class plugin, create a cobra command that installs the plugin
128126
for _, firstClassPluginCommand := range fcp.Commands {
@@ -133,7 +131,7 @@ func (fcp *FirstClassPlugin) getCommands(plugins *plugin.ValidatedPlugins) []*co
133131
sourceType: FirstClassSourceType,
134132
},
135133
RunE: func(cmd *cobra.Command, args []string) error {
136-
return fcp.runFirstClassPluginCommand(cmd, args, ghClient, plugins)
134+
return fcp.runFirstClassPluginCommand(cmd, args, plugins)
137135
},
138136
DisableFlagParsing: true,
139137
}

internal/cli/plugin/github.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2025 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+
"github.com/cli/go-gh/v2/pkg/auth"
19+
"github.com/google/go-github/v61/github"
20+
)
21+
22+
func NewAuthenticatedGithubClient() *github.Client {
23+
// create a new github client
24+
ghClient := github.NewClient(nil)
25+
26+
// try to get the gh token from either the gh cli config or the environment variable (GH_TOKEN/GITHUB_TOKEN)
27+
if token, _ := auth.TokenForHost("github.com"); token != "" {
28+
ghClient = ghClient.WithAuthToken(token)
29+
}
30+
31+
return ghClient
32+
}

internal/cli/plugin/install.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type InstallOpts struct {
3131
cli.PreRunOpts
3232
cli.OutputOpts
3333
Opts
34+
ghClient *github.Client
3435
githubAsset *GithubAsset
3536
}
3637

@@ -75,7 +76,7 @@ func (opts *InstallOpts) validatePlugin(pluginDirectoryPath string) error {
7576

7677
func (opts *InstallOpts) Run(ctx context.Context) error {
7778
// get all plugin assets info from github repository
78-
assets, err := opts.githubAsset.getReleaseAssets()
79+
assets, err := opts.githubAsset.getReleaseAssets(opts.ghClient)
7980
if err != nil {
8081
return err
8182
}
@@ -87,7 +88,7 @@ func (opts *InstallOpts) Run(ctx context.Context) error {
8788
}
8889

8990
// download plugin asset archive file and save it as ReadCloser
90-
rc, err := opts.githubAsset.getPluginAssetsAsReadCloser(assetID, signatureID, pubKeyID)
91+
rc, err := opts.githubAsset.getPluginAssetsAsReadCloser(opts.ghClient, assetID, signatureID, pubKeyID)
9192
if err != nil {
9293
return err
9394
}
@@ -118,7 +119,9 @@ func (opts *InstallOpts) Run(ctx context.Context) error {
118119
}
119120

120121
func InstallBuilder(pluginOpts *Opts) *cobra.Command {
121-
opts := &InstallOpts{}
122+
opts := &InstallOpts{
123+
ghClient: NewAuthenticatedGithubClient(),
124+
}
122125
opts.Opts = *pluginOpts
123126

124127
const use = "install"
@@ -150,7 +153,6 @@ MongoDB provides an example plugin: https://github.com/mongodb/atlas-cli-plugin-
150153
return err
151154
}
152155
opts.githubAsset = githubAssetRelease
153-
opts.githubAsset.ghClient = github.NewClient(nil)
154156

155157
return opts.PreRunE(opts.checkForDuplicatePlugins)
156158
},

internal/cli/plugin/plugin_github_asset.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,9 @@ const (
5151
)
5252

5353
type GithubAsset struct {
54-
ghClient *github.Client
55-
owner string
56-
name string
57-
version *semver.Version
54+
owner string
55+
name string
56+
version *semver.Version
5857
}
5958

6059
func (g *GithubAsset) repository() string {
@@ -77,21 +76,24 @@ func (g *GithubAsset) getPluginDirectoryName() string {
7776
return fmt.Sprintf("%s@%s", g.owner, g.name)
7877
}
7978

80-
func (g *GithubAsset) getReleaseAssets() ([]*github.ReleaseAsset, error) {
79+
func (g *GithubAsset) getReleaseAssets(ghClient *github.Client) ([]*github.ReleaseAsset, error) {
8180
var err error
8281
var release *github.RepositoryRelease
8382

8483
// download latest release if version is not specified
8584
if g.version == nil {
8685
// download the 100 latest releases
8786
const MaxPerPage = 100
88-
releases, _, err := g.ghClient.Repositories.ListReleases(context.Background(), g.owner, g.name, &github.ListOptions{
87+
releases, _, err := ghClient.Repositories.ListReleases(context.Background(), g.owner, g.name, &github.ListOptions{
8988
Page: 0,
9089
PerPage: MaxPerPage,
9190
})
9291

9392
if err != nil {
94-
return nil, fmt.Errorf("could not fetch releases for %s, access to GitHub is required, see https://dochub.mongodb.org/core/atlas-cli-deploy-docker, %w", g.repository(), err)
93+
return nil, fmt.Errorf("could not fetch releases for %s, access to GitHub is required, see https://dochub.mongodb.org/core/atlas-cli-deploy-docker\n"+
94+
"if you are using a private repository, you need to set the GH_TOKEN environment variable (needs content read access to the repository) or authenticate with the github cli\n"+
95+
"more info about access tokens: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token\n\n"+
96+
"error: %w", g.repository(), err)
9597
}
9698

9799
// get the latest release that doesn't have prerelease info or metadata in the version tag
@@ -101,10 +103,10 @@ func (g *GithubAsset) getReleaseAssets() ([]*github.ReleaseAsset, error) {
101103
}
102104
} else {
103105
// try to find the release with the version tag with v prefix, if it does not exist try again without the prefix
104-
release, _, err = g.ghClient.Repositories.GetReleaseByTag(context.Background(), g.owner, g.name, "v"+g.version.String())
106+
release, _, err = ghClient.Repositories.GetReleaseByTag(context.Background(), g.owner, g.name, "v"+g.version.String())
105107

106108
if release == nil || err != nil {
107-
release, _, err = g.ghClient.Repositories.GetReleaseByTag(context.Background(), g.owner, g.name, g.version.String())
109+
release, _, err = ghClient.Repositories.GetReleaseByTag(context.Background(), g.owner, g.name, g.version.String())
108110
}
109111

110112
if err != nil {
@@ -240,8 +242,8 @@ func getSignatureAssetandKeyID(name string, assets []*github.ReleaseAsset) (int6
240242
return *signatureAsset.ID, *pubKeyAsset.ID, nil
241243
}
242244

243-
func (g *GithubAsset) getPluginAssetsAsReadCloser(assetID, sigAssetID, pubKeyAssetID int64) (io.ReadCloser, error) {
244-
rc, _, err := g.ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, assetID, http.DefaultClient)
245+
func (g *GithubAsset) getPluginAssetsAsReadCloser(ghClient *github.Client, assetID, sigAssetID, pubKeyAssetID int64) (io.ReadCloser, error) {
246+
rc, _, err := ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, assetID, http.DefaultClient)
245247
if err != nil {
246248
return nil, fmt.Errorf("could not download asset with ID %d from %s", assetID, g.repository())
247249
}
@@ -253,7 +255,7 @@ func (g *GithubAsset) getPluginAssetsAsReadCloser(assetID, sigAssetID, pubKeyAss
253255

254256
// Only do verification if IDs are not 0, i.e. when there is signature package available
255257
if sigAssetID != 0 && pubKeyAssetID != 0 {
256-
err = g.verifyAssetSignature(asset, sigAssetID, pubKeyAssetID)
258+
err = g.verifyAssetSignature(ghClient, asset, sigAssetID, pubKeyAssetID)
257259
if err != nil {
258260
return nil, err
259261
}
@@ -264,14 +266,14 @@ func (g *GithubAsset) getPluginAssetsAsReadCloser(assetID, sigAssetID, pubKeyAss
264266

265267
// verifyAssetSignature verifies the asset signature.
266268
// Returns nil if signature check is successful.
267-
func (g *GithubAsset) verifyAssetSignature(asset []byte, sigAssetID, pubKeyAssetID int64) error {
268-
sigRc, _, err := g.ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, sigAssetID, http.DefaultClient)
269+
func (g *GithubAsset) verifyAssetSignature(ghClient *github.Client, asset []byte, sigAssetID, pubKeyAssetID int64) error {
270+
sigRc, _, err := ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, sigAssetID, http.DefaultClient)
269271
if err != nil {
270272
return fmt.Errorf("could not download signature asset with ID %d from %s", sigAssetID, g.repository())
271273
}
272274
defer sigRc.Close()
273275

274-
keyRc, _, err := g.ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, pubKeyAssetID, http.DefaultClient)
276+
keyRc, _, err := ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, pubKeyAssetID, http.DefaultClient)
275277
if err != nil {
276278
return fmt.Errorf("could not download public key asset with ID %d from %s", pubKeyAssetID, g.repository())
277279
}

internal/cli/plugin/update.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type UpdateOpts struct {
4848
UpdateAll bool
4949
pluginSpecifier string
5050
pluginUpdateVersion *semver.Version
51+
ghClient *github.Client
5152
}
5253

5354
func printPluginUpdateWarning(p *plugin.Plugin, err error) {
@@ -132,7 +133,7 @@ func (opts *UpdateOpts) validatePlugin(pluginDirectoryPath string) error {
132133

133134
func (opts *UpdateOpts) updatePlugin(ctx context.Context, githubAssetRelease *GithubAsset, existingPlugin *plugin.Plugin) error {
134135
// get all plugin assets info from github repository
135-
assets, err := githubAssetRelease.getReleaseAssets()
136+
assets, err := githubAssetRelease.getReleaseAssets(opts.ghClient)
136137
if err != nil {
137138
return err
138139
}
@@ -144,7 +145,7 @@ func (opts *UpdateOpts) updatePlugin(ctx context.Context, githubAssetRelease *Gi
144145
}
145146

146147
// download plugin asset archive file and save it as ReadCloser
147-
rc, err := githubAssetRelease.getPluginAssetsAsReadCloser(assetID, signatureID, pubKeyID)
148+
rc, err := githubAssetRelease.getPluginAssetsAsReadCloser(opts.ghClient, assetID, signatureID, pubKeyID)
148149
if err != nil {
149150
return err
150151
}
@@ -198,8 +199,6 @@ func (opts *UpdateOpts) updatePlugin(ctx context.Context, githubAssetRelease *Gi
198199
}
199200

200201
func (opts *UpdateOpts) Run(ctx context.Context) error {
201-
ghClient := github.NewClient(nil)
202-
203202
// if update flag is set, update all plugin, if not update only specified plugin
204203
if opts.UpdateAll {
205204
// try to create GithubAssetRelease from each plugin - when create use it to update the plugin
@@ -218,7 +217,6 @@ func (opts *UpdateOpts) Run(ctx context.Context) error {
218217
}
219218

220219
// update using GithubAsset
221-
githubAsset.ghClient = ghClient
222220
err = opts.updatePlugin(ctx, githubAsset, p)
223221
if err != nil {
224222
printPluginUpdateWarning(p, err)
@@ -249,7 +247,6 @@ func (opts *UpdateOpts) Run(ctx context.Context) error {
249247

250248
// update using GithubAsset
251249
opts.Print(fmt.Sprintf(`Updating plugin "%s"`, existingPlugin.Name))
252-
githubAsset.ghClient = ghClient
253250
err = opts.updatePlugin(ctx, githubAsset, existingPlugin)
254251
if err != nil {
255252
return err
@@ -262,6 +259,7 @@ func (opts *UpdateOpts) Run(ctx context.Context) error {
262259
func UpdateBuilder(pluginOpts *Opts) *cobra.Command {
263260
opts := &UpdateOpts{
264261
UpdateAll: false,
262+
ghClient: NewAuthenticatedGithubClient(),
265263
}
266264
opts.Opts = *pluginOpts
267265

0 commit comments

Comments
 (0)