From 95f8f48f6e1df48799f8dc9fc4799477f2588320 Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Thu, 10 Aug 2023 19:18:33 +0200 Subject: [PATCH] feat: make it possible to read secrets from files --- changelog/unreleased/file-secrets.md | 12 ++++++ docs/content/usage.md | 21 ++++++++++ pkg/action/helper.go | 16 ++++++++ pkg/action/server.go | 59 +++++++++++++--------------- pkg/command/command.go | 4 +- pkg/config/config.go | 33 ++++++++++++++++ 6 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 changelog/unreleased/file-secrets.md create mode 100644 pkg/action/helper.go diff --git a/changelog/unreleased/file-secrets.md b/changelog/unreleased/file-secrets.md new file mode 100644 index 0000000..6150292 --- /dev/null +++ b/changelog/unreleased/file-secrets.md @@ -0,0 +1,12 @@ +Change: Read secrets form files + +We have added proper support to load secrets like the token or the private key +for app authentication from files or from base64-encoded strings. Just provide +the flags or environment variables for token or private key with a DSN formatted +string like `file://path/to/file` or `base64://Zm9vYmFy`. + +Since the private key for GitHub App authentication had been provided in +base64-encoded format this is a breaking change as this won't work anymore until +you prefix the value with `base64://`. + +https://github.com/promhippie/github_exporter/pull/245 diff --git a/docs/content/usage.md b/docs/content/usage.md index 530691b..49b1312 100644 --- a/docs/content/usage.md +++ b/docs/content/usage.md @@ -98,6 +98,27 @@ directly at [http://localhost:9504/metrics](http://localhost:9504/metrics): - GITHUB_EXPORTER_REPO=promhippie/example {{< / highlight >}} +It's also possible to provide the token to access the GitHub API gets provided +by a file, in case you are using some kind of secret provider. For this use case +you can write the token to a file on any path and reference it with the +following format: + +{{< highlight diff >}} + github-exporter: + image: promhippie/github-exporter:latest + restart: always + environment: +- - GITHUB_EXPORTER_TOKEN=bldyecdtysdahs76ygtbw51w3oeo6a4cvjwoitmb ++ - GITHUB_EXPORTER_TOKEN=file://path/to/secret/file/with/token + - GITHUB_EXPORTER_LOG_PRETTY=true + - GITHUB_EXPORTER_ORG=promhippie + - GITHUB_EXPORTER_REPO=promhippie/example +{{< / highlight >}} + +Besides the `file://` format we currently also support `base64://` which expects +the token in a base64 encoded format. This functionality can be used for the +token and the private key for GitHub App authentication so far. + If you want to collect the metrics of all repositories within an organization you are able to use globbing, but be aware that all repositories matched by globbing won't provide metrics for the number of subscribers, the number of diff --git a/pkg/action/helper.go b/pkg/action/helper.go new file mode 100644 index 0000000..f6fa442 --- /dev/null +++ b/pkg/action/helper.go @@ -0,0 +1,16 @@ +package action + +// boolP returns a boolean pointer. +func boolP(i bool) *bool { + return &i +} + +// stringP returns a string pointer. +func stringP(i string) *string { + return &i +} + +// slceP returns a slice pointer. +func sliceP(i []string) *[]string { + return &i +} diff --git a/pkg/action/server.go b/pkg/action/server.go index 1398437..b681b38 100644 --- a/pkg/action/server.go +++ b/pkg/action/server.go @@ -3,7 +3,6 @@ package action import ( "context" "crypto/tls" - "encoding/base64" "io" "net/http" "os" @@ -233,30 +232,6 @@ func handler(cfg *config.Config, logger log.Logger, client *github.Client) *chi. return mux } -func boolP(i bool) *bool { - return &i -} - -func stringP(i string) *string { - return &i -} - -func sliceP(i []string) *[]string { - return &i -} - -func contentOrDecode(file string) ([]byte, error) { - decoded, err := base64.StdEncoding.DecodeString( - file, - ) - - if err != nil { - return os.ReadFile(file) - } - - return decoded, nil -} - func useEnterprise(cfg *config.Config, _ log.Logger) bool { return cfg.Target.BaseURL != "" } @@ -271,7 +246,7 @@ func getClient(cfg *config.Config, logger log.Logger) (*github.Client, error) { } if useApplication(cfg, logger) { - privateKey, err := contentOrDecode(cfg.Target.PrivateKey) + privateKey, err := config.Value(cfg.Target.PrivateKey) if err != nil { level.Error(logger).Log( @@ -286,7 +261,7 @@ func getClient(cfg *config.Config, logger log.Logger) (*github.Client, error) { http.DefaultTransport, cfg.Target.AppID, cfg.Target.InstallID, - privateKey, + []byte(privateKey), ) if err != nil { @@ -305,12 +280,23 @@ func getClient(cfg *config.Config, logger log.Logger) (*github.Client, error) { ), nil } + accessToken, err := config.Value(cfg.Target.Token) + + if err != nil { + level.Error(logger).Log( + "msg", "Failed to read token", + "err", err, + ) + + return nil, err + } + return github.NewClient( oauth2.NewClient( context.Background(), oauth2.StaticTokenSource( &oauth2.Token{ - AccessToken: cfg.Target.Token, + AccessToken: accessToken, }, ), ), @@ -319,7 +305,7 @@ func getClient(cfg *config.Config, logger log.Logger) (*github.Client, error) { func getEnterprise(cfg *config.Config, logger log.Logger) (*github.Client, error) { if useApplication(cfg, logger) { - privateKey, err := contentOrDecode(cfg.Target.PrivateKey) + privateKey, err := config.Value(cfg.Target.PrivateKey) if err != nil { level.Error(logger).Log( @@ -334,7 +320,7 @@ func getEnterprise(cfg *config.Config, logger log.Logger) (*github.Client, error http.DefaultTransport, cfg.Target.AppID, cfg.Target.InstallID, - privateKey, + []byte(privateKey), ) if err != nil { @@ -373,6 +359,17 @@ func getEnterprise(cfg *config.Config, logger log.Logger) (*github.Client, error return client, err } + accessToken, err := config.Value(cfg.Target.Token) + + if err != nil { + level.Error(logger).Log( + "msg", "Failed to read token", + "err", err, + ) + + return nil, err + } + client, err := github.NewEnterpriseClient( cfg.Target.BaseURL, cfg.Target.BaseURL, @@ -390,7 +387,7 @@ func getEnterprise(cfg *config.Config, logger log.Logger) (*github.Client, error ), oauth2.StaticTokenSource( &oauth2.Token{ - AccessToken: cfg.Target.Token, + AccessToken: accessToken, }, ), ), diff --git a/pkg/command/command.go b/pkg/command/command.go index 1caccab..09ad5cb 100644 --- a/pkg/command/command.go +++ b/pkg/command/command.go @@ -112,7 +112,7 @@ func RootFlags(cfg *config.Config) []cli.Flag { &cli.StringFlag{ Name: "github.token", Value: "", - Usage: "Access token for the GitHub API", + Usage: "Access token for the GitHub API, also supports file:// and base64://", EnvVars: []string{"GITHUB_EXPORTER_TOKEN"}, Destination: &cfg.Target.Token, }, @@ -131,7 +131,7 @@ func RootFlags(cfg *config.Config) []cli.Flag { &cli.StringFlag{ Name: "github.private_key", Value: "", - Usage: "Private key for the GitHub app, path or base64-encoded", + Usage: "Private key for the GitHub app, also supports file:// and base64://", EnvVars: []string{"GITHUB_EXPORTER_PRIVATE_KEY"}, Destination: &cfg.Target.PrivateKey, }, diff --git a/pkg/config/config.go b/pkg/config/config.go index 461aa68..63c894e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,6 +1,10 @@ package config import ( + "encoding/base64" + "fmt" + "os" + "strings" "time" "github.com/urfave/cli/v2" @@ -65,3 +69,32 @@ type Config struct { func Load() *Config { return &Config{} } + +// Value returns the config value based on a DSN. +func Value(val string) (string, error) { + if strings.HasPrefix(val, "file://") { + content, err := os.ReadFile( + strings.TrimPrefix(val, "file://"), + ) + + if err != nil { + return "", fmt.Errorf("failed to parse secret file: %w", err) + } + + return string(content), nil + } + + if strings.HasPrefix(val, "base64://") { + content, err := base64.StdEncoding.DecodeString( + strings.TrimPrefix(val, "base64://"), + ) + + if err != nil { + return "", fmt.Errorf("failed to parse base64 value: %w", err) + } + + return string(content), nil + } + + return val, nil +}