Skip to content

Commit

Permalink
Merge pull request #76 from rigup/plugin-install-private-repo
Browse files Browse the repository at this point in the history
Adding ability to install plugins from private repos
  • Loading branch information
jessesomerville authored May 10, 2021
2 parents 33deb67 + d75b47b commit 01eb0cf
Show file tree
Hide file tree
Showing 13 changed files with 410 additions and 53 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
<img src="docs/img/logo.png" height="160">
</p>

> **NOTICE:** There is a bug in ephemeral-iam v0.0.16 that results in a failure
> to update to the next version. If you are on v0.0.16, you will need to manually
> update to v0.0.17.
# ephemeral-iam

A CLI tool that utilizes service account token generation to enable users to
Expand Down
34 changes: 33 additions & 1 deletion cmd/eiam/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"io/ioutil"
"strconv"
"strings"

"github.com/lithammer/dedent"
"github.com/sirupsen/logrus"
Expand All @@ -35,6 +36,7 @@ var (
loggingFormats = []string{"text", "json", "debug"}
boolConfigFields = []string{
appconfig.AuthProxyVerbose,
appconfig.GithubAuth,
appconfig.LoggingLevelTruncation,
appconfig.LoggingPadLevelText,
}
Expand All @@ -60,12 +62,23 @@ var configInfo = dedent.Dedent(`
│ authproxy.verbose │ When set to 'true', verbose output for │
│ │ proxy logs will be enabled │
├────────────────────────────────┼─────────────────────────────────────────────┤
│ binarypaths.cloudsqlproxy │ The path to the cloud_sql_proxy binary on │
│ │ your filesystem │
├────────────────────────────────┼─────────────────────────────────────────────┤
│ binarypaths.gcloud │ The path to the gcloud binary on your │
│ │ filesystem │
├────────────────────────────────┼─────────────────────────────────────────────┤
│ binarypaths.kubectl │ The path to the kubectl binary on your │
│ │ filesystem │
├────────────────────────────────┼─────────────────────────────────────────────┤
│ github.auth │ When set to 'true', the "plugins install" │
│ │ command will use the configured personal │
│ │ access token to authenticate to the Github │
│ │ API. │
├────────────────────────────────┼─────────────────────────────────────────────┤
│ github.tokens │ The configured Github personal access │
│ │ tokens │
├────────────────────────────────┼─────────────────────────────────────────────┤
│ logging.format │ The format for which to write console logs │
│ │ Can be 'json', 'text', or 'debug' │
├────────────────────────────────┼─────────────────────────────────────────────┤
Expand Down Expand Up @@ -105,7 +118,26 @@ func newCmdConfigPrint() *cobra.Command {
if err != nil {
return errorsutil.New("Failed to read configuration file", err)
}
fmt.Printf("\n%s\n", string(data))
lines := strings.Split(string(data), "\n")
inTokens := false
fmt.Printf("\n")
for i := 0; i < len(lines); i++ {
if lines[i] == " tokens:" {
inTokens = true
} else if inTokens {
// Mask Github personal access tokens.
tokenConfig := viper.GetStringMapString(appconfig.GithubTokens)
for tokenName := range tokenConfig {
fmt.Printf(" %s: **********\n", tokenName)
i++
}
i--
inTokens = false
continue
}
fmt.Println(lines[i])
}
fmt.Printf("\n")
return nil
},
}
Expand Down
24 changes: 17 additions & 7 deletions cmd/eiam/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,21 @@ func newCmdPlugins() *cobra.Command {
Plugins are '.so' files (Golang dynamic libraries) and stored in the 'plugins' directory
of your eiam configuration folder.
- Installing a plugin -
To install a plugin, take the '.so' file and place it in the 'plugins' directory of your
eiam configuration folder. eiam will automatically load any valid plugins in that
directory and the commands added by those plugins will be listed when you run:
'eiam --help'.
------------------------------- Installing a plugin -------------------------------
Plugins are loaded from the '/path/to/config/ephemeral-iam/plugins' directory. To install a
plugin, you just place the plugin's binary in that directory and eiam will automatically
discover and load it.
If the plugin you want to install is hosted in a Github repo and the binary is published as
a release in the repository, you can install the plugin using the 'eiam plugin install'
command.
`),
}

cmd.AddCommand(newCmdPluginsList())
cmd.AddCommand(newCmdPluginsInstall())
cmd.AddCommand(newCmdPluginsRemove())
cmd.AddCommand(newCmdPluginsAuth())
return cmd
}

Expand All @@ -70,6 +74,7 @@ func newCmdPluginsList() *cobra.Command {
func newCmdPluginsInstall() *cobra.Command {
var (
url string
tokenName string
repoOwner string
repoName string
)
Expand All @@ -81,6 +86,10 @@ func newCmdPluginsInstall() *cobra.Command {
The latest release in the provided repository is downloaded, extracted, and
the binary files are moved to the "plugins" directory.
If the plugin is hosted in a private repository, you need to provide
ephemeral-iam with a Github personal access token to authenticate
with. See 'eiam plugins auth --help' for more details.
`),
Args: func(cmd *cobra.Command, args []string) error {
urlRegex := regexp.MustCompile(`github\.com/(?P<user>[[:alnum:]\-]+)/(?P<repo>[[:alnum:]\.\-_]+)`)
Expand All @@ -99,13 +108,14 @@ func newCmdPluginsInstall() *cobra.Command {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return plugins.InstallPlugin(repoOwner, repoName)
return plugins.InstallPlugin(repoOwner, repoName, tokenName)
},
}
cmd.Flags().StringVarP(&url, "url", "u", "", "The URL of the plugin's Github repo")
if err := cmd.MarkFlagRequired("url"); err != nil {
util.Logger.Fatal(err.Error())
}
cmd.Flags().StringVarP(&tokenName, "token", "t", "", "The name of the Github access token to use for private repos")
return cmd
}

Expand Down Expand Up @@ -141,7 +151,7 @@ func newCmdPluginsRemove() *cobra.Command {

func selectPlugin() (*plugins.EphemeralIamPlugin, error) {
templates := &promptui.SelectTemplates{
Label: "{{ . }}?",
Label: "{{ . }}",
Active: " ► {{ .Name | red }}",
Inactive: " {{ .Name | red }}",
Selected: " ► {{ .Name | red | cyan }}",
Expand Down
158 changes: 158 additions & 0 deletions cmd/eiam/plugins_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package eiam

import (
"fmt"
"regexp"

"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/rigup/ephemeral-iam/internal/appconfig"
util "github.com/rigup/ephemeral-iam/internal/eiamutil"
errorsutil "github.com/rigup/ephemeral-iam/internal/errors"
)

var tokenName string

func newCmdPluginsAuth() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Manage Github authentication for installing plugins from private repos",
Long: `
To use the "eiam plugins install" command with private Github repos, ephemeral-iam
needs to make authenticated calls to the Github API using a Github personal access
token. The "eiam plugins auth" subcommands allow you to configure ephemeral-iam to
make authenticated calls to Github and manage the credentials that it uses.
You can configure multiple tokens at once and designate which token to use by
referencing the name that it was given when it was added (defaults to "default").
`,
}
cmd.AddCommand(newCmdPluginsAuthAdd())
cmd.AddCommand(newCmdPluginsAuthList())
cmd.AddCommand(newCmdPluginsAuthDelete())
return cmd
}

func newCmdPluginsAuthAdd() *cobra.Command {
tokenConfig := viper.GetStringMapString(appconfig.GithubTokens)
cmd := &cobra.Command{
Use: "add",
Short: "Add a Github personal access token to use to install plugins from private repositories",
RunE: func(cmd *cobra.Command, args []string) error {
if _, ok := tokenConfig[tokenName]; ok {
util.Logger.Warnf("A token with the name %s already exists", tokenName)
prompt := promptui.Prompt{
Label: fmt.Sprintf("Overwrite %s", tokenName),
IsConfirm: true,
}

if _, err := prompt.Run(); err != nil {
util.Logger.Warnf("Abandoning deletion of token: %s", tokenName)
return nil
}
}
util.Logger.Infof("Adding token with the name %s", tokenName)
prompt := promptui.Prompt{
Label: "Enter your Github Personal Access Token: ",
Mask: '●',
Validate: func(input string) error {
if len(input) != 40 {
return fmt.Errorf("incorrect input length: expected 40, got %d", len(input))
}
tokenRegex := regexp.MustCompile(`^(?:ghp_)?[[:alnum:]]{36}(?:[[:alnum:]]{4})?$`)
if !tokenRegex.MatchString(input) {
return fmt.Errorf("invalid input: input was not a valid personal access token")
}
return nil
},
}

token, err := prompt.Run()
if err != nil {
return errorsutil.New("User input failed", err)
}

tokenConfig[tokenName] = token
viper.Set(appconfig.GithubTokens, tokenConfig)
if !viper.GetBool(appconfig.GithubAuth) {
viper.Set(appconfig.GithubAuth, true)
}

if err := viper.WriteConfig(); err != nil {
return errorsutil.New("Failed to write updated configuration", err)
}
return nil
},
}
cmd.Flags().StringVarP(&tokenName, "name", "n", "default", "The name associated with the target access token")
return cmd
}

func newCmdPluginsAuthList() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Print the existing Github personal access tokens",
RunE: func(cmd *cobra.Command, args []string) error {
if !viper.GetBool(appconfig.GithubAuth) {
util.Logger.Warn("No Github credentials are currently configured.")
return nil
}
fmt.Println("\n GITHUB ACCESS TOKENS\n----------------------")
for tokenName := range viper.GetStringMapString(appconfig.GithubTokens) {
fmt.Println(tokenName)
}
fmt.Printf("\n")
return nil
},
}
return cmd
}

func newCmdPluginsAuthDelete() *cobra.Command {
tokenConfig := viper.GetStringMapString(appconfig.GithubTokens)
cmd := &cobra.Command{
Use: "delete",
Short: "Delete an existing Github personal access token from the config",
RunE: func(cmd *cobra.Command, args []string) error {
if tokenName == "" {
tn, err := util.SelectToken(tokenConfig)
if err != nil {
return errorsutil.New("Failed to select access token", err)
}
tokenName = tn
}
if _, ok := tokenConfig[tokenName]; !ok {
err := fmt.Errorf("no token with the name %s exists in the config", tokenName)
return errorsutil.New("Failed to delete token from config", err)
}

prompt := promptui.Prompt{
Label: fmt.Sprintf("Delete %s", tokenName),
IsConfirm: true,
}

if _, err := prompt.Run(); err != nil {
util.Logger.Warnf("Abandoning deletion of token: %s", tokenName)
return nil
}

delete(tokenConfig, tokenName)
viper.Set(appconfig.GithubTokens, tokenConfig)

if len(tokenConfig) == 0 {
viper.Set(appconfig.GithubAuth, false)
}

if err := viper.WriteConfig(); err != nil {
return errorsutil.New("Failed to write updated configuration", err)
}
util.Logger.Infof("%s was successfully deleted", tokenName)

return nil
},
}
cmd.Flags().StringVarP(&tokenName, "name", "n", "", "The name associated with the target access token")
return cmd
}
35 changes: 33 additions & 2 deletions docs/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,41 @@ To install a plugin, you just place the plugin's binary in that directory and

If the plugin you want to install is hosted in a Github repo and the binary is
published as a release in the repository, you can install the plugin using the
`eiam plugin install` command:
`eiam plugins install` command:

```
$ eiam plugin install --url github.com/user/repo-name
$ eiam plugins install --url github.com/user/repo-name
```

### Plugin stored in a private repository
If the plugin is hosted in a private repository, you need to provide `ephemeral-iam`
with a Github personal access token to authenticate with. You can use the
`eiam plugins auth` commands to add, list, and remove these access tokens.

**Add tokens:**
```
$ eiam plugins auth add --name "personal-token"
INFO Adding token with the name personal-token
✔ Enter your Github Personal Access Token: : ●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
$ eiam plugins auth add --name "organization-token"
INFO Adding token with the name organization-token
✔ Enter your Github Personal Access Token: : ●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
```

**List tokens:**
```
$ eiam plugins auth list
GITHUB ACCESS TOKENS
----------------------
personal-token
organization-token
```

**Install with authentication:**
```
$ eiam plugins install --url github.com/user/repo-name --token personal-token
```

## Developing a new plugin
Expand Down
6 changes: 3 additions & 3 deletions eiam.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import (
)

func main() {
errorsutil.CheckError(appconfig.InitConfig())
errorsutil.CheckError(appconfig.Setup())

if appconfig.Version != "v0.0.0" {
appconfig.CheckForNewRelease()
}

errorsutil.CheckError(appconfig.InitConfig())
errorsutil.CheckError(appconfig.Setup())

rootCmd, err := eiam.NewEphemeralIamCommand()
// Kill the loaded plugin clients. This is happening here to ensure that
// Kill is called after the command has finished running, but also accounts
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1
golang.org/x/mod v0.4.2
golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed
google.golang.org/api v0.45.0
google.golang.org/genproto v0.0.0-20210427215850-f767ed18ee4d
Expand Down
Loading

0 comments on commit 01eb0cf

Please sign in to comment.