Repository: nats-io/nsc
Pull request #342 (2021-01-08 02:26:11)

[Added] support for service export response threshold
Signed-off-by: Matthias Hanel <mh@synadia.com>


Patch:
diff --git a/cmd/addexport.go b/cmd/addexport.go
index ab84331..c96a4ef 100644
--- a/cmd/addexport.go
+++ b/cmd/addexport.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"strconv"
 	"strings"
+	"time"
 
 	cli "github.com/nats-io/cliprompts/v2"
 	"github.com/nats-io/jwt/v2"
@@ -46,6 +47,7 @@ func createAddExportCmd() *cobra.Command {
 	cmd.Flags().BoolVarP(&params.private, "private", "p", false, "private export - requires an activation to access")
 	cmd.Flags().StringVarP(&params.latSubject, "latency", "", "", "latency metrics subject (services only)")
 	cmd.Flags().StringVarP(&params.latSampling, "sampling", "", "", "latency sampling percentage [1-100] or `header`  (services only)")
+	cmd.Flags().DurationVarP(&params.responseThreshold, "response-threshold", "", 0, "response threshold duration (units ms/s/m/h) (services only)")
 	hm := fmt.Sprintf("response type for the service [%s | %s | %s] (services only)", jwt.ResponseTypeSingleton, jwt.ResponseTypeStream, jwt.ResponseTypeChunked)
 	cmd.Flags().StringVarP(&params.responseType, "response-type", "", jwt.ResponseTypeSingleton, hm)
 	params.AccountContextParams.BindFlags(cmd)
@@ -65,10 +67,11 @@ type AddExportParams struct {
 	private bool
 	service bool
 	SignerParams
-	subject      string
-	latSubject   string
-	latSampling  string
-	responseType string
+	subject           string
+	latSubject        string
+	latSampling       string
+	responseType      string
+	responseThreshold time.Duration
 }
 
 func (p *AddExportParams) longHelp() string {
@@ -170,6 +173,11 @@ func (p *AddExportParams) PreInteractive(ctx ActionCtx) error {
 			return err
 		}
 		p.export.ResponseType = jwt.ResponseType(choices[s])
+
+		p.export.ResponseThreshold, err = promptDuration("response threshold (0 disabled)", p.responseThreshold)
+		if err != nil {
+			return err
+		}
 	}
 
 	if err := p.SignerParams.Edit(ctx); err != nil {
@@ -297,6 +305,7 @@ func (p *AddExportParams) Validate(ctx ActionCtx) error {
 			return fmt.Errorf("unknown response type %q", p.responseType)
 		}
 		p.export.ResponseType = rt
+		p.export.ResponseThreshold = p.responseThreshold
 	}
 
 	if err = p.SignerParams.Resolve(ctx); err != nil {
diff --git a/cmd/common.go b/cmd/common.go
index 920d2b8..b3728ab 100644
--- a/cmd/common.go
+++ b/cmd/common.go
@@ -569,3 +569,14 @@ func StoreAccountAndUpdateStatus(ctx ActionCtx, token string, status *store.Repo
 		status.AddFromError(err)
 	}
 }
+
+func promptDuration(label string, defaultValue time.Duration) (time.Duration, error) {
+	value, err := cli.Prompt(label, defaultValue.String())
+	if err != nil {
+		return time.Duration(0), err
+	}
+	if value == "" {
+		return time.Duration(0), nil
+	}
+	return time.ParseDuration(value)
+}
diff --git a/cmd/editexport.go b/cmd/editexport.go
index b2cf35e..09e780e 100644
--- a/cmd/editexport.go
+++ b/cmd/editexport.go
@@ -18,6 +18,7 @@ package cmd
 import (
 	"errors"
 	"fmt"
+	"time"
 
 	cli "github.com/nats-io/cliprompts/v2"
 	"github.com/nats-io/jwt/v2"
@@ -47,6 +48,7 @@ func createEditExportCmd() *cobra.Command {
 	cmd.Flags().BoolVarP(&params.rmLatencySampling, "rm-latency-sampling", "", false, "remove latency sampling")
 	cmd.Flags().StringVarP(&params.description, "description", "", "", "Description for this export")
 	cmd.Flags().StringVarP(&params.infoUrl, "info-url", "", "", "Link for more info on this export")
+	cmd.Flags().DurationVarP(&params.responseThreshold, "response-threshold", "", 0, "response threshold duration (units ms/s/m/h) (services only)")
 
 	hm := fmt.Sprintf("response type for the service [%s | %s | %s] (services only)", jwt.ResponseTypeSingleton, jwt.ResponseTypeStream, jwt.ResponseTypeChunked)
 	cmd.Flags().StringVarP(&params.responseType, "response-type", "", jwt.ResponseTypeSingleton, hm)
@@ -75,6 +77,7 @@ type EditExportParams struct {
 	rmLatencySampling bool
 	infoUrl           string
 	description       string
+	responseThreshold time.Duration
 }
 
 func (p *EditExportParams) SetDefaults(ctx ActionCtx) error {
@@ -231,6 +234,10 @@ func (p *EditExportParams) PostInteractive(ctx ActionCtx) error {
 			return err
 		}
 		p.responseType = choices[s]
+		p.responseThreshold, err = promptDuration("response threshold (0 disabled)", p.responseThreshold)
+		if err != nil {
+			return err
+		}
 	}
 
 	if err = p.SignerParams.Edit(ctx); err != nil {
@@ -265,6 +272,8 @@ func (p *EditExportParams) Validate(ctx ActionCtx) error {
 			rt != jwt.ResponseTypeChunked {
 			return fmt.Errorf("unknown response type %q", p.responseType)
 		}
+	} else if p.responseThreshold != time.Duration(0) {
+		return errors.New("response threshold is only applicable to services")
 	}
 
 	if err = p.SignerParams.Resolve(ctx); err != nil {
@@ -313,6 +322,10 @@ func (p *EditExportParams) syncOptions(ctx ActionCtx) {
 		p.responseType = string(old.ResponseType)
 	}
 
+	if !(cmd.Flag("response-threshold").Changed) {
+		p.responseThreshold = old.ResponseThreshold
+	}
+
 	if !(cmd.Flag("description").Changed) {
 		p.description = old.Description
 	}
@@ -385,6 +398,8 @@ func (p *EditExportParams) Run(ctx ActionCtx) (store.Status, error) {
 			}
 		}
 
+		export.ResponseThreshold = p.responseThreshold
+
 		rt := jwt.ResponseType(p.responseType)
 		if old.ResponseType != rt {
 			export.ResponseType = rt
diff --git a/dependencies.md b/dependencies.md
new file mode 100644
index 0000000..e6b32c5
--- /dev/null
+++ b/dependencies.md
@@ -0,0 +1,23 @@
+# External Dependencies
+
+This file lists the dependencies used in this repository.
+
+| Dependency | License |
+|-|-|
+| github.com/blang/semver | The MIT License |
+| github.com/briandowns/spinner | Apache License |
+| github.com/dustin/go-humanize | MIT |
+| github.com/mitchellh/go-homedir | The MIT License (MIT) |
+| github.com/nats-io/cliprompts/v2 | Apache License |
+| github.com/nats-io/jwt | Apache License |
+| github.com/nats-io/nats-server/v2 | Apache License |
+| github.com/nats-io/nats.go | Apache License |
+| github.com/nats-io/nkeys | Apache License |
+| github.com/nats-io/nuid | Apache License |
+| github.com/rhysd/go-github-selfupdate | the MIT License |
+| github.com/spf13/cobra | Apache License |
+| github.com/spf13/pflag | bsd-3-clause |
+| github.com/spf13/viper | The MIT License (MIT) |
+| github.com/stretchr/testify | MIT |
+| github.com/xlab/tablewriter | Apache License 2.0 |
+| go | BSD 3-Clause "New" or "Revised" License |


Test patch:
diff --git a/cmd/addexport_test.go b/cmd/addexport_test.go
index 0b7f03c..98656f6 100644
--- a/cmd/addexport_test.go
+++ b/cmd/addexport_test.go
@@ -17,6 +17,7 @@ package cmd
 
 import (
 	"testing"
+	"time"
 
 	"github.com/nats-io/jwt/v2"
 	"github.com/stretchr/testify/require"
@@ -39,6 +40,8 @@ func Test_AddExport(t *testing.T) {
 		{createAddExportCmd(), []string{"add", "export", "--subject", "mar", "--name", "ar", "--service"}, nil, []string{"added public service export \"ar\""}, false},
 		{createAddExportCmd(), []string{"add", "export", "--subject", "pubstream", "--private"}, nil, []string{"added private stream export \"pubstream\""}, false},
 		{createAddExportCmd(), []string{"add", "export", "--subject", "pubservice", "--private", "--service"}, nil, []string{"added private service export \"pubservice\""}, false},
+		{createAddExportCmd(), []string{"add", "export", "--subject", "th.1", "--service", "--response-threshold", "1s"}, nil, []string{"added public service export \"th.1\""}, false},
+		{createAddExportCmd(), []string{"add", "export", "--subject", "th.2", "--response-threshold", "1whatever"}, nil, []string{`time: unknown unit`}, true},
 	}
 
 	tests.Run(t, "root", "add")
@@ -133,7 +136,7 @@ func Test_AddExportAccountNameRequired(t *testing.T) {
 	require.Equal(t, "bbbb", ac.Exports[0].Name)
 }
 
-func TestAddExportInteractive(t *testing.T) {
+func TestAddStreamExportInteractive(t *testing.T) {
 	ts := NewTestStore(t, "test")
 	defer ts.Done(t)
 
@@ -151,6 +154,31 @@ func TestAddExportInteractive(t *testing.T) {
 	require.Len(t, ac.Exports, 1)
 	require.Equal(t, "Foo Stream", ac.Exports[0].Name)
 	require.Equal(t, "foo.>", string(ac.Exports[0].Subject))
+	require.Equal(t, jwt.Stream, ac.Exports[0].Type)
+
+}
+func TestAddServiceExportInteractive(t *testing.T) {
+	ts := NewTestStore(t, "test")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	ts.AddAccount(t, "B")
+
+	input := []interface{}{0, 1, "bar.>", "Bar Stream", true, true, "header", "foo", 0, "1s", 0}
+	cmd := createAddExportCmd()
+	HoistRootFlags(cmd)
+	_, _, err := ExecuteInteractiveCmd(cmd, input, "-i")
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Len(t, ac.Exports, 1)
+	require.Equal(t, "Bar Stream", ac.Exports[0].Name)
+	require.Equal(t, "bar.>", string(ac.Exports[0].Subject))
+	require.Equal(t, jwt.Service, ac.Exports[0].Type)
+	require.Equal(t, time.Second, ac.Exports[0].ResponseThreshold)
+	require.Equal(t, jwt.Headers, ac.Exports[0].Latency.Sampling)
+
 }
 
 func TestAddExportNonInteractive(t *testing.T) {
@@ -213,7 +241,7 @@ func TestAddServiceLatencyInteractive(t *testing.T) {
 	cmd := createAddExportCmd()
 
 	// service, subject, name, private, track, freq
-	args := []interface{}{1, "q", "q", false, true, "100", "q.lat", 1}
+	args := []interface{}{1, "q", "q", false, true, "100", "q.lat", 1, "0"}
 	_, _, err := ExecuteInteractiveCmd(cmd, args)
 	require.NoError(t, err)
 
@@ -225,4 +253,5 @@ func TestAddServiceLatencyInteractive(t *testing.T) {
 	require.Equal(t, "q.lat", string(ac.Exports[0].Latency.Results))
 	require.Equal(t, jwt.SamplingRate(100), ac.Exports[0].Latency.Sampling)
 	require.EqualValues(t, jwt.ResponseTypeStream, ac.Exports[0].ResponseType)
+	require.Equal(t, time.Duration(0), ac.Exports[0].ResponseThreshold)
 }
diff --git a/cmd/editexport_test.go b/cmd/editexport_test.go
index d1c0014..c2306cb 100644
--- a/cmd/editexport_test.go
+++ b/cmd/editexport_test.go
@@ -17,6 +17,7 @@ package cmd
 
 import (
 	"testing"
+	"time"
 
 	"github.com/nats-io/jwt/v2"
 	"github.com/stretchr/testify/require"
@@ -48,7 +49,7 @@ func Test_EditExport_Latency(t *testing.T) {
 	require.NoError(t, err)
 	require.Nil(t, ac.Exports[0].Latency)
 
-	_, _, err = ExecuteCmd(createEditExportCmd(), "--subject", "a", "--sampling", "100", "--latency", "lat")
+	_, _, err = ExecuteCmd(createEditExportCmd(), "--subject", "a", "--sampling", "100", "--latency", "lat", "--response-threshold", "1s")
 	require.NoError(t, err)
 
 	ac, err = ts.Store.ReadAccountClaim("A")
@@ -56,6 +57,7 @@ func Test_EditExport_Latency(t *testing.T) {
 	require.NotNil(t, ac.Exports[0].Latency)
 	require.Equal(t, jwt.SamplingRate(100), ac.Exports[0].Latency.Sampling)
 	require.Equal(t, jwt.Subject("lat"), ac.Exports[0].Latency.Results)
+	require.Equal(t, time.Second, ac.Exports[0].ResponseThreshold)
 }
 
 func Test_EditExportInteractive(t *testing.T) {
@@ -67,7 +69,7 @@ func Test_EditExportInteractive(t *testing.T) {
 	ts.AddExport(t, "A", jwt.Service, "b", true)
 
 	link := "http://foo/bar"
-	_, _, err := ExecuteInteractiveCmd(createEditExportCmd(), []interface{}{1, 1, "c", "c", false, false, 1, "desc", link})
+	_, _, err := ExecuteInteractiveCmd(createEditExportCmd(), []interface{}{1, 1, "c", "c", false, false, 1, "1s", "desc", link})
 	require.NoError(t, err)
 
 	ac, err := ts.Store.ReadAccountClaim("A")
@@ -77,6 +79,7 @@ func Test_EditExportInteractive(t *testing.T) {
 	require.EqualValues(t, jwt.ResponseTypeStream, ac.Exports[1].ResponseType)
 	require.Equal(t, ac.Exports[1].Description, "desc")
 	require.Equal(t, ac.Exports[1].InfoURL, link)
+	require.Equal(t, ac.Exports[1].ResponseThreshold, time.Second)
 }
 
 func Test_EditExportInteractiveLatency(t *testing.T) {
@@ -86,7 +89,7 @@ func Test_EditExportInteractiveLatency(t *testing.T) {
 	ts.AddAccount(t, "A")
 	ts.AddExport(t, "A", jwt.Service, "a", true)
 
-	_, _, err := ExecuteInteractiveCmd(createEditExportCmd(), []interface{}{0, 1, "c", "c", false, true, "header", "lat", 2, "", ""})
+	_, _, err := ExecuteInteractiveCmd(createEditExportCmd(), []interface{}{0, 1, "c", "c", false, true, "header", "lat", 2, "", "", ""})
 	require.NoError(t, err)
 
 	ac, err := ts.Store.ReadAccountClaim("A")