Repository: nats-io/nsc (go)

Issue:
The only way to enable jetstream for an account is by setting one (or more) limit(s) of the jetstream settings for that account
### What version were you using?

Using `nats-server` `2.10.7` and version `2.8.5` of `nsc`

### What environment was the server running in?

Mac Apple M1 Pro

### Is this defect reproducible?

```
# Create operator
nsc add operator --name local
# Add account
nsc add account APP
# Describe account
nsc describe account APP
```
This shows:

> +--------------------------------------------------------------------------------------+
|                                   Account Details                                    |
+---------------------------+----------------------------------------------------------+
| Name                      | APP                                                      |
| Account ID                | AA2XZ3FGUMJ6KA7H33336PIQQKR42JCPEFWVVARFR5VFDZHMGCL5GG52 |
| Issuer ID                 | OCQZZICCSFQ7X7PJ3H3BPBOOFLHI5VBYCOUCLCIDEQZGKOOLFKRRWRUU |
| Issued                    | 2023-12-19 02:00:08 UTC                                  |
| Expires                   |                                                          |
+---------------------------+----------------------------------------------------------+
| Signing Keys              | AANHPBG53PJY6GNPMMW7TXEXMQQQYR5W5XHCDWTVW7GI4LQEPWF4J4E6 |
+---------------------------+----------------------------------------------------------+
| Max Connections           | Unlimited                                                |
| Max Leaf Node Connections | Unlimited                                                |
| Max Data                  | Unlimited                                                |
| Max Exports               | Unlimited                                                |
| Max Imports               | Unlimited                                                |
| Max Msg Payload           | Unlimited                                                |
| Max Subscriptions         | Unlimited                                                |
| Exports Allows Wildcards  | True                                                     |
| Disallow Bearer Token     | False                                                    |
| Response Permissions      | Not Set                                                  |
+---------------------------+----------------------------------------------------------+
| Jetstream                 | Disabled                                                 |
+---------------------------+----------------------------------------------------------+
| Imports                   | None                                                     |
| Exports                   | None                                                     |
+---------------------------+----------------------------------------------------------+

With the main note that `Jetstream` is `Disabled`
```
# Set an arbitrary limit for jetstream
nsc edit account APP --js-mem-storage 10g
```

We now see `Jetstream` is enabled:

> | Jetstream                 | Enabled                                                  |
| Max Disk Storage          | Disabled                                                 |
| Max Mem Storage           | 10 GB                                                    |
| Max Streams               | Unlimited                                                |
| Max Consumer              | Unlimited                                                |
| Max Ack Pending           | Consumer Setting                                         |
| Max Ack Pending           | Unlimited                                                |
| Max Bytes                 | optional (Stream setting)                                |
| Max Memory Stream         | Unlimited                                                |
| Max Disk Stream           | Unlimited                                                |

```
# Remove that limit
nsc edit account APP --js-mem-storage -1
```

And now `Jetstream` is still enabled:

> | Jetstream                 | Enabled                                                  |
| Max Disk Storage          | Disabled                                                 |
| Max Mem Storage           | Unlimited                                                |
| Max Streams               | Unlimited                                                |
| Max Consumer              | Unlimited                                                |
| Max Ack Pending           | Consumer Setting                                         |
| Max Ack Pending           | Unlimited                                                |
| Max Bytes                 | optional (Stream setting)                                |
| Max Memory Stream         | Unlimited                                                |
| Max Disk Stream           | Unlimited                                                |

### Given the capability you are leveraging, describe your expectation?

My expectations is that there is an `Enable Jetstream` option for an account that doesn't require setting a `Jetstream` limit.

There is a `--js-disable` option, would be great to have a `--js-enable` option that enables `Jetstream` for an account without requiring any limit manipulation

### Given the expectation, what is the defect you are observing?

A `--js-enable` option (or similar) appears to be missing.
The static auth option allows for a `jetstream: enabled` option for comparison

Pull request description:
[FEAT] enable jetstream flag
Simple flag to enable jetstream  tier - sets the Disk/Mem storage to -1

Fix #631 

Patch:
diff --git a/cmd/editaccount.go b/cmd/editaccount.go
index 81d659a..08ecf7a 100644
--- a/cmd/editaccount.go
+++ b/cmd/editaccount.go
@@ -118,6 +118,7 @@ func createEditAccount() *cobra.Command {
 
 	cmd.Flags().StringVarP(&params.AccountContextParams.Name, "name", "n", "", "account to edit")
 	cmd.Flags().BoolVarP(&params.disableJetStream, "js-disable", "", false, "disables all JetStream limits in the account by deleting any limits")
+	cmd.Flags().IntVarP(&params.enableJetStream, "js-enable", "", -1, "enables JetStream for the specified tier")
 
 	params.signingKeys.BindFlags("sk", "", nkeys.PrefixByteAccount, cmd)
 	params.TimeParams.BindFlags(cmd)
@@ -168,6 +169,7 @@ type EditAccountParams struct {
 	signingKeys          SigningKeysParams
 	rmSigningKeys        []string
 	disableJetStream     bool
+	enableJetStream      int
 	traceContextSubject  string
 	traceContextSampling NumberParams
 }
@@ -180,14 +182,17 @@ func (p *EditAccountParams) SetDefaults(ctx ActionCtx) error {
 	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
 
 	hasDeleteTier := ctx.AnySet("rm-js-tier")
-	p.hasJSSetParams = ctx.AnySet("js-tier", "js-mem-storage",
+	p.hasJSSetParams = ctx.AnySet("js-tier",
+		"js-mem-storage",
 		"js-disk-storage",
 		"js-streams",
 		"js-consumer",
 		"js-max-mem-stream",
 		"js-max-disk-stream",
 		"js-max-bytes-required",
-		"js-max-ack-pending")
+		"js-max-ack-pending",
+		"js-enable",
+	)
 
 	if hasDeleteTier && p.hasJSSetParams {
 		return fmt.Errorf("rm-js-tier is exclusive of all other js options")
@@ -197,6 +202,27 @@ func (p *EditAccountParams) SetDefaults(ctx ActionCtx) error {
 		return fmt.Errorf("js-disable is exclusive of all other js options")
 	}
 
+	hasEnableTier := ctx.AnySet("js-enable")
+	p.hasJSSetParams = ctx.AnySet("js-tier",
+		"js-mem-storage",
+		"js-disk-storage",
+		"js-streams",
+		"js-consumer",
+		"js-max-mem-stream",
+		"js-max-disk-stream",
+		"js-max-bytes-required",
+		"js-max-ack-pending",
+		"rm-js-tier",
+	)
+
+	if hasEnableTier && p.hasJSSetParams {
+		return fmt.Errorf("js-enable is exclusive of all other js options")
+	}
+
+	if p.disableJetStream && (p.hasJSSetParams || hasEnableTier) {
+		return fmt.Errorf("js-enable is exclusive of all other js options")
+	}
+
 	if !InteractiveFlag && ctx.NothingToDo(
 		"start", "expiry", "tag", "rm-tag", "conns", "leaf-conns", "exports", "imports", "subscriptions",
 		"payload", "data", "wildcard-exports", "sk", "rm-sk", "description", "info-url", "response-ttl", "allow-pub-response",
@@ -213,6 +239,7 @@ func (p *EditAccountParams) SetDefaults(ctx ActionCtx) error {
 		"js-max-bytes-required",
 		"js-max-ack-pending",
 		"js-disable",
+		"js-enable",
 		"trace-context-subject",
 		"trace-context-sampling",
 	) {
@@ -604,6 +631,7 @@ func (p *EditAccountParams) checkSystemAccount(ctx ActionCtx) error {
 	if p.MaxAckPending > 0 {
 		mustUnset = append(mustUnset, "--js-max-ack-pending")
 	}
+
 	if len(mustUnset) == 0 {
 		// no user specified values set - so set to zero/false
 		// reset all the flags to zero
@@ -682,6 +710,20 @@ func (p *EditAccountParams) Validate(ctx ActionCtx) error {
 		}
 	}
 
+	if p.enableJetStream > -1 {
+		tier, err := p.getTierLimits(p.enableJetStream)
+		if err != nil {
+			return err
+		}
+		if jsLimitsSet(tier) {
+			label := "global"
+			if p.enableJetStream > 0 {
+				label = fmt.Sprintf("R%d", p.enableJetStream)
+			}
+			return fmt.Errorf("jetstream tier %s is already enabled", label)
+		}
+	}
+
 	return nil
 }
 
@@ -694,8 +736,8 @@ func (p *EditAccountParams) applyLimits(ctx ActionCtx, r *store.Report) error {
 	}
 	params := &p.JetStreamLimitParams
 
-	// on delete we don't honor any of the JS options
-	if p.DeleteTier != -1 {
+	// on delete or enable we don't honor any of the JS options
+	if p.DeleteTier != -1 || p.enableJetStream != -1 {
 		params.MemMaxStreamBytes = 0
 		params.DiskMaxStreamBytes = 0
 		params.MaxAckPending = 0
@@ -706,27 +748,30 @@ func (p *EditAccountParams) applyLimits(ctx ActionCtx, r *store.Report) error {
 		params.MaxBytesRequired = false
 	}
 
-	switch p.DeleteTier {
-	case -1:
-		break
-	case 0:
-		// values are zeroed by the params which are zeroed above
-		p.claim.Limits.JetStreamLimits = jwt.JetStreamLimits{}
-		r.AddOK("deleted global limit")
-	default:
-		if p.claim.Limits.JetStreamTieredLimits != nil {
-			label := fmt.Sprintf("R%d", p.DeleteTier)
-			_, ok := p.claim.Limits.JetStreamTieredLimits[label]
-			if ok {
-				delete(p.claim.Limits.JetStreamTieredLimits, label)
-				r.AddOK("deleted tier limit %s", label)
+	if p.DeleteTier != -1 {
+		switch p.DeleteTier {
+		case -1:
+			break
+		case 0:
+			// values are zeroed by the params which are zeroed above
+			p.claim.Limits.JetStreamLimits = jwt.JetStreamLimits{}
+			r.AddOK("deleted global limit")
+		default:
+			if p.claim.Limits.JetStreamTieredLimits != nil {
+				label := fmt.Sprintf("R%d", p.DeleteTier)
+				_, ok := p.claim.Limits.JetStreamTieredLimits[label]
+				if ok {
+					delete(p.claim.Limits.JetStreamTieredLimits, label)
+					r.AddOK("deleted tier limit %s", label)
+				} else {
+					return fmt.Errorf("account doesn't have tier %s limit", label)
+				}
 			} else {
-				return fmt.Errorf("account doesn't have tier %s limit", label)
+				return errors.New("account doesn't have tier limits")
 			}
-		} else {
-			return errors.New("account doesn't have tier limits")
 		}
 	}
+
 	if p.DeleteTier != -1 {
 		return nil
 	}
@@ -735,6 +780,39 @@ func (p *EditAccountParams) applyLimits(ctx ActionCtx, r *store.Report) error {
 		return p.doDisableJetStream(r)
 	}
 
+	if p.enableJetStream != -1 {
+		switch p.enableJetStream {
+		case -1:
+			break
+		case 0:
+			// values are zeroed by the params which are zeroed above
+			p.claim.Limits.JetStreamLimits = jwt.JetStreamLimits{
+				DiskStorage:   -1,
+				MemoryStorage: -1,
+			}
+			r.AddOK("enabled global limit")
+		default:
+			label := fmt.Sprintf("R%d", p.enableJetStream)
+			_, ok := p.claim.Limits.JetStreamTieredLimits[label]
+			if ok {
+				return fmt.Errorf("tier limit %s is already enabled", label)
+			} else {
+				if p.claim.Limits.JetStreamTieredLimits == nil {
+					p.claim.Limits.JetStreamTieredLimits = make(map[string]jwt.JetStreamLimits)
+				}
+				p.claim.Limits.JetStreamTieredLimits[label] = jwt.JetStreamLimits{
+					DiskStorage:   -1,
+					MemoryStorage: -1,
+				}
+				r.AddOK("enabled tier limit %s", label)
+			}
+		}
+	}
+
+	if p.enableJetStream != -1 {
+		return nil
+	}
+
 	label := p.tierLabel()
 
 	limits.Streams = params.Streams.Int64()
diff --git a/go.mod b/go.mod
index 8df418f..536070b 100644
--- a/go.mod
+++ b/go.mod
@@ -44,14 +44,14 @@ require (
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/tcnksm/go-gitconfig v0.1.2 // indirect
 	github.com/ulikunitz/xz v0.5.11 // indirect
-	golang.org/x/crypto v0.19.0 // indirect
-	golang.org/x/net v0.21.0 // indirect
-	golang.org/x/oauth2 v0.17.0 // indirect
-	golang.org/x/sys v0.17.0 // indirect
-	golang.org/x/term v0.17.0 // indirect
+	golang.org/x/crypto v0.21.0 // indirect
+	golang.org/x/net v0.22.0 // indirect
+	golang.org/x/oauth2 v0.18.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/term v0.18.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/protobuf v1.32.0 // indirect
+	google.golang.org/protobuf v1.33.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index fa59fd7..1423663 100644
--- a/go.sum
+++ b/go.sum
@@ -116,6 +116,8 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -126,10 +128,14 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
 golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -148,11 +154,15 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -176,6 +186,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
 google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=


Test patch:
diff --git a/cmd/editaccount_test.go b/cmd/editaccount_test.go
index efb6274..420109f 100644
--- a/cmd/editaccount_test.go
+++ b/cmd/editaccount_test.go
@@ -475,3 +475,51 @@ func Test_TracingSubject(t *testing.T) {
 	require.NoError(t, err)
 	require.Nil(t, ac.Trace)
 }
+
+func Test_EnableTier(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Equal(t, ac.Limits.JetStreamLimits, jwt.JetStreamLimits{})
+
+	_, _, err = ExecuteCmd(createEditAccount(), "A", "--js-enable", "0")
+	require.NoError(t, err)
+
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Equal(t, ac.Limits.JetStreamLimits, jwt.JetStreamLimits{DiskStorage: -1, MemoryStorage: -1})
+}
+
+func Test_EnableTierDoesntClobber(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Equal(t, ac.Limits.JetStreamLimits, jwt.JetStreamLimits{})
+
+	_, _, err = ExecuteCmd(createEditAccount(), "A", "--js-enable", "0")
+	require.NoError(t, err)
+
+	_, _, err = ExecuteCmd(createEditAccount(), "A", "--js-enable", "0")
+	require.Error(t, err)
+	require.Equal(t, "jetstream tier global is already enabled", err.Error())
+}
+
+func Test_EnableTierNoOtherFlag(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+	ts.AddAccount(t, "A")
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Equal(t, ac.Limits.JetStreamLimits, jwt.JetStreamLimits{})
+
+	_, _, err = ExecuteCmd(createEditAccount(), "A", "--js-enable", "0", "--rm-js-tier", "0")
+	require.Error(t, err)
+	require.Equal(t, "rm-js-tier is exclusive of all other js options", err.Error())
+}