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

config: Fork out to git binary for config data. #149

Merged
merged 1 commit into from
Sep 27, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
111 changes: 71 additions & 40 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@
package config

import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"strings"
)

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
format "github.com/go-git/go-git/v5/plumbing/format/config"
var (
// execFn is a function to get the raw git config.
// Configurable to allow for overriding for testing.
execFn = realExec
)

// Config represents configuration options for gitsign.
Expand Down Expand Up @@ -48,21 +55,13 @@ type Config struct {
// Get fetches the gitsign config options for the repo in the current working
// directory.
func Get() (*Config, error) {
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{DetectDotGit: true})
if err != nil {
return nil, err
}
return getWithRepo(repo)
}

// getWithRepo fetches a config for a given repository. This is separated out
// from Get so that we can create in-memory repos for testing.
func getWithRepo(repo *git.Repository) (*Config, error) {
cfg, err := repo.ConfigScoped(config.GlobalScope)
r, err := execFn()
if err != nil {
return nil, err
return nil, fmt.Errorf("error reading config: %w", err)
}
cfg := parseConfig(r)

// Start with default config
out := &Config{
Fulcio: "https://fulcio.sigstore.dev",
Rekor: "https://rekor.sigstore.dev",
Expand All @@ -71,13 +70,9 @@ func getWithRepo(repo *git.Repository) (*Config, error) {
}

// Get values from config file.
for _, s := range cfg.Raw.Sections {
if s.IsName("gitsign") {
applyGitOptions(out, s.Options)
}
}
applyGitOptions(out, cfg)

// Get values from env vars
// Get values from env vars.

// Check for common environment variables that could be shared with other
// Sigstore tools. Gitsign envs should take precedence.
Expand All @@ -95,25 +90,61 @@ func getWithRepo(repo *git.Repository) (*Config, error) {
return out, nil
}

func applyGitOptions(out *Config, opts format.Options) {
// Iterate over options once instead of using Get (which itself iterates
// over options until a matching key is found).
for _, o := range opts {
switch o.Key {
case "fulcio":
out.Fulcio = o.Value
case "rekor":
out.Rekor = o.Value
case "clientID":
out.ClientID = o.Value
case "redirectURL":
out.RedirectURL = o.Value
case "issuer":
out.Issuer = o.Value
case "logPath":
out.LogPath = o.Value
case "connectorID":
out.ConnectorID = o.Value
// realExec forks out to the git binary to read the git config.
// We do this as a hack since go-git has issues parsing global configs
// for custom fields (https://github.com/go-git/go-git/issues/508) and
// doesn't support deprecated subsection syntaxes
// (https://github.com/sigstore/gitsign/issues/142).
func realExec() (io.Reader, error) {
cmd := exec.Command("git", "config", "--get-regexp", `^gitsign\.`)
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
if cmd.ProcessState.ExitCode() == 1 && stderr.Len() == 0 {
// git config returning exit code 1 with no stderr message can
// happen if there are no gitsign related configs set. Treat this
// like an non-error / empty config.
return stdout, nil
}
return nil, fmt.Errorf("%w: %s", err, stderr)
}
return stdout, nil
}

func parseConfig(r io.Reader) map[string]string {
out := map[string]string{}

s := bufio.NewScanner(r)
for s.Scan() {
raw := s.Text()
data := strings.Split(raw, " ")
if len(data) < 2 {
continue
}
out[data[0]] = data[1]
}
return out
}

func applyGitOptions(out *Config, cfg map[string]string) {
for k, v := range cfg {
switch {
case strings.EqualFold(k, "gitsign.fulcio"):
out.Fulcio = v
case strings.EqualFold(k, "gitsign.rekor"):
out.Rekor = v
case strings.EqualFold(k, "gitsign.clientID"):
out.ClientID = v
case strings.EqualFold(k, "gitsign.redirectURL"):
out.RedirectURL = v
case strings.EqualFold(k, "gitsign.issuer"):
out.Issuer = v
case strings.EqualFold(k, "gitsign.logPath"):
out.LogPath = v
case strings.EqualFold(k, "gitsign.connectorID"):
out.ConnectorID = v
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package config

import (
"io"
"os"
"testing"

"github.com/go-git/go-billy/v5/memfs"
Expand Down Expand Up @@ -85,7 +87,11 @@ func TestGet(t *testing.T) {
ConnectorID: "bar",
}

got, err := getWithRepo(repo)
execFn = func() (io.Reader, error) {
return os.Open("testdata/config.txt")
}

got, err := Get()
if err != nil {
t.Fatal(err)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/config/testdata/config.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
foo bar
gitsign.foo bar
gitsign.foo.bar baz
gitsign

gitsign.connectorid https://accounts.google.com
gitsign.FULCIO example.com
gitsign.Rekor example.com