diff --git a/build/ci/library_owners.json b/build/ci/library_owners.json index d1a1a2d4c1..ec86d63e9f 100644 --- a/build/ci/library_owners.json +++ b/build/ci/library_owners.json @@ -10,6 +10,7 @@ "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys": "apix-2", "github.com/bradleyjkemp/cupaloy/v2": "apix-2", "github.com/briandowns/spinner": "apix-2", + "github.com/cli/go-gh/v2": "apix-2", "github.com/creack/pty": "apix-2", "github.com/denisbrodbeck/machineid": "apix-2", "github.com/evergreen-ci/shrub": "apix-2", diff --git a/build/package/purls.txt b/build/package/purls.txt index a0ac0cafe0..a3e6365cc4 100644 --- a/build/package/purls.txt +++ b/build/package/purls.txt @@ -35,6 +35,8 @@ pkg:golang/github.com/bodgit/plumbing@v1.3.0 pkg:golang/github.com/bodgit/sevenzip@v1.6.0 pkg:golang/github.com/bodgit/windows@v1.0.1 pkg:golang/github.com/briandowns/spinner@v1.23.2 +pkg:golang/github.com/cli/go-gh/v2@v2.12.1 +pkg:golang/github.com/cli/safeexec@v1.0.0 pkg:golang/github.com/cloudflare/circl@v1.6.1 pkg:golang/github.com/denisbrodbeck/machineid@v1.0.1 pkg:golang/github.com/dsnet/compress@v0.0.2-0.20230904184137-39efe44ab707 @@ -65,7 +67,7 @@ pkg:golang/github.com/klauspost/pgzip@v1.2.6 pkg:golang/github.com/kylelemons/godebug@v1.1.0 pkg:golang/github.com/mattn/go-colorable@v0.1.13 pkg:golang/github.com/mattn/go-isatty@v0.0.20 -pkg:golang/github.com/mgutz/ansi@v0.0.0-20170206155736-9520e82c474b +pkg:golang/github.com/mgutz/ansi@v0.0.0-20200706080929-d51e80ef957d pkg:golang/github.com/mholt/archives@v0.1.2 pkg:golang/github.com/minio/minlz@v1.0.0 pkg:golang/github.com/mongodb-forks/digest@v1.1.0 diff --git a/go.mod b/go.mod index a83fcd4c8a..25ef6814ca 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( require ( github.com/bmatcuk/doublestar/v4 v4.0.2 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cli/safeexec v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/addlicense v1.1.1 // indirect github.com/google/go-licenses/v2 v2.0.0-alpha.1 // indirect @@ -101,6 +102,7 @@ require ( github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.0 // indirect github.com/bodgit/windows v1.0.1 // indirect + github.com/cli/go-gh/v2 v2.12.1 github.com/cloudflare/circl v1.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect @@ -135,7 +137,7 @@ require ( github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/minio/minlz v1.0.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/montanaflynn/stats v0.7.1 // indirect diff --git a/go.sum b/go.sum index 62a99ec7da..1917feb19d 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,10 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cli/go-gh/v2 v2.12.1 h1:SVt1/afj5FRAythyMV3WJKaUfDNsxXTIe7arZbwTWKA= +github.com/cli/go-gh/v2 v2.12.1/go.mod h1:+5aXmEOJsH9fc9mBHfincDwnS02j2AIA/DsTH0Bk5uw= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 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 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A= github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs= github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= diff --git a/internal/cli/plugin/first_class.go b/internal/cli/plugin/first_class.go index d0589ad9a3..ead0b6f9ce 100644 --- a/internal/cli/plugin/first_class.go +++ b/internal/cli/plugin/first_class.go @@ -17,7 +17,6 @@ package plugin import ( "fmt" - "github.com/google/go-github/v61/github" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/plugin" "github.com/spf13/cobra" ) @@ -90,16 +89,16 @@ func (fcp *FirstClassPlugin) isAlreadyInstalled(plugins *plugin.ValidatedPlugins return false } -func (fcp *FirstClassPlugin) runFirstClassPluginCommand(cmd *cobra.Command, args []string, ghClient *github.Client, plugins *plugin.ValidatedPlugins) error { +func (fcp *FirstClassPlugin) runFirstClassPluginCommand(cmd *cobra.Command, args []string, plugins *plugin.ValidatedPlugins) error { installOpts := &InstallOpts{ Opts: Opts{ plugins: plugins, }, + ghClient: NewAuthenticatedGithubClient(), } installOpts.githubAsset = &GithubAsset{ - ghClient: ghClient, - owner: fcp.Github.Owner, - name: fcp.Github.Name, + owner: fcp.Github.Owner, + name: fcp.Github.Name, } installOpts.Print("Installing first class plugin " + fcp.Name) @@ -122,7 +121,6 @@ func (fcp *FirstClassPlugin) runFirstClassPluginCommand(cmd *cobra.Command, args func (fcp *FirstClassPlugin) getCommands(plugins *plugin.ValidatedPlugins) []*cobra.Command { commands := make([]*cobra.Command, 0, len(fcp.Commands)) - ghClient := github.NewClient(nil) // for every command listed in the first class plugin, create a cobra command that installs the plugin for _, firstClassPluginCommand := range fcp.Commands { @@ -133,7 +131,7 @@ func (fcp *FirstClassPlugin) getCommands(plugins *plugin.ValidatedPlugins) []*co sourceType: FirstClassSourceType, }, RunE: func(cmd *cobra.Command, args []string) error { - return fcp.runFirstClassPluginCommand(cmd, args, ghClient, plugins) + return fcp.runFirstClassPluginCommand(cmd, args, plugins) }, DisableFlagParsing: true, } diff --git a/internal/cli/plugin/github.go b/internal/cli/plugin/github.go new file mode 100644 index 0000000000..23b14da520 --- /dev/null +++ b/internal/cli/plugin/github.go @@ -0,0 +1,32 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "github.com/cli/go-gh/v2/pkg/auth" + "github.com/google/go-github/v61/github" +) + +func NewAuthenticatedGithubClient() *github.Client { + // create a new github client + ghClient := github.NewClient(nil) + + // try to get the gh token from either the gh cli config or the environment variable (GH_TOKEN/GITHUB_TOKEN) + if token, _ := auth.TokenForHost("github.com"); token != "" { + ghClient = ghClient.WithAuthToken(token) + } + + return ghClient +} diff --git a/internal/cli/plugin/install.go b/internal/cli/plugin/install.go index a0e84862b9..5a864d24bf 100644 --- a/internal/cli/plugin/install.go +++ b/internal/cli/plugin/install.go @@ -31,6 +31,7 @@ type InstallOpts struct { cli.PreRunOpts cli.OutputOpts Opts + ghClient *github.Client githubAsset *GithubAsset } @@ -75,7 +76,7 @@ func (opts *InstallOpts) validatePlugin(pluginDirectoryPath string) error { func (opts *InstallOpts) Run(ctx context.Context) error { // get all plugin assets info from github repository - assets, err := opts.githubAsset.getReleaseAssets() + assets, err := opts.githubAsset.getReleaseAssets(opts.ghClient) if err != nil { return err } @@ -87,7 +88,7 @@ func (opts *InstallOpts) Run(ctx context.Context) error { } // download plugin asset archive file and save it as ReadCloser - rc, err := opts.githubAsset.getPluginAssetsAsReadCloser(assetID, signatureID, pubKeyID) + rc, err := opts.githubAsset.getPluginAssetsAsReadCloser(opts.ghClient, assetID, signatureID, pubKeyID) if err != nil { return err } @@ -118,7 +119,9 @@ func (opts *InstallOpts) Run(ctx context.Context) error { } func InstallBuilder(pluginOpts *Opts) *cobra.Command { - opts := &InstallOpts{} + opts := &InstallOpts{ + ghClient: NewAuthenticatedGithubClient(), + } opts.Opts = *pluginOpts const use = "install" @@ -150,7 +153,6 @@ MongoDB provides an example plugin: https://github.com/mongodb/atlas-cli-plugin- return err } opts.githubAsset = githubAssetRelease - opts.githubAsset.ghClient = github.NewClient(nil) return opts.PreRunE(opts.checkForDuplicatePlugins) }, diff --git a/internal/cli/plugin/plugin_github_asset.go b/internal/cli/plugin/plugin_github_asset.go index a53f779499..95218950c3 100644 --- a/internal/cli/plugin/plugin_github_asset.go +++ b/internal/cli/plugin/plugin_github_asset.go @@ -51,10 +51,9 @@ const ( ) type GithubAsset struct { - ghClient *github.Client - owner string - name string - version *semver.Version + owner string + name string + version *semver.Version } func (g *GithubAsset) repository() string { @@ -77,7 +76,7 @@ func (g *GithubAsset) getPluginDirectoryName() string { return fmt.Sprintf("%s@%s", g.owner, g.name) } -func (g *GithubAsset) getReleaseAssets() ([]*github.ReleaseAsset, error) { +func (g *GithubAsset) getReleaseAssets(ghClient *github.Client) ([]*github.ReleaseAsset, error) { var err error var release *github.RepositoryRelease @@ -85,13 +84,16 @@ func (g *GithubAsset) getReleaseAssets() ([]*github.ReleaseAsset, error) { if g.version == nil { // download the 100 latest releases const MaxPerPage = 100 - releases, _, err := g.ghClient.Repositories.ListReleases(context.Background(), g.owner, g.name, &github.ListOptions{ + releases, _, err := ghClient.Repositories.ListReleases(context.Background(), g.owner, g.name, &github.ListOptions{ Page: 0, PerPage: MaxPerPage, }) if err != nil { - 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) + 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"+ + "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"+ + "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"+ + "error: %w", g.repository(), err) } // 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) { } } else { // try to find the release with the version tag with v prefix, if it does not exist try again without the prefix - release, _, err = g.ghClient.Repositories.GetReleaseByTag(context.Background(), g.owner, g.name, "v"+g.version.String()) + release, _, err = ghClient.Repositories.GetReleaseByTag(context.Background(), g.owner, g.name, "v"+g.version.String()) if release == nil || err != nil { - release, _, err = g.ghClient.Repositories.GetReleaseByTag(context.Background(), g.owner, g.name, g.version.String()) + release, _, err = ghClient.Repositories.GetReleaseByTag(context.Background(), g.owner, g.name, g.version.String()) } if err != nil { @@ -240,8 +242,8 @@ func getSignatureAssetandKeyID(name string, assets []*github.ReleaseAsset) (int6 return *signatureAsset.ID, *pubKeyAsset.ID, nil } -func (g *GithubAsset) getPluginAssetsAsReadCloser(assetID, sigAssetID, pubKeyAssetID int64) (io.ReadCloser, error) { - rc, _, err := g.ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, assetID, http.DefaultClient) +func (g *GithubAsset) getPluginAssetsAsReadCloser(ghClient *github.Client, assetID, sigAssetID, pubKeyAssetID int64) (io.ReadCloser, error) { + rc, _, err := ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, assetID, http.DefaultClient) if err != nil { return nil, fmt.Errorf("could not download asset with ID %d from %s", assetID, g.repository()) } @@ -253,7 +255,7 @@ func (g *GithubAsset) getPluginAssetsAsReadCloser(assetID, sigAssetID, pubKeyAss // Only do verification if IDs are not 0, i.e. when there is signature package available if sigAssetID != 0 && pubKeyAssetID != 0 { - err = g.verifyAssetSignature(asset, sigAssetID, pubKeyAssetID) + err = g.verifyAssetSignature(ghClient, asset, sigAssetID, pubKeyAssetID) if err != nil { return nil, err } @@ -264,14 +266,14 @@ func (g *GithubAsset) getPluginAssetsAsReadCloser(assetID, sigAssetID, pubKeyAss // verifyAssetSignature verifies the asset signature. // Returns nil if signature check is successful. -func (g *GithubAsset) verifyAssetSignature(asset []byte, sigAssetID, pubKeyAssetID int64) error { - sigRc, _, err := g.ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, sigAssetID, http.DefaultClient) +func (g *GithubAsset) verifyAssetSignature(ghClient *github.Client, asset []byte, sigAssetID, pubKeyAssetID int64) error { + sigRc, _, err := ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, sigAssetID, http.DefaultClient) if err != nil { return fmt.Errorf("could not download signature asset with ID %d from %s", sigAssetID, g.repository()) } defer sigRc.Close() - keyRc, _, err := g.ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, pubKeyAssetID, http.DefaultClient) + keyRc, _, err := ghClient.Repositories.DownloadReleaseAsset(context.Background(), g.owner, g.name, pubKeyAssetID, http.DefaultClient) if err != nil { return fmt.Errorf("could not download public key asset with ID %d from %s", pubKeyAssetID, g.repository()) } diff --git a/internal/cli/plugin/update.go b/internal/cli/plugin/update.go index 03a7f259b1..7a35ba0908 100644 --- a/internal/cli/plugin/update.go +++ b/internal/cli/plugin/update.go @@ -48,6 +48,7 @@ type UpdateOpts struct { UpdateAll bool pluginSpecifier string pluginUpdateVersion *semver.Version + ghClient *github.Client } func printPluginUpdateWarning(p *plugin.Plugin, err error) { @@ -132,7 +133,7 @@ func (opts *UpdateOpts) validatePlugin(pluginDirectoryPath string) error { func (opts *UpdateOpts) updatePlugin(ctx context.Context, githubAssetRelease *GithubAsset, existingPlugin *plugin.Plugin) error { // get all plugin assets info from github repository - assets, err := githubAssetRelease.getReleaseAssets() + assets, err := githubAssetRelease.getReleaseAssets(opts.ghClient) if err != nil { return err } @@ -144,7 +145,7 @@ func (opts *UpdateOpts) updatePlugin(ctx context.Context, githubAssetRelease *Gi } // download plugin asset archive file and save it as ReadCloser - rc, err := githubAssetRelease.getPluginAssetsAsReadCloser(assetID, signatureID, pubKeyID) + rc, err := githubAssetRelease.getPluginAssetsAsReadCloser(opts.ghClient, assetID, signatureID, pubKeyID) if err != nil { return err } @@ -198,8 +199,6 @@ func (opts *UpdateOpts) updatePlugin(ctx context.Context, githubAssetRelease *Gi } func (opts *UpdateOpts) Run(ctx context.Context) error { - ghClient := github.NewClient(nil) - // if update flag is set, update all plugin, if not update only specified plugin if opts.UpdateAll { // 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 { } // update using GithubAsset - githubAsset.ghClient = ghClient err = opts.updatePlugin(ctx, githubAsset, p) if err != nil { printPluginUpdateWarning(p, err) @@ -249,7 +247,6 @@ func (opts *UpdateOpts) Run(ctx context.Context) error { // update using GithubAsset opts.Print(fmt.Sprintf(`Updating plugin "%s"`, existingPlugin.Name)) - githubAsset.ghClient = ghClient err = opts.updatePlugin(ctx, githubAsset, existingPlugin) if err != nil { return err @@ -262,6 +259,7 @@ func (opts *UpdateOpts) Run(ctx context.Context) error { func UpdateBuilder(pluginOpts *Opts) *cobra.Command { opts := &UpdateOpts{ UpdateAll: false, + ghClient: NewAuthenticatedGithubClient(), } opts.Opts = *pluginOpts