Skip to content

Commit

Permalink
Merge pull request #5 from Shackelford-Arden/auth-caching
Browse files Browse the repository at this point in the history
Adding support for caching Nomad and Consul tokens
  • Loading branch information
Shackelford-Arden authored Apr 26, 2024
2 parents 88f9d77 + 10a49fa commit 9deea73
Show file tree
Hide file tree
Showing 22 changed files with 475 additions and 98 deletions.
5 changes: 5 additions & 0 deletions .changes/unreleased/Added-20240426-165101.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Added
body: caching of Nomad and Consul tokens
time: 2024-04-26T16:51:01.905576593-05:00
custom:
Author: Shackelford-Arden
5 changes: 5 additions & 0 deletions .changes/unreleased/Changed-20240426-165238.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Changed
body: Moved config file into dedicated hctx directory
time: 2024-04-26T16:52:38.373349377-05:00
custom:
Author: Shackelford-Arden
5 changes: 5 additions & 0 deletions .changes/unreleased/Fixed-20240426-165036.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: Fixed
body: Ensuring use and unset aliases are included in the activate script
time: 2024-04-26T16:50:36.886496648-05:00
custom:
Author: Shackelford-Arden
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ source ~/.zshrc

### Define Your Configuration

`hctx` assumes the config file will be in `~/.config/.hctx.hcl`. If it doesn't exist, it will create an empty
`hctx` assumes the config file will be in `~/.config/hctx/config.hcl`. If it doesn't exist, it will create an empty
one for you when you first run it.

Here is an example configuration file:
Expand Down Expand Up @@ -101,6 +101,32 @@ _Note: hctx will only unset the environment variables that are configured in the
hctx unset
```

### Caching Tokens

_Only applies to Nomad and Consul. Vault has built-in caching._

By default, `hctx` does _not_ attempt to cache any credentials/tokens for Nomad or Consul.

To enable it, simply set the global setting in your config:

```hcl
cache_auth = true
```

With this enabled, `hctx` will store credentials when switching between stacks.

This can be helpful when/if you need to quickly switch between two or more stacks, but
don't want to bother with authenticating each time you switch.

_Note: Using `unset` will not cache anything, as it assumes you are no longer using
that stack._

Preferably, Nomad and Consul CLIs would do the caching for you. If
either implement this in the future, `hctx` will be updated to prefer
those methods over itself.

You can find cache file in `~/.config/hctx/cache.json`.

### Shell Prompts

This section contains information about how one _might_ configure the
Expand Down Expand Up @@ -134,13 +160,16 @@ format = 'hctx [$env_value]($style)'
- With something like a `-verbose` flag (w/ an alias of `-v`!), include full values of each stack
- Probably table format
- [ ] Add self-update
- [ ] Add configuration to indicate an environment is production
- [x] Add configuration to indicate an environment is production
- This is "available" by letting users use aliases. Users can update their prompts accordingly.
- Could potentially come into play w/ shell prompt updating
- [x] Add support for stack aliases
- Let daily usage use shorter names where shell prompt updating uses slightly more verbose naming
- [ ] Add `add` command
- **Due to the way HCL itself works, this is not an option while using HCL as the config file.**
- [ ] Add `edit` command
- I'd want to make sure that a user could modify a single attribute of a stack.
- **Due to the way HCL itself works, this is not an option while using HCL as the config file.**

## Maybes

Expand Down
120 changes: 120 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package cache

import (
"encoding/json"
"fmt"
"github.com/Shackelford-Arden/hctx/config"
"github.com/Shackelford-Arden/hctx/models"
"os"
)

const FilePerms = os.FileMode(0600)
const FileName = "cache.json"

func CachePath() (string, error) {

// Get user homedir
userHome, homeErr := os.UserHomeDir()
if homeErr != nil {
return "", fmt.Errorf("failed to get user homedir: %s", homeErr)
}

return fmt.Sprintf("%s/%s/%s/%s", userHome, config.ConfigParentDir, config.ConfigDir, FileName), nil
}

type Cache map[string]models.StackCache

func NewCache(cachePath string) (*Cache, error) {

if cachePath == "" {
cp, err := CachePath()
if err != nil {
return nil, fmt.Errorf("failed getting cache path: %s", err.Error())
}
cachePath = cp
}

cachePathStat, err := os.Stat(cachePath)
if os.IsNotExist(err) {
cacheCreated, createErr := os.Create(cachePath)
if createErr != nil {
return nil, fmt.Errorf("failed to create %s: %s", cachePath, createErr)
}

emptyCache := []byte("{}")
if _, err := cacheCreated.Write(emptyCache); err != nil {
return nil, fmt.Errorf("failed to write empty cache to %s: %s", cachePath, err)
}
}

// Set appropriate permissions
if cachePathStat == nil {
cachePathStat, _ = os.Stat(cachePath)
}

currentPerm := cachePathStat.Mode().Perm()
if currentPerm != FilePerms {
setPermErr := os.Chmod(cachePath, FilePerms)
if setPermErr != nil {
return nil, fmt.Errorf("failed to set permissions on %s: %s", cachePath, setPermErr)
}
}

cacheFile, _ := os.ReadFile(cachePath)
var cache *Cache

cacheParseErr := json.Unmarshal(cacheFile, &cache)
if cacheParseErr != nil {
return nil, fmt.Errorf("failed to unmarshal %s: %s", cachePath, cacheParseErr)
}

return cache, nil
}

func (c *Cache) Update(stackName string, data models.StackCache) error {
(*c)[stackName] = data
saveErr := c.Save("")
if saveErr != nil {
return fmt.Errorf("failed to update cache: %s", saveErr.Error())
}

return nil
}

func (c *Cache) Get(stackName string) *models.StackCache {
var cacheStack *models.StackCache

for name, cache := range *c {
if name == stackName {
cacheStack = &cache
break
}
}

return cacheStack
}

func (c *Cache) Save(path string) error {

cp := path

if path == "" {
cachePath, err := CachePath()
if err != nil {
return fmt.Errorf("failed getting cache path: %s", err.Error())
}
cp = cachePath
}

cacheData, err := json.MarshalIndent(*c, "", " ")
if err != nil {
return fmt.Errorf("failed marshalling cache data: %s", err.Error())
}

writeErr := os.WriteFile(cp, cacheData, FilePerms)
if writeErr != nil {
return fmt.Errorf("failed writing cache data to %s: %s", cp, writeErr)
}

return nil
}
80 changes: 80 additions & 0 deletions cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cache

import (
"os"
"testing"
)

func TestNewEmptyCache(t *testing.T) {

cachePath := "testdata/empty-cache.json"

cache, err := NewCache(cachePath)
if err != nil {
t.Fatal(err)
}

if cache == nil {
t.Fatal("cache is nil when it shouldn't be")
}
}

func TestCreateNonExistentCacheFile(t *testing.T) {

tmpCachePath := "/tmp/missing-cache.json"
defer os.Remove(tmpCachePath)

cache, err := NewCache(tmpCachePath)
if err != nil {
t.Fatal(err)

}

// Validate permissions are set correctly
cacheStat, err := os.Stat(tmpCachePath)
if err != nil {
t.Fatal(err)
}

if cacheStat.Mode().Perm() != FilePerms {
t.Fatalf("cache file %s has an invalid permissions %d", tmpCachePath, cacheStat.Mode())
}

if cache == nil {
t.Fatal("cache should not be nil")
}
}

func TestValidCache(t *testing.T) {
cachePath := "testdata/valid-cache.json"

cache, err := NewCache(cachePath)
if err != nil {
t.Fatal(err)
}

cacheItem := cache.Get("test")

if cacheItem == nil {
t.Fatal("cache item is nil when it shouldn't be")
}

if cacheItem.NomadToken != "test-token" && cacheItem.ConsulToken != "" {
t.Fatal("cache item is not valid")
}
}

func TestMissingCacheItem(t *testing.T) {
cachePath := "testdata/valid-cache.json"

cache, err := NewCache(cachePath)
if err != nil {
t.Fatal(err)
}

cacheItem := cache.Get("fake-test")

if cacheItem != nil {
t.Fatal("cached item should be nil, as fake-test should be missing.")
}
}
1 change: 1 addition & 0 deletions cache/testdata/empty-cache.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
5 changes: 5 additions & 0 deletions cache/testdata/valid-cache.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"test": {
"nomad-token": "test-token"
}
}
22 changes: 22 additions & 0 deletions cache/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cache

import (
"github.com/Shackelford-Arden/hctx/models"
"os"
)

func GetCacheableValues() models.StackCache {
var currentValues models.StackCache

nt := os.Getenv(models.NomadToken)
if nt != "" {
currentValues.NomadToken = nt
}

ct := os.Getenv(models.ConsulToken)
if nt != "" {
currentValues.ConsulToken = ct
}

return currentValues
}
2 changes: 1 addition & 1 deletion cmd/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ hctx () {
fi
shift
case "$command" in
(use|unset) if [[ ! " $@ " =~ " --help " ]] && [[ ! " $@ " =~ " -h " ]]
(use|u|unset|un) if [[ ! " $@ " =~ " --help " ]] && [[ ! " $@ " =~ " -h " ]]
then
eval "$(command $HCTX_PATH "$command" "$@")"
return $?
Expand Down
11 changes: 2 additions & 9 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,20 @@ import (
"github.com/Shackelford-Arden/hctx/types"
"os"

"github.com/Shackelford-Arden/hctx/models"
"github.com/urfave/cli/v2"
)

func List(ctx *cli.Context) error {

// Parse config
cfg, cfgErr := models.NewConfig("")
if cfgErr != nil {
return cfgErr
}

if len(cfg.Stacks) == 0 {
if len(AppConfig.Stacks) == 0 {
fmt.Fprintf(ctx.App.Writer, "No stacks!\n")
return nil
}

currStack := os.Getenv(types.StackNameEnv)

fmt.Println("Stacks:")
for _, stack := range cfg.Stacks {
for _, stack := range AppConfig.Stacks {
var indicator string
if currStack != "" && (stack.Name == currStack || stack.Alias == currStack) {
indicator = "*"
Expand Down
Loading

0 comments on commit 9deea73

Please sign in to comment.