github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/editexport.go (about)

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