github.com/kbehouse/nsc@v0.0.6/cmd/editexport.go (about)

     1  /*
     2   * Copyright 2018-2020 The NATS Authors
     3   * Licensed under the Apache License, Version 2.0 (the "License");
     4   * you may not use this file except in compliance with the License.
     5   * You may obtain a copy of the License at
     6   *
     7   * http://www.apache.org/licenses/LICENSE-2.0
     8   *
     9   * Unless required by applicable law or agreed to in writing, software
    10   * distributed under the License is distributed on an "AS IS" BASIS,
    11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12   * See the License for the specific language governing permissions and
    13   * limitations under the License.
    14   */
    15  
    16  package cmd
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"time"
    22  
    23  	"github.com/kbehouse/nsc/cmd/store"
    24  	cli "github.com/nats-io/cliprompts/v2"
    25  	"github.com/nats-io/jwt/v2"
    26  	"github.com/nats-io/nkeys"
    27  	"github.com/spf13/cobra"
    28  )
    29  
    30  func createEditExportCmd() *cobra.Command {
    31  	var params EditExportParams
    32  	cmd := &cobra.Command{
    33  		Use:          "export",
    34  		Short:        "Edit an export",
    35  		Args:         MaxArgs(0),
    36  		SilenceUsage: true,
    37  		RunE: func(cmd *cobra.Command, args []string) error {
    38  			return RunAction(cmd, args, &params)
    39  		},
    40  	}
    41  
    42  	cmd.Flags().StringVarP(&params.name, "name", "n", "", "export name")
    43  	cmd.Flags().StringVarP(&params.subject, "subject", "s", "", "subject")
    44  	cmd.Flags().BoolVarP(&params.service, "service", "r", false, "export type service")
    45  	cmd.Flags().BoolVarP(&params.private, "private", "p", false, "private export - requires an activation to access")
    46  	cmd.Flags().StringVarP(&params.latSubject, "latency", "", "", "latency metrics subject (services only)")
    47  	cmd.Flags().StringVarP(&params.latSampling, "sampling", "", "", "latency sampling percentage [1-100] or `header` - 0 disables it (services only)")
    48  	cmd.Flags().BoolVarP(&params.rmLatencySampling, "rm-latency-sampling", "", false, "remove latency sampling")
    49  	cmd.Flags().StringVarP(&params.description, "description", "", "", "Description for this export")
    50  	cmd.Flags().StringVarP(&params.infoUrl, "info-url", "", "", "Link for more info on this export")
    51  	cmd.Flags().DurationVarP(&params.responseThreshold, "response-threshold", "", 0, "response threshold duration (units ms/s/m/h) (services only)")
    52  
    53  	hm := fmt.Sprintf("response type for the service [%s | %s | %s] (services only)", jwt.ResponseTypeSingleton, jwt.ResponseTypeStream, jwt.ResponseTypeChunked)
    54  	cmd.Flags().StringVarP(&params.responseType, "response-type", "", jwt.ResponseTypeSingleton, hm)
    55  	params.AccountContextParams.BindFlags(cmd)
    56  
    57  	return cmd
    58  }
    59  
    60  func init() {
    61  	editCmd.AddCommand(createEditExportCmd())
    62  }
    63  
    64  type EditExportParams struct {
    65  	AccountContextParams
    66  	SignerParams
    67  	claim   *jwt.AccountClaims
    68  	index   int
    69  	subject string
    70  
    71  	name              string
    72  	latSampling       string
    73  	latSubject        string
    74  	service           bool
    75  	private           bool
    76  	responseType      string
    77  	rmLatencySampling bool
    78  	infoUrl           string
    79  	description       string
    80  	responseThreshold time.Duration
    81  }
    82  
    83  func (p *EditExportParams) SetDefaults(ctx ActionCtx) error {
    84  	if !InteractiveFlag {
    85  		if ctx.NothingToDo("name", "subject", "service", "private", "latency", "sampling", "response-type",
    86  			"description", "info-url") {
    87  			return errors.New("please specify some options")
    88  		}
    89  	}
    90  	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
    91  		return err
    92  	}
    93  	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
    94  	p.index = -1
    95  	return nil
    96  }
    97  
    98  func (p *EditExportParams) PreInteractive(ctx ActionCtx) error {
    99  	var err error
   100  	if err = p.AccountContextParams.Edit(ctx); err != nil {
   101  		return err
   102  	}
   103  	return nil
   104  }
   105  
   106  func (p *EditExportParams) Load(ctx ActionCtx) error {
   107  	var err error
   108  
   109  	if err = p.AccountContextParams.Validate(ctx); err != nil {
   110  		return err
   111  	}
   112  
   113  	p.claim, err = ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	switch len(p.claim.Exports) {
   119  	case 0:
   120  		return fmt.Errorf("account %q doesn't have exports", p.AccountContextParams.Name)
   121  	case 1:
   122  		if p.subject == "" {
   123  			p.subject = string(p.claim.Exports[0].Subject)
   124  		}
   125  	}
   126  
   127  	for i, e := range p.claim.Exports {
   128  		if string(e.Subject) == p.subject {
   129  			p.index = i
   130  			break
   131  		}
   132  	}
   133  
   134  	// if we are not running in interactive set the option default the non-set values
   135  	if !InteractiveFlag {
   136  		p.syncOptions(ctx)
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  func (p *EditExportParams) PostInteractive(ctx ActionCtx) error {
   143  	var err error
   144  
   145  	choices, err := GetAccountExports(p.claim)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	labels := AccountExportChoices(choices).String()
   150  	index := p.index
   151  	if index == -1 {
   152  		index = 0
   153  	}
   154  	p.index, err = cli.Select("select export to edit", labels[index], labels)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	sel := choices[p.index].Selection
   160  
   161  	kinds := []string{jwt.Stream.String(), jwt.Service.String()}
   162  	k := kinds[0]
   163  	if sel.Type == jwt.Service {
   164  		k = kinds[1]
   165  	}
   166  	i, err := cli.Select("export type", k, kinds)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	p.service = i == 1
   171  
   172  	svFn := func(s string) error {
   173  		var export jwt.Export
   174  		export.Type = jwt.Stream
   175  		if p.service {
   176  			export.Type = jwt.Service
   177  		}
   178  		export.Subject = jwt.Subject(s)
   179  		var vr jwt.ValidationResults
   180  		export.Validate(&vr)
   181  		if len(vr.Issues) > 0 {
   182  			return errors.New(vr.Issues[0].Description)
   183  		}
   184  		return nil
   185  	}
   186  
   187  	p.subject, err = cli.Prompt("subject", string(sel.Subject), cli.Val(svFn))
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	if p.name == "" {
   193  		p.name = sel.Name
   194  	}
   195  	p.name, err = cli.Prompt("name", p.name, cli.NewLengthValidator(1))
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	p.private, err = cli.Confirm(fmt.Sprintf("private %s", k), sel.TokenReq)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	if p.service {
   206  		ok, err := cli.Confirm("track service latency", false)
   207  		if err != nil {
   208  			return err
   209  		}
   210  		if ok {
   211  			cls := ""
   212  			results := jwt.Subject("")
   213  			if sel.Latency != nil {
   214  				cls = latSamplingRateToString(sel.Latency.Sampling)
   215  				results = sel.Latency.Results
   216  			}
   217  			samp, err := cli.Prompt("sampling percentage [1-100] or `header`", cls, cli.Val(SamplingValidator))
   218  			if err != nil {
   219  				return err
   220  			}
   221  			p.latSampling = samp
   222  
   223  			p.latSubject, err = cli.Prompt("latency metrics subject", string(results), cli.Val(LatencyMetricsSubjectValidator))
   224  			if err != nil {
   225  				return err
   226  			}
   227  		} else {
   228  			p.rmLatencySampling = true
   229  		}
   230  
   231  		choices := []string{jwt.ResponseTypeSingleton, jwt.ResponseTypeStream, jwt.ResponseTypeChunked}
   232  		s, err := cli.Select("service response type", p.responseType, choices)
   233  		if err != nil {
   234  			return err
   235  		}
   236  		p.responseType = choices[s]
   237  		p.responseThreshold, err = promptDuration("response threshold (0 disabled)", p.responseThreshold)
   238  		if err != nil {
   239  			return err
   240  		}
   241  	}
   242  
   243  	if err = p.SignerParams.Edit(ctx); err != nil {
   244  		return err
   245  	}
   246  
   247  	if p.description, err = cli.Prompt("Export Description", p.description, validatorMaxLen(jwt.MaxInfoLength)); err != nil {
   248  		return err
   249  	}
   250  
   251  	if p.infoUrl, err = cli.Prompt("Info url", p.infoUrl, validatorUrlOrEmpty()); err != nil {
   252  		return err
   253  	}
   254  
   255  	return nil
   256  }
   257  
   258  func (p *EditExportParams) Validate(ctx ActionCtx) error {
   259  	ctx.CurrentCmd().SilenceUsage = false
   260  	var err error
   261  	if p.subject == "" {
   262  		return errors.New("a subject is required")
   263  	}
   264  	if p.index == -1 {
   265  		return fmt.Errorf("no export with subject %q found", p.subject)
   266  	}
   267  
   268  	if p.service {
   269  		rt := jwt.ResponseType(p.responseType)
   270  		if rt != jwt.ResponseTypeSingleton &&
   271  			rt != jwt.ResponseTypeStream &&
   272  			rt != jwt.ResponseTypeChunked {
   273  			return fmt.Errorf("unknown response type %q", p.responseType)
   274  		}
   275  	} else if p.responseThreshold != time.Duration(0) {
   276  		return errors.New("response threshold is only applicable to services")
   277  	}
   278  
   279  	if err = p.SignerParams.Resolve(ctx); err != nil {
   280  		return err
   281  	}
   282  
   283  	return nil
   284  }
   285  
   286  func (p *EditExportParams) syncOptions(ctx ActionCtx) {
   287  	if p.index == -1 {
   288  		return
   289  	}
   290  	old := *p.claim.Exports[p.index]
   291  
   292  	cmd := ctx.CurrentCmd()
   293  	if !cmd.Flag("service").Changed {
   294  		p.service = old.Type == jwt.Service
   295  	}
   296  	if !cmd.Flag("response-type").Changed {
   297  		if old.ResponseType == "" {
   298  			old.ResponseType = jwt.ResponseTypeSingleton
   299  		}
   300  		p.responseType = string(old.ResponseType)
   301  	}
   302  	if !(cmd.Flag("name").Changed) {
   303  		p.name = old.Name
   304  	}
   305  	if !(cmd.Flag("private").Changed) {
   306  		p.private = old.TokenReq
   307  	}
   308  	sampling := jwt.SamplingRate(0)
   309  	latency := ""
   310  	if old.Latency != nil {
   311  		sampling = old.Latency.Sampling
   312  		latency = string(old.Latency.Results)
   313  	}
   314  	if !(cmd.Flag("latency").Changed) {
   315  		p.latSubject = latency
   316  	}
   317  	if !(cmd.Flag("sampling").Changed) {
   318  		p.latSampling = latSamplingRateToString(sampling)
   319  	}
   320  
   321  	if !(cmd.Flag("response-type").Changed) {
   322  		p.responseType = string(old.ResponseType)
   323  	}
   324  
   325  	if !(cmd.Flag("response-threshold").Changed) {
   326  		p.responseThreshold = old.ResponseThreshold
   327  	}
   328  
   329  	if !(cmd.Flag("description").Changed) {
   330  		p.description = old.Description
   331  	}
   332  
   333  	if !(cmd.Flag("info-url").Changed) {
   334  		p.infoUrl = old.InfoURL
   335  	}
   336  }
   337  
   338  func (p *EditExportParams) Run(ctx ActionCtx) (store.Status, error) {
   339  	old := *p.claim.Exports[p.index]
   340  	// old vr
   341  	var vr jwt.ValidationResults
   342  	if err := p.claim.Exports.Validate(&vr); err != nil {
   343  		return nil, err
   344  	}
   345  
   346  	r := store.NewDetailedReport(false)
   347  	var export jwt.Export
   348  	export.Name = p.name
   349  	if export.Name != old.Name {
   350  		r.AddOK("changed export name to %s", export.Name)
   351  	}
   352  
   353  	export.TokenReq = p.private
   354  	if export.TokenReq != old.TokenReq {
   355  		r.AddWarning("changed export to be private - this will break importers")
   356  	}
   357  	export.Subject = jwt.Subject(p.subject)
   358  	if export.Subject != old.Subject {
   359  		r.AddWarning("changed subject to %q - this will break importers", export.Subject)
   360  	}
   361  	export.Type = jwt.Stream
   362  	if p.service {
   363  		export.Type = jwt.Service
   364  	}
   365  	if export.Type != old.Type {
   366  		r.AddWarning("changed export type to %q - this will break importers", export.Type.String())
   367  	}
   368  
   369  	if export.Type == jwt.Service {
   370  		// old response type may be blank
   371  		if old.ResponseType == "" {
   372  			old.ResponseType = jwt.ResponseTypeSingleton
   373  		}
   374  
   375  		if p.rmLatencySampling {
   376  			export.Latency = nil
   377  			if old.Latency != nil {
   378  				r.AddOK("removed latency tracking")
   379  			} else {
   380  				r.AddOK("no need to remove latency tracking as it was not set")
   381  			}
   382  		} else {
   383  			oldSampling := jwt.SamplingRate(0)
   384  			oldReport := jwt.Subject("")
   385  			if old.Latency != nil {
   386  				oldSampling = old.Latency.Sampling
   387  				oldReport = old.Latency.Results
   388  			}
   389  			if p.latSubject != "" {
   390  				export.Latency = &jwt.ServiceLatency{Results: jwt.Subject(p.latSubject), Sampling: latSamplingRate(p.latSampling)}
   391  				if oldSampling != export.Latency.Sampling {
   392  					r.AddOK("changed service latency to %d%%", export.Latency.Sampling)
   393  				}
   394  				if oldReport != "" && oldReport != export.Latency.Results {
   395  					r.AddOK("changed service latency subject to %s", export.Latency.Results)
   396  					r.AddWarning("changed latency subject will break consumers of the report")
   397  				}
   398  			}
   399  		}
   400  
   401  		export.ResponseThreshold = p.responseThreshold
   402  
   403  		rt := jwt.ResponseType(p.responseType)
   404  		if old.ResponseType != rt {
   405  			export.ResponseType = rt
   406  			r.AddOK("changed response type to %s", p.responseType)
   407  		}
   408  	}
   409  
   410  	export.Description = p.description
   411  	if export.Description != old.Description {
   412  		r.AddOK(`changed description to %q`, p.description)
   413  	}
   414  
   415  	export.InfoURL = p.infoUrl
   416  	if export.InfoURL != old.InfoURL {
   417  		r.AddOK(`changed info url to %q`, p.infoUrl)
   418  	}
   419  
   420  	p.claim.Exports[p.index] = &export
   421  
   422  	var vr2 jwt.ValidationResults
   423  	if err := p.claim.Exports.Validate(&vr2); err != nil {
   424  		return nil, err
   425  	}
   426  	// filter out all the old validations
   427  	uvr := jwt.CreateValidationResults()
   428  	if len(vr.Issues) > 0 {
   429  		for _, nis := range vr.Issues {
   430  			for _, is := range vr2.Issues {
   431  				if nis.Description == is.Description {
   432  					continue
   433  				}
   434  			}
   435  			uvr.Add(nis)
   436  		}
   437  	} else {
   438  		uvr = &vr2
   439  	}
   440  	// fail validation
   441  	if len(uvr.Issues) > 0 {
   442  		return nil, errors.New(uvr.Issues[0].Error())
   443  	}
   444  
   445  	token, err := p.claim.Encode(p.signerKP)
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  
   450  	StoreAccountAndUpdateStatus(ctx, token, r)
   451  	if r.HasNoErrors() {
   452  		r.AddOK("edited %s export %q", export.Type, export.Name)
   453  	}
   454  	return r, err
   455  }