mirror of
https://github.com/acaloiaro/ess
synced 2026-07-21 18:29:08 +00:00
- Re-implement with Golang in place of of Bash - Add `env-sample-sync install` to support native git hooks (instead of relying on `pre-commit`)
291 lines
7.3 KiB
Go
291 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/hashicorp/go-envparse"
|
|
)
|
|
|
|
type exampleFlag map[string]string
|
|
|
|
func (e exampleFlag) String() string {
|
|
var str string
|
|
for key, val := range e {
|
|
str = fmt.Sprintf(`%s --example="%s=%s"`, str, key, val)
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
func (e exampleFlag) Set(value string) (err error) {
|
|
exampleParts := strings.Split(value, "=")
|
|
if len(exampleParts) != 2 {
|
|
err = errors.New("examples must be provided in KEY=VALUE format")
|
|
return
|
|
}
|
|
|
|
e[exampleParts[0]] = exampleParts[1]
|
|
|
|
return nil
|
|
}
|
|
|
|
var envFileFlag string
|
|
var sampleFileFlag string
|
|
var examplesFlag = make(exampleFlag)
|
|
|
|
func init() {
|
|
flag.StringVar(&envFileFlag, "env-file", ".env", "-env-file=.env_file")
|
|
flag.StringVar(&sampleFileFlag, "sample-file", "env.sample", "-sample-file=env_var.sample")
|
|
flag.Var(examplesFlag, "example", "--example=FOO=\"my foo value\" --example=BAR=\"my bar value\"")
|
|
|
|
flag.Usage = func() {
|
|
cmd := os.Args[0]
|
|
fmt.Fprintf(os.Stderr, "Usage of %s:\n", cmd)
|
|
fmt.Fprintf(os.Stderr, "%s [-flags] %s \n\n", cmd, "[install|sync]")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
flag.Parse()
|
|
}
|
|
|
|
func main() {
|
|
command := "sync"
|
|
args := flag.Args()
|
|
if len(args) > 0 {
|
|
command = args[0]
|
|
}
|
|
|
|
projectPath, gitDirPath := gitDirPaths()
|
|
|
|
switch command {
|
|
case "sync":
|
|
sync(projectPath)
|
|
cmd := exec.Command("git", "add", filepath.Join(projectPath, sampleFileFlag))
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
fmt.Printf("unable to add sample file '%s' to git: %v", sampleFileFlag, err)
|
|
os.Exit(1)
|
|
}
|
|
case "install":
|
|
err := installHook(gitDirPath)
|
|
if err != nil {
|
|
fmt.Println("unable to install pre-commit hook:", err)
|
|
os.Exit(1)
|
|
}
|
|
default:
|
|
fmt.Printf("env-sample-sync: unknown command '%s'\n\n", command)
|
|
flag.Usage()
|
|
}
|
|
}
|
|
|
|
func sync(dir string) {
|
|
envFilePath := filepath.Join(dir, envFileFlag)
|
|
sampleFilePath := filepath.Join(dir, sampleFileFlag)
|
|
|
|
envFileReader, err := os.Open(envFilePath)
|
|
if err != nil {
|
|
fmt.Printf("env file '%s' was not found. skipping sync.\n", envFilePath)
|
|
os.Exit(0)
|
|
}
|
|
|
|
envFile, err := envparse.Parse(envFileReader)
|
|
if err != nil {
|
|
fmt.Println("unable to parse env file:", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
scrubEnvFile(envFile, examplesFlag)
|
|
err = writeSampleFile(envFile, envFilePath, sampleFilePath)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func writeSampleFile(sampleFileContent map[string]string, envFilePath, sampleFilePath string) (err error) {
|
|
sampleFile, err := os.Create(sampleFilePath)
|
|
if err != nil {
|
|
err = fmt.Errorf("unable to create sample file: %w", err)
|
|
return
|
|
}
|
|
defer sampleFile.Close()
|
|
|
|
sampleWriter := bufio.NewWriter(sampleFile)
|
|
envFile, err := os.Open(envFilePath)
|
|
if err != nil {
|
|
err = fmt.Errorf("unable to read env file: %w", err)
|
|
return
|
|
}
|
|
defer envFile.Close()
|
|
|
|
scanner := bufio.NewScanner(envFile)
|
|
|
|
// interate through the env file line-by-line, searching for environment variables
|
|
// any line that starts with a key from `sampleFileContents` followed by an equal sign `=` is considered
|
|
// a line that contains a secret
|
|
for scanner.Scan() {
|
|
envFileLine := scanner.Text()
|
|
scrubbedLine := replaceSecrets(envFileLine, sampleFileContent)
|
|
sampleWriter.WriteString(fmt.Sprintf("%s\n", scrubbedLine))
|
|
}
|
|
|
|
sampleWriter.Flush()
|
|
|
|
return
|
|
}
|
|
|
|
func scrubEnvFile(envFile map[string]string, examples map[string]string) {
|
|
for envFileKey := range envFile {
|
|
exampleVal, ok := examples[envFileKey]
|
|
if ok {
|
|
envFile[envFileKey] = exampleVal
|
|
} else {
|
|
envFile[envFileKey] = fmt.Sprintf("<%s>", envFileKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
// replaceSecrets replaces the content of env file entries (lines) with a new line that is scrubbed of secrets
|
|
//
|
|
// any line containing a variable name followed by and equal sign is considered to contain a secret
|
|
func replaceSecrets(envFileEntry string, sampleFileContent map[string]string) (newLine string) {
|
|
var r *regexp.Regexp
|
|
newLine = envFileEntry
|
|
|
|
for secretKey, secretPlaceholder := range sampleFileContent {
|
|
r = regexp.MustCompile(fmt.Sprintf("(%s.*=.*)", secretKey))
|
|
if r.MatchString(envFileEntry) {
|
|
newLine = r.ReplaceAllString(envFileEntry, fmt.Sprintf("%s=%s", secretKey, secretPlaceholder))
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// gitDirPath returns the path the the GIT_DIR
|
|
func gitDirPaths() (projectPath, gitDir string) {
|
|
var outb, errb bytes.Buffer
|
|
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
|
cmd.Stdout = &outb
|
|
cmd.Stderr = &errb
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
fmt.Println("unable to find project directory:", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
projectPath = strings.TrimRight(outb.String(), "\r\n")
|
|
|
|
outb.Reset()
|
|
errb.Reset()
|
|
|
|
cmd = exec.Command("git", "rev-parse", "--git-dir")
|
|
cmd.Stdout = &outb
|
|
cmd.Stderr = &errb
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
fmt.Println("unable to find GIT_DIR:", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
gitDir = strings.TrimRight(outb.String(), "\r\n")
|
|
gitDir = filepath.Join(projectPath, gitDir)
|
|
|
|
return
|
|
}
|
|
|
|
var preCommitScriptTemplate = `#!/usr/bin/env bash
|
|
# File generated by env-sample-sync https://github.com/acaloiaro/env-sample-sync
|
|
|
|
ARGS=({{ .Args }})
|
|
|
|
for SCRIPT in $(dirname -- "${BASH_SOURCE[0]}")/{{ .PreCommitHooksDir }}/*; do
|
|
exec "$SCRIPT" "${ARGS[@]}"
|
|
done
|
|
`
|
|
|
|
// installHook installs the env-sample-sync git hook to 'dir'
|
|
func installHook(gitDirPath string) (err error) {
|
|
hooksScriptDirName := "pre-commit-hooks.d"
|
|
hooksDirName := filepath.Join(gitDirPath, "hooks")
|
|
hooksScriptDirPath := filepath.Join(hooksDirName, hooksScriptDirName)
|
|
preCommitHookScriptPath := filepath.Join(hooksDirName, "pre-commit")
|
|
|
|
_, err = os.Stat(preCommitHookScriptPath)
|
|
if !os.IsNotExist(err) {
|
|
fmt.Printf("a pre-commit hook is already installed. Overwrite? [y/n]: ")
|
|
var response string
|
|
fmt.Scanln(&response)
|
|
if response != "y" {
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
// Create a directory to place pre-commit-hook executable within
|
|
if _, err = os.Stat(hooksScriptDirPath); os.IsNotExist(err) {
|
|
if err = os.Mkdir(hooksScriptDirPath, os.ModePerm); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
preCommitHookPath := filepath.Join(hooksScriptDirPath, "0-env-sample-sync")
|
|
preCommitHook, err := os.OpenFile(preCommitHookPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
|
|
if err != nil {
|
|
fmt.Println("unable to install pre-commit hook", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
executablePath, err := exec.LookPath(os.Args[0])
|
|
if err != nil {
|
|
fmt.Println("unable to find 'env-sample-sync' in $PATH:", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
executable, err := os.Open(executablePath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// copy the currently running executable wholsale into the pre commit hooks directory
|
|
io.Copy(preCommitHook, executable)
|
|
|
|
tmpl, err := template.New("pre-commit-script").Parse(preCommitScriptTemplate)
|
|
if err != nil {
|
|
log.Println("unable to parse pre-commit script template:", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
args := fmt.Sprintf("--env-file=%s --sample-file=%s %s", envFileFlag, sampleFileFlag, examplesFlag)
|
|
var buff = bytes.NewBufferString("")
|
|
err = tmpl.Execute(buff, map[string]string{
|
|
"Args": args,
|
|
"PreCommitHooksDir": hooksScriptDirName,
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
preCommitHookScript, err := os.OpenFile(preCommitHookScriptPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = buff.WriteTo(preCommitHookScript)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
fmt.Println("env-sample-sync pre-commit hook installed!")
|
|
return
|
|
}
|