Repository: nats-io/nsc
Pull request #220 (2019-10-14 20:14:15)

added ability to edit exports


Patch:
diff --git a/cmd/editexport.go b/cmd/editexport.go
new file mode 100644
index 0000000..b55f29f
--- /dev/null
+++ b/cmd/editexport.go
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2018-2019 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+
+	"github.com/nats-io/jwt"
+	"github.com/nats-io/nkeys"
+	"github.com/nats-io/nsc/cli"
+	"github.com/nats-io/nsc/cmd/store"
+	"github.com/spf13/cobra"
+)
+
+func createEditExportCmd() *cobra.Command {
+	var params EditExportParams
+	cmd := &cobra.Command{
+		Use:          "export",
+		Short:        "Edit an export",
+		Args:         MaxArgs(0),
+		SilenceUsage: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return RunAction(cmd, args, &params)
+		},
+	}
+
+	cmd.Flags().StringVarP(&params.name, "name", "n", "", "export name")
+	cmd.Flags().StringVarP(&params.subject, "subject", "s", "", "subject")
+	cmd.Flags().BoolVarP(&params.service, "service", "r", false, "export type service")
+	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().IntVarP(&params.latSampling, "sampling", "", 0, "latency sampling percentage [0-100] - 0 disables it (services only)")
+	params.AccountContextParams.BindFlags(cmd)
+
+	return cmd
+}
+
+func init() {
+	editCmd.AddCommand(createEditExportCmd())
+}
+
+type EditExportParams struct {
+	AccountContextParams
+	SignerParams
+	claim   *jwt.AccountClaims
+	index   int
+	subject string
+
+	name        string
+	latSampling int
+	latSubject  string
+	service     bool
+	private     bool
+}
+
+func (p *EditExportParams) SetDefaults(ctx ActionCtx) error {
+	if !InteractiveFlag {
+		if ctx.NothingToDo("name", "subject", "service", "private", "latency", "sampling") {
+			return errors.New("please specify some options")
+		}
+	}
+	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
+		return err
+	}
+	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
+	p.index = -1
+	return nil
+}
+
+func (p *EditExportParams) PreInteractive(ctx ActionCtx) error {
+	var err error
+	if err = p.AccountContextParams.Edit(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (p *EditExportParams) Load(ctx ActionCtx) error {
+	var err error
+
+	if err = p.AccountContextParams.Validate(ctx); err != nil {
+		return err
+	}
+
+	p.claim, err = ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name)
+	if err != nil {
+		return err
+	}
+
+	switch len(p.claim.Exports) {
+	case 0:
+		return fmt.Errorf("account %q doesn't have exports", p.AccountContextParams.Name)
+	case 1:
+		if p.subject == "" {
+			p.subject = string(p.claim.Exports[0].Subject)
+		}
+	}
+
+	for i, e := range p.claim.Exports {
+		if string(e.Subject) == p.subject {
+			p.index = i
+			break
+		}
+	}
+	// if we are not running in interactive set the option default the non-set values
+	if !InteractiveFlag {
+		p.syncOptions(ctx)
+	}
+
+	return nil
+}
+
+func (p *EditExportParams) PostInteractive(ctx ActionCtx) error {
+	var err error
+
+	choices, err := GetAccountExports(p.claim)
+	if err != nil {
+		return err
+	}
+	labels := AccountExportChoices(choices).String()
+	index := p.index
+	if index == -1 {
+		index = 0
+	}
+	p.index, err = cli.PromptChoices("select export to edit", labels[index], labels)
+	if err != nil {
+		return err
+	}
+
+	sel := choices[p.index].Selection
+
+	kinds := []string{jwt.Stream.String(), jwt.Service.String()}
+	k := kinds[0]
+	if sel.Type == jwt.Service {
+		k = kinds[1]
+	}
+	i, err := cli.PromptChoices("export type", k, kinds)
+	if err != nil {
+		return err
+	}
+	p.service = i == 1
+
+	svFn := func(s string) error {
+		var export jwt.Export
+		export.Type = jwt.Stream
+		if p.service {
+			export.Type = jwt.Service
+		}
+		export.Subject = jwt.Subject(s)
+		var vr jwt.ValidationResults
+		export.Validate(&vr)
+		if len(vr.Issues) > 0 {
+			return errors.New(vr.Issues[0].Description)
+		}
+		return nil
+	}
+
+	p.subject, err = cli.Prompt("subject", string(sel.Subject), true, svFn)
+	if err != nil {
+		return err
+	}
+
+	if p.name == "" {
+		p.name = sel.Name
+	}
+	p.name, err = cli.Prompt("name", p.name, true, cli.LengthValidator(1))
+	if err != nil {
+		return err
+	}
+
+	p.private, err = cli.PromptBoolean(fmt.Sprintf("private %s", k), sel.TokenReq)
+	if err != nil {
+		return err
+	}
+
+	if p.service {
+		ok, err := cli.PromptBoolean("track service latency", false)
+		if err != nil {
+			return err
+		}
+		if ok {
+			cls := 0
+			results := jwt.Subject("")
+			if sel.Latency != nil {
+				cls = sel.Latency.Sampling
+				results = sel.Latency.Results
+			}
+			_, err = cli.Prompt("sampling percentage [0-100] (0 disables)", strconv.Itoa(cls), false, func(s string) error {
+				v, err := strconv.Atoi(s)
+				if err != nil {
+					return err
+				}
+				if v < 0 || v > 100 {
+					return errors.New("sampling must be between 0 and 100 inclusive")
+				}
+				p.latSampling = v
+				return nil
+			})
+			p.latSubject, err = cli.Prompt("latency metrics subject", string(results), true, func(s string) error {
+				var lat jwt.ServiceLatency
+				lat.Results = jwt.Subject(s)
+				lat.Sampling = p.latSampling
+				var vr jwt.ValidationResults
+				lat.Validate(&vr)
+				if len(vr.Issues) > 0 {
+					return errors.New(vr.Issues[0].Description)
+				}
+				return nil
+			})
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	if err = p.SignerParams.Edit(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (p *EditExportParams) Validate(ctx ActionCtx) error {
+	ctx.CurrentCmd().SilenceUsage = false
+	var err error
+	if p.subject == "" {
+		return errors.New("a subject is required")
+	}
+	if p.index == -1 {
+		return fmt.Errorf("no export with subject %q found", p.subject)
+	}
+
+	if err = p.SignerParams.Resolve(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (p *EditExportParams) syncOptions(ctx ActionCtx) {
+	if p.index == -1 {
+		return
+	}
+	old := *p.claim.Exports[p.index]
+
+	cmd := ctx.CurrentCmd()
+	if !cmd.Flag("service").Changed {
+		p.service = old.Type == jwt.Service
+	}
+	if !(cmd.Flag("name").Changed) {
+		p.name = old.Name
+	}
+	if !(cmd.Flag("private").Changed) {
+		p.private = old.TokenReq
+	}
+	sampling := 0
+	latency := ""
+	if old.Latency != nil {
+		sampling = old.Latency.Sampling
+		latency = string(old.Latency.Results)
+	}
+	if !(cmd.Flag("latency").Changed) {
+		p.latSubject = latency
+	}
+	if !(cmd.Flag("sampling").Changed) {
+		p.latSampling = sampling
+	}
+}
+
+func (p *EditExportParams) Run(ctx ActionCtx) (store.Status, error) {
+	old := *p.claim.Exports[p.index]
+	// old vr
+	var vr jwt.ValidationResults
+	if err := p.claim.Exports.Validate(&vr); err != nil {
+		return nil, err
+	}
+
+	r := store.NewDetailedReport(false)
+	var export jwt.Export
+	export.Name = p.name
+	if export.Name != old.Name {
+		r.AddOK("changed export name to %s", export.Name)
+	}
+
+	export.TokenReq = p.private
+	if export.TokenReq != old.TokenReq {
+		r.AddWarning("changed export to be private - this will break importers")
+	}
+	export.Subject = jwt.Subject(p.subject)
+	if export.Subject != old.Subject {
+		r.AddWarning("changed subject to %q - this will break importers", export.Subject)
+	}
+	export.Type = jwt.Stream
+	if p.service {
+		export.Type = jwt.Service
+	}
+	if export.Type != old.Type {
+		r.AddWarning("changed export type to %q - this will break importers", export.Type.String())
+	}
+
+	if export.Type == jwt.Service {
+		oldSampling := 0
+		oldReport := jwt.Subject("")
+		if old.Latency != nil {
+			oldSampling = old.Latency.Sampling
+			oldReport = old.Latency.Results
+		}
+		if p.latSubject != "" {
+			export.Latency = &jwt.ServiceLatency{Results: jwt.Subject(p.latSubject), Sampling: p.latSampling}
+			if oldSampling != export.Latency.Sampling {
+				r.AddOK("changed service latency to %d%%", export.Latency.Sampling)
+			}
+			if oldReport != export.Latency.Results {
+				r.AddOK("changed service latency subject to %s", export.Latency.Results)
+				r.AddWarning("changed latency subject will break consumers of the report")
+			}
+		}
+	}
+
+	p.claim.Exports[p.index] = &export
+
+	var vr2 jwt.ValidationResults
+	if err := p.claim.Exports.Validate(&vr2); err != nil {
+		return nil, err
+	}
+	// filter out all the old validations
+	uvr := jwt.CreateValidationResults()
+	if len(vr.Issues) > 0 {
+		for _, nis := range vr.Issues {
+			for _, is := range vr2.Issues {
+				if nis.Description == is.Description {
+					continue
+				}
+			}
+			uvr.Add(nis)
+		}
+	} else {
+		uvr = &vr2
+	}
+	// fail validation
+	if len(uvr.Issues) > 0 {
+		return nil, errors.New(uvr.Issues[0].Error())
+	}
+
+	token, err := p.claim.Encode(p.signerKP)
+	if err != nil {
+		return nil, err
+	}
+
+	StoreAccountAndUpdateStatus(ctx, token, r)
+	if r.HasNoErrors() {
+		r.AddOK("edited %s export %q", export.Type, export.Name)
+	}
+	return r, err
+}


Test patch:
diff --git a/cmd/editexport_test.go b/cmd/editexport_test.go
new file mode 100644
index 0000000..4517df3
--- /dev/null
+++ b/cmd/editexport_test.go
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018-2019 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cmd
+
+import (
+	"testing"
+
+	"github.com/nats-io/jwt"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_EditExport_Private(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	ts.AddExport(t, "A", jwt.Service, "a", true)
+
+	_, _, err := ExecuteCmd(createEditExportCmd(), "--subject", "a", "--private")
+	require.NoError(t, err)
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.True(t, ac.Exports[0].TokenReq)
+}
+
+func Test_EditExport_Latency(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	ts.AddExport(t, "A", jwt.Service, "a", true)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.Nil(t, ac.Exports[0].Latency)
+
+	_, _, err = ExecuteCmd(createEditExportCmd(), "--subject", "a", "--sampling", "100", "--latency", "lat")
+	require.NoError(t, err)
+
+	ac, err = ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.NotNil(t, ac.Exports[0].Latency)
+	require.Equal(t, 100, ac.Exports[0].Latency.Sampling)
+	require.Equal(t, jwt.Subject("lat"), ac.Exports[0].Latency.Results)
+}
+
+func Test_EditExportInteractive(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	ts.AddExport(t, "A", jwt.Service, "a", true)
+	ts.AddExport(t, "A", jwt.Service, "b", true)
+
+	_, _, err := ExecuteInteractiveCmd(createEditExportCmd(), []interface{}{1, 1, "c", "c", false, false})
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Len(t, ac.Exports, 2)
+	require.Equal(t, jwt.Subject("c"), ac.Exports[1].Subject)
+}
+
+func Test_EditExportInteractiveLatency(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	ts.AddExport(t, "A", jwt.Service, "a", true)
+
+	_, _, err := ExecuteInteractiveCmd(createEditExportCmd(), []interface{}{0, 1, "c", "c", false, true, "100", "lat"})
+	require.NoError(t, err)
+
+	ac, err := ts.Store.ReadAccountClaim("A")
+	require.NoError(t, err)
+	require.Equal(t, jwt.Subject("c"), ac.Exports[0].Subject)
+	require.Equal(t, "c", ac.Exports[0].Name)
+	require.NotNil(t, ac.Exports[0].Latency)
+	require.Equal(t, 100, ac.Exports[0].Latency.Sampling)
+	require.Equal(t, jwt.Subject("lat"), ac.Exports[0].Latency.Results)
+}
+
+func Test_EditExportNoExports(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	ts.AddAccount(t, "A")
+	_, _, err := ExecuteCmd(createEditExportCmd(), "--subject", "a")
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "doesn't have exports")
+}