Skip to content

Commit

Permalink
cli: consolidate update command into set command (#23)
Browse files Browse the repository at this point in the history
Remove 'update' command and combine that functionality into the
'set' command. To remove a variable, now just use 'set' command
and prefix the variable name with a dash, e.g.

envy set ns -FOO -BAR
  • Loading branch information
shoenig authored Feb 19, 2023
1 parent 58f9a22 commit de21375
Show file tree
Hide file tree
Showing 30 changed files with 486 additions and 692 deletions.
2 changes: 0 additions & 2 deletions OSSMETADATA

This file was deleted.

69 changes: 48 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,88 @@ Use `envy` to manage sensitive environment variables when running commands.

# Project Overview

`github.com/shoenig/envy` provides a command-line utility for managing
secretive environment variables when running commands.
`github.com/shoenig/envy` provides a CLI utility for running commands with secret
environment variables like `GITHUB_TOKEN`, etc.

`envy` builds on ideas from [envchain](https://github.com/sorah/envchain) and [schain](https://github.com/evanphx/schain). It makes use of the [go-keyring](https://github.com/zalando/go-keyring) library for multi-platform keyring management. Encryption is based on Go's built-in `crypto/aes` library. Persistent storage is managed through [boltdb](https://github.com/etcd-io/bbolt).
`envy` builds on ideas from [envchain](https://github.com/sorah/envchain) and [schain](https://github.com/evanphx/schain).
It makes use of the [go-keyring](https://github.com/zalando/go-keyring) library for multi-platform keyring management.
Encryption is based on Go's built-in `crypto/aes` library.
Persistent storage is managed through [boltdb](https://github.com/etcd-io/bbolt).

Supports **Linux**, **macOS**, and **Windows**

# Getting Started

#### Install

The `envy` command is availble to download from the [Releases](https://github.com/shoenig/envy/releases) page.
The `envy` command is available to download from the [Releases](https://github.com/shoenig/envy/releases) page.

Multiple operating systems and architectures are supported, including

- Linux
- macOS
- Windows

#### Build from source
#### Install from Go module

The `envy` command can be installed from source by running

The `envy` command can be compiled by running
```bash
$ go install github.com/shoenig/envy@latest
```

# Example Usages

#### usage overview

```bash
Subcommands for envy:
exec Run command with environment variables from namespace.
list List all namespaces.
purge Purge a namespace.
set Set environment variable(s) for namespace.
exec Run a command with environment variables from namespace.
use `-insulate` to exclude external environment variables
list List available namespaces.
purge Remove a namespace and all of its environment variables.
set Set/Update/Remove environment variable(s) for namespace.
show Show environment variable(s) in namespace.
update Add or Update environment variable(s) in namespace.
use `-decrypt` to include decrypted values
```

#### set a namespace

```bash
$ envy set example FOO=1 BAR=2 BAZ=3
```

#### update existing variable in a namespace

```bash
$ envy set example FOO=4
```

#### remove variable from namespace

```bash
$ envy set example a=foo b=bar c=baz
stored 3 items in "example"
$ envy set example -FOO
```

#### execute command

```bash
$ envy exec example hack/test.sh
a: is foo, b is: bar
$ envy exec example env
BAR=2
BAZ=3
... <many more from user> ...
```

#### execute command excluding external environment

```bash
$ envy exec -insulate example env
BAR=2
BAZ=3
```

#### list namespaces

```bash
$ envy list
consul/connect-acls:no_tls
Expand All @@ -68,26 +98,23 @@ test
```

#### show namespace

```bash
$ envy show test
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
```

#### show namespace w/ values

```bash
$ envy show --decrypt test
AWS_ACCESS_KEY_ID=aaabbbccc
AWS_SECRET_ACCESS_KEY=233kjsdf309jfsd
```

#### update variable in namespace
```bash
$ envy update test AWS_ACCESS_KEY_ID=xxxxyyyyzzz
updated 1 items in "test"
```

#### remove namespace

```bash
$ envy purge test
purged namespace "test"
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
module github.com/shoenig/envy

go 1.19
go 1.20

require (
github.com/gojuno/minimock/v3 v3.0.10
github.com/google/subcommands v1.2.0
github.com/hashicorp/go-set v0.1.9
github.com/hashicorp/go-uuid v1.0.3
github.com/pkg/errors v0.9.1
github.com/shoenig/go-conceal v0.4.1
github.com/shoenig/ignore v0.4.0
github.com/shoenig/regexplus v0.3.0
github.com/shoenig/secrets v0.3.0
github.com/shoenig/test v0.6.1
github.com/zalando/go-keyring v0.2.2
go.etcd.io/bbolt v1.3.7
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6
)

require (
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/hashicorp/go-set v0.1.9 h1:XuQSsDfOAvgRjoKWG2qg8NxVEQJMXGdrZh8BgX6O8n4=
github.com/hashicorp/go-set v0.1.9/go.mod h1:/IR7VHUqnKI+QfKkaMjZ575bf65Y8DzHRKnOobRpNcQ=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hexdigest/gowrap v1.1.7/go.mod h1:Z+nBFUDLa01iaNM+/jzoOA1JJ7sm51rnYFauKFUB5fs=
Expand Down Expand Up @@ -65,12 +67,12 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/shoenig/go-conceal v0.4.1 h1:THZ4N2vWIhGvXARoZE1MafptDDrpw70q9tKeqg1RaZU=
github.com/shoenig/go-conceal v0.4.1/go.mod h1:TVpH0H5i3AQV62NhN+SWAwElHEVSq6I9uH5A6hAQJFU=
github.com/shoenig/ignore v0.4.0 h1:qPOWs0slbPMtenC0H3cKvu5Kn3hQFTE3yK6YJvyNDlA=
github.com/shoenig/ignore v0.4.0/go.mod h1:VF91FoiYAwXq4KinOq6zP5xfFw/Ib6MfftaGKYTpmwo=
github.com/shoenig/regexplus v0.3.0 h1:+eJZ5P4y1IY4+NgkIIvIqqdG/uYRBuSg4vWUjEppLcY=
github.com/shoenig/regexplus v0.3.0/go.mod h1:AT46KcYMK7iD/drPHdFtmB42TetxBe5S/O3vMGPa5cw=
github.com/shoenig/secrets v0.3.0 h1:pKYeLDTixg2+y75jRVDyX6E57JJ3mBsg7hjpG7Y59pU=
github.com/shoenig/secrets v0.3.0/go.mod h1:as+66KSjA42q+Db/SITaICU4QmL8ORaYNhRUBY6g5sc=
github.com/shoenig/test v0.6.1 h1:TVIih3yGvaH8Yci2OedB/NAhOC9UlNi5+ajCVyMPflg=
github.com/shoenig/test v0.6.1/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
Expand All @@ -91,6 +93,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6 h1:Ic9KukPQ7PegFzHckNiMTQXGgEszA7mY2Fn4ZMtnMbw=
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand Down
74 changes: 39 additions & 35 deletions internal/commands/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ package commands

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

"github.com/hashicorp/go-set"
"github.com/pkg/errors"
"github.com/shoenig/envy/internal/keyring"
"github.com/shoenig/envy/internal/safe"
"github.com/shoenig/go-conceal"
"github.com/shoenig/regexplus"
"github.com/shoenig/secrets"
)

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

Expand All @@ -25,7 +28,8 @@ func checkName(namespace string) error {
}

type Extractor interface {
Namespace(args []interface{}) (*safe.Namespace, error)
PreProcess(args []string) (string, *set.Set[string], *set.HashSet[*conceal.Text, int], error)
Namespace(vars *set.HashSet[*conceal.Text, int]) (*safe.Namespace, error)
}

type extractor struct {
Expand All @@ -38,44 +42,44 @@ func newExtractor(ring keyring.Ring) Extractor {
}
}

func (e *extractor) Namespace(args []interface{}) (*safe.Namespace, error) {
_, namespace, argv, err := extract(args)
if err != nil {
return nil, err
// PreProcess returns
// - the namespace
// - the set of keys to be removed
// - the set of key/values to be added
// - any error
func (e *extractor) PreProcess(args []string) (string, *set.Set[string], *set.HashSet[*conceal.Text, int], error) {
if len(args) < 2 {
return "", nil, nil, fmt.Errorf("requires at least 2 arguments (namespace, <key,...>)")
}
ns := args[0]
rm := set.New[string](4)
add := set.NewHashSet[*conceal.Text, int](8)
for i := 1; i < len(args); i++ {
s := args[i]
switch {
case strings.HasPrefix(s, "-"):
rm.Insert(strings.TrimPrefix(s, "-"))
case strings.Contains(s, "="):
add.Insert(conceal.New(s))
default:
return "", nil, nil, fmt.Errorf("argument must start with '-' or contain '='")
}
}
return ns, rm, add, nil
}

content, err := e.process(argv)
func (e *extractor) Namespace(vars *set.HashSet[*conceal.Text, int]) (*safe.Namespace, error) {
content, err := e.process(vars.Slice())
if err != nil {
return nil, err
}

return &safe.Namespace{
Name: namespace,
Name: "",
Content: content,
}, nil
}

func extract(args []interface{}) (string, string, []secrets.Text, error) {
arguments := make([]secrets.Text, 0)
for _, arg := range args[0].([]string) {
arguments = append(arguments, secrets.New(arg))
}

if len(arguments) < 2 {
return "", "", nil, errors.New("not enough arguments")
}

command := arguments[0].Secret()
namespace := arguments[1].Secret()

if err := checkName(namespace); err != nil {
return "", "", nil, err
}

return command, namespace, arguments[2:], nil
}

func (e *extractor) process(args []secrets.Text) (map[string]safe.Encrypted, error) {
func (e *extractor) process(args []*conceal.Text) (map[string]safe.Encrypted, error) {
content := make(map[string]safe.Encrypted, len(args))
for _, kv := range args {
if key, secret, err := e.encryptEnvVar(kv); err != nil {
Expand All @@ -87,16 +91,16 @@ func (e *extractor) process(args []secrets.Text) (map[string]safe.Encrypted, err
return content, nil
}

func (e *extractor) encryptEnvVar(kv secrets.Text) (string, safe.Encrypted, error) {
m := regexplus.FindNamedSubmatches(argRe, kv.Secret())
func (e *extractor) encryptEnvVar(kv *conceal.Text) (string, safe.Encrypted, error) {
m := regexplus.FindNamedSubmatches(argRe, kv.Unveil())
if len(m) == 2 {
s := e.encrypt(secrets.New(m["secret"]))
s := e.encrypt(conceal.New(m["secret"]))
return m["key"], s, nil
}
return "", nil, errors.New("malformed environment variable pair")
}

func (e *extractor) encrypt(s secrets.Text) safe.Encrypted {
func (e *extractor) encrypt(s *conceal.Text) safe.Encrypted {
return e.ring.Encrypt(s)
}

Expand Down
39 changes: 1 addition & 38 deletions internal/commands/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package commands
import (
"bytes"
"flag"
"io/ioutil"
"os"
"testing"

Expand All @@ -18,7 +17,7 @@ func init() {
}

func newDBFile(t *testing.T) string {
f, err := ioutil.TempFile("", "tool-")
f, err := os.CreateTemp("", "tool-")
must.NoError(t, err)
err = f.Close()
must.NoError(t, err)
Expand All @@ -41,39 +40,3 @@ func setupFlagSet(t *testing.T, arguments []string) (*flag.FlagSet, interface{})
must.NoError(t, err)
return fs, arguments
}

func TestCommon_args(t *testing.T) {

// google/subcommands passes args wrapped like this
wrap := func(a []string) []interface{} {
return []interface{}{a}
}

t.Run("no arguments", func(t *testing.T) {
_, _, _, err := extract(wrap([]string{}))
must.EqError(t, err, "not enough arguments")
})

t.Run("one argument", func(t *testing.T) {
_, _, _, err := extract(wrap([]string{"foo"}))
must.EqError(t, err, "not enough arguments")
})

t.Run("two arguments", func(t *testing.T) {
verb, ns, argv, err := extract(wrap([]string{"foo", "bar"}))
must.Eq(t, "foo", verb)
must.Eq(t, "bar", ns)
must.SliceEmpty(t, argv)
must.NoError(t, err)
})

t.Run("four arguments", func(t *testing.T) {
verb, ns, secrets, err := extract(wrap([]string{"a", "b", "c", "d"}))
must.Eq(t, "a", verb)
must.Eq(t, "b", ns)
must.Eq(t, 2, len(secrets))
must.Eq(t, "c", secrets[0].Secret())
must.Eq(t, "d", secrets[1].Secret())
must.NoError(t, err)
})
}
2 changes: 1 addition & 1 deletion internal/commands/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func envContext(insulate bool) []string {

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

0 comments on commit de21375

Please sign in to comment.