Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Secrets from file #245

Merged
merged 1 commit into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions changelog/unreleased/file-secrets.md
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions docs/content/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ 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 other secret values like 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
Expand Down
16 changes: 16 additions & 0 deletions pkg/action/helper.go
Original file line number Diff line number Diff line change
@@ -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
}
75 changes: 43 additions & 32 deletions pkg/action/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package action
import (
"context"
"crypto/tls"
"encoding/base64"
"io"
"net/http"
"os"
Expand Down Expand Up @@ -222,9 +221,23 @@ func handler(cfg *config.Config, db store.Store, logger log.Logger, client *gith

if cfg.Collector.Workflows {
root.HandleFunc(cfg.Webhook.Path, func(w http.ResponseWriter, r *http.Request) {
secret, err := config.Value(cfg.Webhook.Secret)

if err != nil {
level.Error(logger).Log(
"msg", "failed to read webhook secret",
"error", err,
)

w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusInternalServerError)

io.WriteString(w, http.StatusText(http.StatusInternalServerError))
}

payload, err := github.ValidatePayload(
r,
[]byte(cfg.Webhook.Secret),
[]byte(secret),
)

if err != nil {
Expand Down Expand Up @@ -297,30 +310,6 @@ func handler(cfg *config.Config, db store.Store, logger log.Logger, client *gith
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 != ""
}
Expand All @@ -335,7 +324,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(
Expand All @@ -350,7 +339,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 {
Expand All @@ -369,12 +358,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,
},
),
),
Expand All @@ -383,7 +383,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(
Expand All @@ -398,7 +398,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 {
Expand Down Expand Up @@ -438,6 +438,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.NewClient(
oauth2.NewClient(
context.WithValue(
Expand All @@ -453,7 +464,7 @@ func getEnterprise(cfg *config.Config, logger log.Logger) (*github.Client, error
),
oauth2.StaticTokenSource(
&oauth2.Token{
AccessToken: cfg.Target.Token,
AccessToken: accessToken,
},
),
),
Expand Down
4 changes: 2 additions & 2 deletions pkg/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,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,
},
Expand All @@ -211,7 +211,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,
},
Expand Down
14 changes: 10 additions & 4 deletions pkg/command/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,25 @@ func setupLogger(cfg *config.Config) log.Logger {
}

func setupStorage(cfg *config.Config, logger log.Logger) (store.Store, error) {
parsed, err := url.Parse(cfg.Database.DSN)
dsn, err := config.Value(cfg.Database.DSN)

if err != nil {
return nil, fmt.Errorf("failed to read dsn: %w", err)
}

parsed, err := url.Parse(dsn)

if err != nil {
return nil, fmt.Errorf("failed to parse dsn: %w", err)
}

switch parsed.Scheme {
case "sqlite", "sqlite3":
return store.NewGenericStore(cfg.Database, logger)
return store.NewGenericStore(dsn, logger)
case "mysql", "mariadb":
return store.NewGenericStore(cfg.Database, logger)
return store.NewGenericStore(dsn, logger)
case "postgres", "postgresql":
return store.NewGenericStore(cfg.Database, logger)
return store.NewGenericStore(dsn, logger)
}

return nil, store.ErrUnknownDriver
Expand Down
33 changes: 33 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package config

import (
"encoding/base64"
"fmt"
"os"
"strings"
"time"

"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -93,3 +97,32 @@ func Labels() *cli.StringSlice {
"run",
)
}

// 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
}
5 changes: 2 additions & 3 deletions pkg/store/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"github.com/golang-migrate/migrate/v4/source"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/google/go-github/v56/github"
"github.com/promhippie/github_exporter/pkg/config"

// Import SQLite driver for database/sql
_ "modernc.org/sqlite"
Expand Down Expand Up @@ -395,8 +394,8 @@ func (s *genericStore) dsn() string {
}

// NewGenericStore initializes a new generic store.
func NewGenericStore(cfg config.Database, logger log.Logger) (Store, error) {
parsed, err := url.Parse(cfg.DSN)
func NewGenericStore(dsn string, logger log.Logger) (Store, error) {
parsed, err := url.Parse(dsn)

if err != nil {
return nil, fmt.Errorf("failed to parse dsn: %w", err)
Expand Down