Skip to content

Commit

Permalink
refactor and fix issues #3, #4, #8
Browse files Browse the repository at this point in the history
  • Loading branch information
teejaded authored and hbollon committed Jun 23, 2023
1 parent 498394b commit 82b9cdb
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 214 deletions.
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/camptocamp/helm-sops

go 1.20

require go.mozilla.org/sops/v3 v3.7.3
require (
go.mozilla.org/sops/v3 v3.7.3
gopkg.in/yaml.v3 v3.0.1
)

require (
cloud.google.com/go/compute v1.5.0 // indirect
Expand Down Expand Up @@ -82,7 +85,6 @@ require (
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/urfave/cli.v1 v1.20.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace go.mozilla.org/sops/v3 v3.7.3 => github.com/camptocamp/sops/v3 v3.7.4-0.20230517081230-891507a64d12
203 changes: 203 additions & 0 deletions helm_wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package main

import (
"crypto/sha256"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"regexp"
"sync"
"syscall"

"go.mozilla.org/sops/v3/decrypt"
)

type HelmWrapper struct {
Errors []error
errMutex sync.Mutex

ExitCode int

helmBinPath string
pipeWriterWaitGroup sync.WaitGroup
valuesArgRegexp *regexp.Regexp
temporaryDirectory string
}

func NewHelmWrapper() (*HelmWrapper, error) {
c := HelmWrapper{}

c.Errors = []error{}
c.pipeWriterWaitGroup = sync.WaitGroup{}
c.valuesArgRegexp = regexp.MustCompile("^(-f|--values)(?:=(.+))?$")

// Determine the name of the helm binary by examining our binary name
helmBinName := "helm"
ourBinName := path.Base(os.Args[0])
if ourBinName == "helm" || ourBinName == "helm2" || ourBinName == "helm3" {
helmBinName = fmt.Sprintf("_%s", ourBinName)
}

var err error
c.helmBinPath, err = exec.LookPath(helmBinName)
if err != nil {
return nil, fmt.Errorf("failed to find Helm binary '%s': %s", helmBinName, err)
}

return &c, nil
}

func (c *HelmWrapper) errorf(msg string, a ...interface{}) error {
e := fmt.Errorf(msg, a...)
c.errMutex.Lock()
c.Errors = append(c.Errors, e)
c.errMutex.Unlock()
return e
}

func (c *HelmWrapper) pipeWriter(outPipeName string, data []byte) {
c.pipeWriterWaitGroup.Add(1)
defer c.pipeWriterWaitGroup.Done()

cleartextSecretFile, err := os.OpenFile(outPipeName, os.O_WRONLY, 0)
if err != nil {
c.errorf("failed to open cleartext secret pipe '%s' in pipe writer: %s", outPipeName, err)
return
}
defer func() {
err := cleartextSecretFile.Close()
if err != nil {
c.errorf("failed to close cleartext secret pipe '%s' in pipe writer: %s", outPipeName, err)
}
}()

_, err = cleartextSecretFile.Write(data)
if err != nil {
c.errorf("failed to write cleartext secret to pipe '%s': %s", outPipeName, err)
}
}

func (c *HelmWrapper) valuesArg(args []string) (string, string, error) {
valuesArgRegexpMatches := c.valuesArgRegexp.FindStringSubmatch(args[0])
if valuesArgRegexpMatches == nil {
return "", "", nil
}

var filename string
if len(valuesArgRegexpMatches[2]) > 0 {
// current arg is in the format --values=filename
filename = valuesArgRegexpMatches[2]
} else if len(args) > 1 {
// arg is in the format "-f filename"
filename = args[1]
} else {
return "", "", c.errorf("missing filename after -f or --values")
}

cleartextSecretFilename := fmt.Sprintf("%s/%x", c.temporaryDirectory, sha256.Sum256([]byte(filename)))

return filename, cleartextSecretFilename, nil
}

func (c *HelmWrapper) replaceValueFileArg(args []string, cleartextSecretFilename string) {
valuesArgRegexpMatches := c.valuesArgRegexp.FindStringSubmatch(args[0])

// replace the filename with our pipe
if len(valuesArgRegexpMatches[2]) > 0 {
args[0] = fmt.Sprintf("%s=%s", valuesArgRegexpMatches[1], cleartextSecretFilename)
} else {
args[1] = cleartextSecretFilename
}
}

func (c *HelmWrapper) mkTmpDir() (func(), error) {
var err error
c.temporaryDirectory, err = ioutil.TempDir("", fmt.Sprintf("%s.", path.Base(os.Args[0])))
if err != nil {
return nil, c.errorf("failed to create temporary directory: %s", err)
}
return func() {
err := os.RemoveAll(c.temporaryDirectory)
if err != nil {
c.errorf("failed to remove temporary directory '%s': %s", c.temporaryDirectory, err)
}
}, nil
}

func (c *HelmWrapper) mkPipe(cleartextSecretFilename string) (func(), error) {
err := syscall.Mkfifo(cleartextSecretFilename, 0600)
if err != nil {
return nil, c.errorf("failed to create cleartext secret pipe '%s': %s", cleartextSecretFilename, err)
}
return func() {
err := os.Remove(cleartextSecretFilename)
if err != nil {
c.errorf("failed to remove cleartext secret pipe '%s': %s", cleartextSecretFilename, err)
}
}, nil
}

func (c *HelmWrapper) RunHelm() {
var err error
// Setup temporary directory and defer cleanup
cleanFn, err := c.mkTmpDir()
if err != nil {
return
}
defer cleanFn()

// Loop through arguments looking for --values or -f.
// If we find a values argument, check if file has a sops section indicating it is encrypted.
// Setup a named pipe and write the decrypted data into that for helm.
for i := range os.Args {
args := os.Args[i:]

filename, cleartextSecretFilename, err := c.valuesArg(args)
if err != nil {
return
}
if filename == "" {
continue
}

encrypted, err := DetectSopsYaml(filename)
if err != nil {
c.errorf("error checking if file is encrypted: %s", err)
return
}
if !encrypted {
continue
}

c.replaceValueFileArg(args, cleartextSecretFilename)

cleartextSecrets, err := decrypt.File(filename, "yaml")
if err != nil {
c.errorf("failed to decrypt secret file '%s': %s", filename, err)
return
}

cleanFn, err := c.mkPipe(cleartextSecretFilename)
if err != nil {
return
}
defer cleanFn()

go c.pipeWriter(cleartextSecretFilename, cleartextSecrets)
}
defer c.pipeWriterWaitGroup.Wait()

cmd := exec.Command(c.helmBinPath, os.Args[1:]...)
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Run()
if err != nil {
c.ExitCode = cmd.ProcessState.ExitCode()
c.errorf("failed to run Helm: %s", err)
}
}
111 changes: 111 additions & 0 deletions helm_wrapper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package main

import (
"os"
"testing"
)

var g_hw *HelmWrapper

func init() {
g_hw, _ = NewHelmWrapper()
}

func TestNewHelmWrapper(t *testing.T) {
// TODO
}

func TestErrorf(t *testing.T) {
g_hw.Errors = []error{}
err := g_hw.errorf("test %s %d %t", "a", 1, true)
if g_hw.Errors[0] != err {
t.Errorf("errorf(test %%s %%d %%t, a, 1, true) = %s; want %s", g_hw.Errors[0], err)
}
}

func TestPipeWriter(t *testing.T) {
// TODO
}

func TestValuesArg(t *testing.T) {
res, _, err := g_hw.valuesArg([]string{"-f", "cat.yaml"})
if res != "cat.yaml" || err != nil {
t.Errorf("valuesArg([]string{\"-f\", \"cat.yaml\"}) = %s, %s; want cat.yaml, <nil>", res, "cat.yaml")
}

res, _, err = g_hw.valuesArg([]string{"--values", "cat.yaml"})
if res != "cat.yaml" || err != nil {
t.Errorf("valuesArg([]string{\"--valuse\", \"cat.yaml\"}) = %s, %s; want cat.yaml, <nil>", res, "cat.yaml")
}

res, _, err = g_hw.valuesArg([]string{"--values=cat.yaml"})
if res != "cat.yaml" || err != nil {
t.Errorf("valuesArg([]string{\"--values=cat.yaml\"}) = %s, %s; want cat.yaml, <nil>", res, "cat.yaml")
}
}

func TestReplaceValueFileArg(t *testing.T) {
args := []string{"-f", "cat.yaml"}
g_hw.replaceValueFileArg(args, "dog.yaml")
if args[1] != "dog.yaml" {
t.Errorf("args[1] = %s; want dog.yaml", args[1])
}

args = []string{"--values", "cat.yaml"}
g_hw.replaceValueFileArg(args, "dog.yaml")
if args[1] != "dog.yaml" {
t.Errorf("args[1] = %s; want dog.yaml", args[1])
}

args = []string{"--values=cat.yaml"}
g_hw.replaceValueFileArg(args, "dog.yaml")
if args[0] != "--values=dog.yaml" {
t.Errorf("args[1] = %s; want --values=dog.yaml", args[1])
}
}

func TestMkTmpDir(t *testing.T) {
// ensure no errors
cleanFn, err := g_hw.mkTmpDir()
if err != nil {
t.Errorf("mkTmpDir error: %s", err)
}

// dir exists
if _, err = os.Stat(g_hw.temporaryDirectory); err != nil {
t.Errorf("mkTmpDir stat error: %s", err)
}

// ensure dir is deleted
cleanFn()
if _, err = os.Stat(g_hw.temporaryDirectory); err == nil {
t.Errorf("mkTmpDir cleanup func did not work")
} else if !os.IsNotExist(err) {
t.Errorf("mkTmpDir cleanup something went wrong: %s", err)
}
}

func TestMkPipe(t *testing.T) {
// ensure no errors
cleanFn, err := g_hw.mkPipe("cat.yaml")
if err != nil {
t.Errorf("mkPipe error: %s", err)
}

// file exists
if _, err = os.Stat("cat.yaml"); err != nil {
t.Errorf("mkPipe stat error: %s", err)
}

// ensure file is deleted
cleanFn()
if _, err = os.Stat("cat.yaml"); err == nil {
t.Errorf("mkPipe cleanup func did not work")
} else if !os.IsNotExist(err) {
t.Errorf("mkPipe cleanup something went wrong: %s", err)
}
}

func TestRunHelm(t *testing.T) {
// TODO
}
Loading

0 comments on commit 82b9cdb

Please sign in to comment.