Repository: nats-io/nsc
Pull request #552 (2023-02-25 12:23:37)

[FEAT] added single flag to set all the data/keys/config to one containing dir
added `--all-dirs` option to store config, data, keys under the same directory - this is useful for generating test configurations


Patch:
diff --git a/cmd/addoperator.go b/cmd/addoperator.go
index 34fffa8..82b073f 100644
--- a/cmd/addoperator.go
+++ b/cmd/addoperator.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2021 The NATS Authors
+ * Copyright 2018-2023 The NATS Authors
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -39,6 +39,9 @@ func createAddOperatorCmd() *cobra.Command {
 			if err := RunStoreLessAction(cmd, args, &params); err != nil {
 				return err
 			}
+			if NscCwdOnly {
+				return nil
+			}
 			return GetConfig().SetOperator(params.name)
 		},
 	}
diff --git a/cmd/defaults.go b/cmd/defaults.go
index ccfcae9..a937de7 100644
--- a/cmd/defaults.go
+++ b/cmd/defaults.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2022 The NATS Authors
+ * Copyright 2018-2023 The NATS Authors
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -23,11 +23,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/nats-io/nsc/v2/cmd/store"
-	"github.com/nats-io/nsc/v2/home"
-
 	"github.com/nats-io/jwt/v2"
 	"github.com/nats-io/nats.go"
+	"github.com/nats-io/nsc/v2/cmd/store"
+	"github.com/nats-io/nsc/v2/home"
 )
 
 // NscHomeEnv the folder for the config file
@@ -203,8 +202,8 @@ func LoadOrInit(configDir string, dataDir string, keystoreDir string) (*ToolConf
 		return nil, err
 	}
 
-	// is the struct modified from the file
-	if (ToolConfig{}) == config {
+	if !NscCwdOnly && (ToolConfig{}) == config {
+		// is the struct modified from the file - so this is a first run here
 		config.GithubUpdates = github
 
 		config.LastUpdate = time.Now().UTC().Unix() // this is not "true" but avoids the initial check
diff --git a/cmd/exportkeys.go b/cmd/exportkeys.go
index 98daae6..480c3ef 100644
--- a/cmd/exportkeys.go
+++ b/cmd/exportkeys.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2022 The NATS Authors
+ * Copyright 2018-2023 The NATS Authors
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -21,9 +21,8 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/spf13/cobra"
-
 	"github.com/nats-io/nsc/v2/cmd/store"
+	"github.com/spf13/cobra"
 )
 
 func createExportKeysCmd() *cobra.Command {
@@ -47,6 +46,10 @@ accounts and users. These keys may be referenced in a different  operator contex
 The --filter flag allows you to specify a few letters in a public key and export only 
 those keys that matching the filter (provided the key type matches --operator, --account,
 --user (or --all).
+
+The --include-jwts flag will export the JWT configurations named by their identity key.
+This allows you to export an entire configuration tree of operators, accounts, users
+and their keys to a directory. To recreate an environment from this export use 'nsc fix'.
 `,
 		Example: `nsc export keys --dir <path> (exports the current operator, account and users keys)
 nsc export keys --operator --accounts --users (exports current operators, all accounts, and users)
@@ -77,6 +80,7 @@ nsc export keys --account <name> (changes the account context to the specified a
 	cmd.Flags().StringVarP(&params.Dir, "dir", "d", "", "directory to export keys to")
 	cmd.Flags().BoolVarP(&params.Force, "force", "F", false, "overwrite existing files")
 	cmd.Flags().BoolVarP(&params.Remove, "remove", "R", false, "removes the original key file from the keyring after exporting it")
+	cmd.Flags().BoolVarP(&params.IncludeJwts, "include-jwts", "", false, "include jwts")
 	cmd.MarkFlagRequired("dir")
 
 	return cmd
@@ -87,9 +91,10 @@ func init() {
 }
 
 type ExportKeysParams struct {
-	Force  bool
-	Remove bool
-	Dir    string
+	Force       bool
+	Remove      bool
+	Dir         string
+	IncludeJwts bool
 	KeyCollectorParams
 }
 
@@ -154,6 +159,25 @@ func (p *ExportKeysParams) Run(ctx ActionCtx) (store.Status, error) {
 		wj = append(wj, j)
 	}
 
+	if p.IncludeJwts {
+		for _, k := range keys.KeyList {
+			if k.Jwt == nil {
+				continue
+			}
+			var j ExportJob
+			j.description = fmt.Sprintf("%s.jwt (%s)", k.Pub, k.Name)
+			j.filepath = filepath.Join(p.Dir, fmt.Sprintf("%s.jwt", k.Pub))
+			_, err = os.Stat(j.filepath)
+			if os.IsNotExist(err) || (err == nil && p.Force) {
+				j.data = k.Jwt
+			} else {
+				sr.AddError("%#q already exists - specify --force to overwrite", j.filepath)
+				continue
+			}
+			wj = append(wj, j)
+		}
+	}
+
 	if keys.Len() == 0 {
 		return nil, errors.New("no keys found to export")
 	}
@@ -164,7 +188,7 @@ func (p *ExportKeysParams) Run(ctx ActionCtx) (store.Status, error) {
 
 	for _, j := range wj {
 		if j.filepath != "" {
-			j.err = os.WriteFile(j.filepath, j.data, 0700)
+			j.err = os.WriteFile(j.filepath, j.data, 0600)
 			if j.err != nil {
 				sr.AddError("error exporting %q: %v", j.description, j.err)
 			} else {
@@ -184,7 +208,7 @@ func (p *ExportKeysParams) Run(ctx ActionCtx) (store.Status, error) {
 			sr.AddWarning("skipped %q - no seed available", j.description)
 		}
 	}
-	return sr, err
+	return sr, nil
 }
 
 type ExportJob struct {
diff --git a/cmd/keycollector.go b/cmd/keycollector.go
index 05e08f8..bb46b34 100644
--- a/cmd/keycollector.go
+++ b/cmd/keycollector.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2022 The NATS Authors
+ * Copyright 2018-2023 The NATS Authors
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -78,6 +78,10 @@ func (p *KeyCollectorParams) handleOperator(ctx ActionCtx) (KeyList, error) {
 	oki.ExpectedKind = nkeys.PrefixByteOperator
 	oki.Pub = oc.Subject
 	oki.Resolve(sctx.KeyStore)
+	oki.Jwt, err = sctx.Store.ReadRawOperatorClaim()
+	if err != nil {
+		return nil, err
+	}
 	keys = append(keys, &oki)
 
 	for _, k := range oc.SigningKeys {
@@ -108,6 +112,10 @@ func (p *KeyCollectorParams) handleAccount(ctx ActionCtx, parent string, name st
 	aki.Name = ac.Name
 	aki.ExpectedKind = nkeys.PrefixByteAccount
 	aki.Pub = ac.Subject
+	aki.Jwt, err = s.ReadRawAccountClaim(name)
+	if err != nil {
+		return nil, err
+	}
 	aki.Resolve(ks)
 	keys = append(keys, &aki)
 
@@ -167,6 +175,10 @@ func (p *KeyCollectorParams) handleUser(ctx ActionCtx, account string, name stri
 	uki.Name = uc.Name
 	uki.Pub = uc.Subject
 	uki.ExpectedKind = nkeys.PrefixByteUser
+	uki.Jwt, err = s.ReadRawUserClaim(account, name)
+	if err != nil {
+		return nil, err
+	}
 	uki.Resolve(ks)
 	return &uki, nil
 }
@@ -290,6 +302,7 @@ type Key struct {
 	Curve        bool
 	KeyPath      string
 	Invalid      bool
+	Jwt          []byte
 }
 
 func (k *Key) Resolve(ks store.KeyStore) {
diff --git a/cmd/list.go b/cmd/list.go
index 7c10252..c46def1 100644
--- a/cmd/list.go
+++ b/cmd/list.go
@@ -58,7 +58,8 @@ func createListOperatorsCmd() *cobra.Command {
 			}
 			operators := config.ListOperators()
 			if len(operators) == 0 {
-				fmt.Println("no operators defined - init an environment")
+				fmt.Printf("no operators defined in store dir %q\n"+
+					"init an environment or change the store root with `env --store <dir>`", config.StoreRoot)
 			} else {
 				sort.Strings(operators)
 				var infos []*EntryInfo
diff --git a/cmd/root.go b/cmd/root.go
index b9bad3c..0937c2a 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -30,12 +30,15 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var ConfigDirFlag string
-var KeysDirFlag string
-var DataDirFlag string
+var (
+	ConfigDirFlag   string
+	KeysDirFlag     string
+	DataDirFlag     string
+	AllDirFlag      string
+	KeyPathFlag     string
+	InteractiveFlag bool
+)
 
-var KeyPathFlag string
-var InteractiveFlag bool
 var NscCwdOnly bool
 var ErrNoOperator = errors.New("set an operator -- 'nsc env -o operatorName'")
 
@@ -155,6 +158,12 @@ var rootCmd = &cobra.Command{
 	Short: "nsc creates NATS operators, accounts, users, and manage their permissions.",
 	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 		var err error
+		if AllDirFlag != "" {
+			ConfigDirFlag = AllDirFlag
+			DataDirFlag = AllDirFlag
+			KeysDirFlag = AllDirFlag
+		}
+
 		// if the flag is set we use it, if not check the old env
 		if ConfigDirFlag != "" {
 			if ConfigDirFlag, err = Expand(ConfigDirFlag); err != nil {
@@ -256,6 +265,6 @@ func HoistRootFlags(cmd *cobra.Command) *cobra.Command {
 	cmd.PersistentFlags().StringVarP(&ConfigDirFlag, "config-dir", "", "", "nsc config directory")
 	cmd.PersistentFlags().StringVarP(&DataDirFlag, "data-dir", "", "", "nsc data store directory")
 	cmd.PersistentFlags().StringVarP(&KeysDirFlag, "keystore-dir", "", "", "nsc keystore directory")
-
+	cmd.PersistentFlags().StringVarP(&AllDirFlag, "all-dirs", "H", "", "sets --config-dir, --data-dir, and --keystore-dir to the same value")
 	return cmd
 }


Test patch:
diff --git a/cmd/adduser_test.go b/cmd/adduser_test.go
index 903dbd2..ae54f6f 100644
--- a/cmd/adduser_test.go
+++ b/cmd/adduser_test.go
@@ -16,14 +16,13 @@
 package cmd
 
 import (
-	"github.com/stretchr/testify/assert"
 	"os"
 	"path/filepath"
 	"testing"
 	"time"
 
 	"github.com/nats-io/nkeys"
-
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
diff --git a/cmd/env_test.go b/cmd/env_test.go
index 5079158..d709f25 100644
--- a/cmd/env_test.go
+++ b/cmd/env_test.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2022 The NATS Authors
+ * Copyright 2018-2023 The NATS Authors
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -17,6 +17,7 @@ package cmd
 
 import (
 	"fmt"
+	"os"
 	"path/filepath"
 	"testing"
 
@@ -71,3 +72,15 @@ func TestEnv_FailsBadAccount(t *testing.T) {
 	require.Error(t, err)
 	require.Contains(t, err.Error(), "\"A\" not in accounts for operator \"O\"")
 }
+
+func TestAllDir(t *testing.T) {
+	p := MakeTempDir(t)
+	defer os.RemoveAll(p)
+
+	_, stderr, err := ExecuteCmd(rootCmd, "env", "--all-dirs", p)
+	require.NoError(t, err)
+	stderr = StripTableDecorations(stderr)
+	require.Contains(t, stderr, fmt.Sprintf("$NKEYS_PATH Yes %s", p))
+	require.Contains(t, stderr, fmt.Sprintf("$NSC_HOME Yes %s", p))
+	require.Contains(t, stderr, fmt.Sprintf("Current Store Dir %s", p))
+}
diff --git a/cmd/exportkeys_test.go b/cmd/exportkeys_test.go
index 2ac6d38..bfc0ba3 100644
--- a/cmd/exportkeys_test.go
+++ b/cmd/exportkeys_test.go
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2022 The NATS Authors
+ * Copyright 2018-2023 The NATS Authors
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -236,3 +236,25 @@ func Test_ExportXKeyInContext(t *testing.T) {
 	require.NoError(t, err)
 	requireExportedKey(t, exportDir, xPK)
 }
+
+func Test_ExportKeyJwt(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	ts.AddUser(t, "A", "U")
+
+	o := ts.GetOperatorPublicKey(t)
+	a := ts.GetAccountPublicKey(t, "A")
+	u := ts.GetUserPublicKey(t, "A", "U")
+
+	exportDir := filepath.Join(ts.Dir, "export")
+	_, _, err := ExecuteCmd(createExportKeysCmd(), "--dir", exportDir, "-A", "--include-jwts")
+	require.NoError(t, err)
+	requireExportedKey(t, exportDir, o)
+	require.FileExists(t, filepath.Join(exportDir, fmt.Sprintf("%s.jwt", o)))
+	requireExportedKey(t, exportDir, a)
+	require.FileExists(t, filepath.Join(exportDir, fmt.Sprintf("%s.jwt", a)))
+	requireExportedKey(t, exportDir, u)
+	require.FileExists(t, filepath.Join(exportDir, fmt.Sprintf("%s.jwt", u)))
+}
diff --git a/cmd/util_test.go b/cmd/util_test.go
index 8603528..74e692b 100644
--- a/cmd/util_test.go
+++ b/cmd/util_test.go
@@ -70,6 +70,10 @@ func ResetSharedFlags() {
 	Json = false
 	Raw = false
 	JsonPath = ""
+	AllDirFlag = ""
+	ConfigDirFlag = ""
+	DataDirFlag = ""
+	KeysDirFlag = ""
 }
 
 func NewEmptyStore(t *testing.T) *TestStore {
diff --git a/cmd/validate_test.go b/cmd/validate_test.go
index f81024a..4ae1c4f 100644
--- a/cmd/validate_test.go
+++ b/cmd/validate_test.go
@@ -18,6 +18,7 @@ package cmd
 import (
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/nats-io/nsc/v2/cmd/store"
@@ -30,10 +31,10 @@ func Test_ValidateNoOperator(t *testing.T) {
 	storeDir := filepath.Join(ts.Dir, "store")
 	require.NoError(t, os.Mkdir(storeDir, 0777))
 	require.DirExists(t, storeDir)
-
 	_, _, err := ExecuteCmd(createValidateCommand())
 	require.Error(t, err)
-	require.Contains(t, err.Error(), "set an operator")
+	require.True(t, strings.Contains(err.Error(), "set an operator") ||
+		strings.Contains(err.Error(), "no such file or directory"))
 }
 
 func Test_ValidateNoAccount(t *testing.T) {