Skip to content

Commit

Permalink
cli: convert to using babycli for argument processing
Browse files Browse the repository at this point in the history
  • Loading branch information
shoenig committed Aug 25, 2024
1 parent 435c581 commit d1d0cae
Show file tree
Hide file tree
Showing 18 changed files with 401 additions and 708 deletions.
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
NAME = envy

default: build

.PHONY: build
.PHONY: compile
build: clean
@echo "--> Build ..."
@echo "--> Compile ..."
CGO_ENABLED=0 go build -o output/$(NAME)

.PHONY: clean
Expand Down Expand Up @@ -39,3 +37,5 @@ release:
@echo "--> RELEASE ..."
envy exec gh-release goreleaser release --clean
$(MAKE) clean

default: compile
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.24.0 // indirect
noxide.lol/go/babycli v0.1.4 // indirect
noxide.lol/go/stacks v1.0.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
noxide.lol/go/babycli v0.1.4 h1:gdgpLn1ydzy7VK3ZQAIbHnnPk1yHfyd6OYLo9UsWulY=
noxide.lol/go/babycli v0.1.4/go.mod h1:WatWwUui1Zf1KtSz4EzLtOkGkljrhOFfXfLHVoBRDj4=
noxide.lol/go/stacks v1.0.0 h1:g4MPkizQF/6B3u1ejGxWDIjL1zM/MSHMzoS1DEZTOWY=
noxide.lol/go/stacks v1.0.0/go.mod h1:rwC8UA5l8uwwfRNAKuBSI+7hpP8ilJVK3gozAn7vjzM=
43 changes: 33 additions & 10 deletions internal/commands/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,55 @@
package commands

import (
"flag"
"regexp"
"strconv"
"strings"

"github.com/hashicorp/go-set/v2"
"github.com/pkg/errors"
"github.com/shoenig/envy/internal/keyring"
"github.com/shoenig/envy/internal/safe"
"github.com/shoenig/envy/internal/setup"
"github.com/shoenig/go-conceal"
"github.com/shoenig/regexplus"
"noxide.lol/go/babycli"
)

var (
argRe = regexp.MustCompile(`^(?P<key>\w+)=(?P<secret>.+)$`)
namespaceRe = regexp.MustCompile(`^[-:/\w]+$`)
)

const (
description = `
The envy is a command line tool for managing profiles of
environment variables. Values are stored securely using
encryption with keys protected by your desktop keychain.`
)

func Invoke(args []string, tool *setup.Tool) babycli.Code {
return invoke(args, tool)
}

func invoke(args []string, tool *setup.Tool) babycli.Code {
r := babycli.New(&babycli.Configuration{
Arguments: args,
Version: "v0",
Top: &babycli.Component{
Name: "envy",
Help: "wrangle environment varibles",
Description: description,
Components: babycli.Components{
newListCmd(tool),
newSetCmd(tool),
newPurgeCmd(tool),
newShowCmd(tool),
newExecCmd(tool),
},
},
})
return r.Run()
}

func checkName(namespace string) error {
if !namespaceRe.MatchString(namespace) {
return errors.New("namespace uses non-word characters")
Expand Down Expand Up @@ -105,11 +136,3 @@ func (e *extractor) encryptEnvVar(kv *conceal.Text) (string, safe.Encrypted, err
func (e *extractor) encrypt(s *conceal.Text) safe.Encrypted {
return e.ring.Encrypt(s)
}

func fsBool(fs *flag.FlagSet, name string) bool {
b, err := strconv.ParseBool(fs.Lookup(name).Value.String())
if err != nil {
return false
}
return b
}
24 changes: 0 additions & 24 deletions internal/commands/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ package commands

import (
"bytes"
"flag"
"os"
"testing"

"github.com/shoenig/envy/internal/output"
"github.com/shoenig/test/must"
"github.com/zalando/go-keyring"
)

Expand All @@ -19,27 +15,7 @@ func init() {
keyring.MockInit()
}

func newDBFile(t *testing.T) string {
f, err := os.CreateTemp("", "tool-")
must.NoError(t, err)
err = f.Close()
must.NoError(t, err)
return f.Name()
}

func cleanupFile(t *testing.T, name string) {
err := os.Remove(name)
must.NoError(t, err)
}

func newWriter() (*bytes.Buffer, *bytes.Buffer, output.Writer) {
var a, b bytes.Buffer
return &a, &b, output.New(&a, &b)
}

func setupFlagSet(t *testing.T, arguments []string) (*flag.FlagSet, interface{}) {
fs := flag.NewFlagSet("test", flag.PanicOnError)
err := fs.Parse(arguments)
must.NoError(t, err)
return fs, arguments
}
163 changes: 66 additions & 97 deletions internal/commands/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,87 +5,88 @@ package commands

import (
"context"
"flag"
"fmt"
"io"
"os"
"os/exec"
"strings"

"github.com/google/subcommands"
"github.com/shoenig/envy/internal/keyring"
"github.com/shoenig/envy/internal/output"
"github.com/shoenig/envy/internal/safe"
"github.com/shoenig/envy/internal/setup"
"noxide.lol/go/babycli"
)

const (
execCmdName = "exec"
execCmdSynopsis = "Run command with environment variables from namespace."
execCmdUsage = "exec [namespace] [command] <args, ...>"

flagInsulate = "insulate"
)

func NewExecCmd(t *setup.Tool) subcommands.Command {
return &execCmd{
writer: t.Writer,
ring: t.Ring,
box: t.Box,
execInputStd: os.Stdin,
execOutputStd: os.Stdout,
execOutputErr: os.Stderr,
func newExecCmd(tool *setup.Tool) *babycli.Component {
return &babycli.Component{
Name: "exec",
Help: "run a command using environment variables from profile",
Flags: babycli.Flags{
{
Type: babycli.BooleanFlag,
Long: "insulate",
Short: "i",
Help: "disable child process from inheriting parent environment variables",
Default: &babycli.Default{
Value: false,
Show: false,
},
},
},
Function: func(c *babycli.Component) babycli.Code {
if c.Nargs() < 2 {
tool.Writer.Errorf("must specify profile and command argument(s)")
return babycli.Failure
}

args := c.Arguments()
p, err := tool.Box.Get(args[0])
if err != nil {
tool.Writer.Errorf("unable to read profile: %v", err)
return babycli.Failure
}

insulate := c.GetBool("insulate")
argVars, command, args := splitArgs(args[1:])
cmd := newCmd(tool, p, insulate, argVars, command, args)

if err := cmd.Run(); err != nil {
tool.Writer.Errorf("failed to exec: %v", err)
return babycli.Failure
}

return babycli.Success
},
}
}

type execCmd struct {
writer output.Writer
ring keyring.Ring
box safe.Box
execInputStd io.Reader
execOutputStd io.Writer
execOutputErr io.Writer
}

func (wc execCmd) Name() string {
return execCmdName
}

func (wc execCmd) Synopsis() string {
return execCmdSynopsis
}

func (wc execCmd) Usage() string {
return execCmdUsage
}

func (wc execCmd) SetFlags(fs *flag.FlagSet) {
// no flags when running command through exec
_ = fs.Bool(flagInsulate, false, "insulate will run command without passing through environment")
}

func (wc execCmd) Execute(_ context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
insulate := fsBool(fs, flagInsulate)

if len(fs.Args()) < 2 {
wc.writer.Errorf("expected namespace and command argument(s)")
return subcommands.ExitUsageError
func env(tool *setup.Tool, ns *safe.Namespace, environment []string) []string {
for key, value := range ns.Content {
secret := tool.Ring.Decrypt(value).Unveil()
environment = append(environment, fmt.Sprintf(
"%s=%s", key, secret,
))
}
return environment
}

ns, err := wc.box.Get(fs.Arg(0))
if err != nil {
wc.writer.Errorf("could not retrieve namespace: %v", err)
return subcommands.ExitUsageError
func envContext(insulate bool) []string {
if insulate {
return nil
}
return os.Environ()
}

argVars, command, args := splitArgs(fs.Args()[1:])
cmd := wc.newCmd(ns, insulate, argVars, command, args)
if err := cmd.Run(); err != nil {
wc.writer.Errorf("failed to exec: %v", err)
return subcommands.ExitFailure
}
func newCmd(tool *setup.Tool, ns *safe.Namespace, insulate bool, argVars []string, command string, args []string) *exec.Cmd {
ctx := context.Background()
cmd := exec.CommandContext(ctx, command, args...)

return subcommands.ExitSuccess
// Environment variables are injected in the following order:
// 1. OS variables if insulate is false
// 2. envy namespace vars
// 3. Variables in input args
cmd.Env = append(env(tool, ns, envContext(insulate)), argVars...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd
}

// splitArgs will split the list of flag.Args() into:
Expand Down Expand Up @@ -123,35 +124,3 @@ func splitArgs(flagArgs []string) (argVars []string, command string, commandArgs

return argVars, command, commandArgs
}

func (wc execCmd) newCmd(ns *safe.Namespace, insulate bool, argVars []string, command string, args []string) *exec.Cmd {
ctx := context.Background()
cmd := exec.CommandContext(ctx, command, args...)

// Environment variables are injected in the following order:
// 1. OS variables if insulate is false
// 2. envy namespace vars
// 3. Variables in input args
cmd.Env = append(wc.env(ns, envContext(insulate)), argVars...)
cmd.Stdout = wc.execOutputStd
cmd.Stderr = wc.execOutputErr
cmd.Stdin = wc.execInputStd
return cmd
}

func envContext(insulate bool) []string {
if insulate {
return nil
}
return os.Environ()
}

func (wc execCmd) env(ns *safe.Namespace, environment []string) []string {
for key, value := range ns.Content {
secret := wc.ring.Decrypt(value).Unveil()
environment = append(environment, fmt.Sprintf(
"%s=%s", key, secret,
))
}
return environment
}
Loading

0 comments on commit d1d0cae

Please sign in to comment.