Skip to content

Commit

Permalink
plugin: Add support for host-specific GitHub tokens (#2025)
Browse files Browse the repository at this point in the history
* Add support for host-specific GitHub tokens

* Update docs/user-guide/plugins.md

Co-authored-by: Ben Drucker <bvdrucker@gmail.com>

---------

Co-authored-by: Ben Drucker <bvdrucker@gmail.com>
  • Loading branch information
wata727 and bendrucker authored Apr 30, 2024
1 parent 568a2fe commit 3802c92
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 3 deletions.
14 changes: 14 additions & 0 deletions docs/user-guide/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ To increase the rate limit, you can send an authenticated request by authenticat

It's also a good idea to cache the plugin directory, as TFLint will only send requests if plugins aren't installed. The [setup-tflint action](https://github.com/terraform-linters/setup-tflint#usage) includes an example of caching in GitHub Actions.

If you host your plugins on GitHub Enterprise Server (GHES), you may need to use a different token than on GitHub.com. In this case, you can use a host-specific token like `GITHUB_TOKEN_example_com`. The hostname must be normalized with Punycode. Use "_" instead of "." and "__" instead of "-".

```hcl
# GITHUB_TOKEN will be used
plugin "foo" {
source = "github.com/org/tflint-ruleset-foo"
}
# GITHUB_TOKEN_example_com will be used preferentially and will fall back to GITHUB_TOKEN if not set.
plugin "bar" {
source = "example.com/org/tflint-ruleset-bar"
}
```

## Keeping plugins up to date

We recommend using automatic updates to keep your plugin version up-to-date. [Renovate supports TFLint plugins](https://docs.renovatebot.com/modules/manager/tflint-plugin/) to easily set up automated update workflows.
Expand Down
53 changes: 50 additions & 3 deletions plugin/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"os"
"path/filepath"
"runtime"
"strings"

"github.com/google/go-github/v53/github"
"github.com/terraform-linters/tflint/tflint"
"golang.org/x/net/idna"
"golang.org/x/oauth2"
)

Expand Down Expand Up @@ -204,6 +206,53 @@ func (c *InstallConfig) downloadToTempFile(asset *github.ReleaseAsset) (*os.File
return file, nil
}

// getGitHubToken gets a GitHub access token from environment variables.
// Environment variables are used in the following order of priority:
//
// - GITHUB_TOKEN_{source_host} (e.g. GITHUB_TOKEN_example_com)
// - GITHUB_TOKEN
//
// In most cases, GITHUB_TOKEN will meet your requirements, but GITHUB_TOKEN_{source_host}
// can be useful, for example if you are hosting your plugin on GHES.
// The host name must be normalized with Punycode, and "-" can be converted to "__" and "." to "-".
func (c *InstallConfig) getGitHubToken() string {
prefix := "GITHUB_TOKEN_"
for _, env := range os.Environ() {
eqIdx := strings.Index(env, "=")
if eqIdx < 0 {
continue
}
name := env[:eqIdx]
value := env[eqIdx+1:]

if !strings.HasPrefix(name, prefix) {
continue
}

rawHost := name[len(prefix):]
rawHost = strings.ReplaceAll(rawHost, "__", "-")
rawHost = strings.ReplaceAll(rawHost, "_", ".")
host, err := idna.Lookup.ToUnicode(rawHost)
if err != nil {
log.Printf(`[DEBUG] Failed to convert "%s" to Unicode format: %s`, rawHost, err)
continue
}

if host != c.SourceHost {
continue
}
log.Printf("[DEBUG] %s set, plugin requests to %s will be authenticated", name, c.SourceHost)
return value
}

if t := os.Getenv("GITHUB_TOKEN"); t != "" {
log.Printf("[DEBUG] GITHUB_TOKEN set, plugin requests to the GitHub API will be authenticated")
return t
}

return ""
}

func extractFileFromZipFile(zipFile *os.File, savePath string) error {
zipFileStat, err := zipFile.Stat()
if err != nil {
Expand Down Expand Up @@ -250,9 +299,7 @@ func newGitHubClient(ctx context.Context, config *InstallConfig) (*github.Client
Transport: http.DefaultTransport,
}

if t := os.Getenv("GITHUB_TOKEN"); t != "" {
log.Printf("[DEBUG] GITHUB_TOKEN set, plugin requests to the GitHub API will be authenticated")

if t := config.getGitHubToken(); t != "" {
hc = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: t,
}))
Expand Down
132 changes: 132 additions & 0 deletions plugin/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,135 @@ func TestNewGitHubClient(t *testing.T) {
})
}
}

func TestGetGitHubToken(t *testing.T) {
tests := []struct {
name string
config *InstallConfig
envs map[string]string
want string
}{
{
name: "no token",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "github.com",
},
},
want: "",
},
{
name: "GITHUB_TOKEN",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "github.com",
},
},
envs: map[string]string{
"GITHUB_TOKEN": "github_com_token",
},
want: "github_com_token",
},
{
name: "GITHUB_TOKEN_example_com",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "example.com",
},
},
envs: map[string]string{
"GITHUB_TOKEN_example_com": "example_com_token",
},
want: "example_com_token",
},
{
name: "GITHUB_TOKEN and GITHUB_TOKEN_example_com",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "example.com",
},
},
envs: map[string]string{
"GITHUB_TOKEN": "github_com_token",
"GITHUB_TOKEN_example_com": "example_com_token",
},
want: "example_com_token",
},
{
name: "GITHUB_TOKEN_example_com and GITHUB_TOKEN_example_org",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "example.com",
},
},
envs: map[string]string{
"GITHUB_TOKEN_example_com": "example_com_token",
"GITHUB_TOKEN_example_org": "example_org_token",
},
want: "example_com_token",
},
{
name: "GITHUB_TOKEN_{source_host} found, but source host is not matched",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "example.org",
},
},
envs: map[string]string{
"GITHUB_TOKEN_example_com": "example_com_token",
},
want: "",
},
{
name: "GITHUB_TOKEN_{source_host} and GITHUB_TOKEN found, but source host is not matched",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "example.org",
},
},
envs: map[string]string{
"GITHUB_TOKEN_example_com": "example_com_token",
"GITHUB_TOKEN": "github_com_token",
},
want: "github_com_token",
},
{
name: "GITHUB_TOKEN_xn--lhr645fjve.jp",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "総務省.jp",
},
},
envs: map[string]string{
"GITHUB_TOKEN_xn--lhr645fjve.jp": "mic_jp_token",
},
want: "mic_jp_token",
},
{
name: "GITHUB_TOKEN_xn____lhr645fjve_jp",
config: &InstallConfig{
PluginConfig: &tflint.PluginConfig{
SourceHost: "総務省.jp",
},
},
envs: map[string]string{
"GITHUB_TOKEN_xn____lhr645fjve_jp": "mic_jp_token",
},
want: "mic_jp_token",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Setenv("GITHUB_TOKEN", "")
for k, v := range test.envs {
t.Setenv(k, v)
}

got := test.config.getGitHubToken()
if got != test.want {
t.Errorf("got %q, want %q", got, test.want)
}
})
}
}

0 comments on commit 3802c92

Please sign in to comment.