github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/addexport.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  	"strconv"
    22  	"strings"
    23  	"time"
    24  
    25  	cli "github.com/nats-io/cliprompts/v2"
    26  	"github.com/nats-io/jwt/v2"
    27  	"github.com/nats-io/nkeys"
    28  	"github.com/nats-io/nsc/v2/cmd/store"
    29  	"github.com/spf13/cobra"
    30  )
    31  
    32  func createAddExportCmd() *cobra.Command {
    33  	var params AddExportParams
    34  	cmd := &cobra.Command{
    35  		Use:          "export",
    36  		Short:        "Add an export",
    37  		Args:         MaxArgs(0),
    38  		Example:      params.longHelp(),
    39  		SilenceUsage: true,
    40  		RunE: func(cmd *cobra.Command, args []string) error {
    41  			return RunAction(cmd, args, &params)
    42  		},
    43  	}
    44  	cmd.Flags().StringVarP(&params.export.Name, "name", "n", "", "export name")
    45  	cmd.Flags().StringVarP(&params.subject, "subject", "s", "", "subject")
    46  	cmd.Flags().BoolVarP(&params.service, "service", "r", false, "export type service")
    47  	cmd.Flags().BoolVarP(&params.private, "private", "p", false, "private export - requires an activation to access")
    48  	cmd.Flags().StringVarP(&params.latSubject, "latency", "", "", "latency metrics subject (services only)")
    49  	cmd.Flags().StringVarP(&params.latSampling, "sampling", "", "", "latency sampling percentage [1-100] or `header`  (services only)")
    50  	cmd.Flags().DurationVarP(&params.responseThreshold, "response-threshold", "", 0, "response threshold duration (units ms/s/m/h) (services only)")
    51  	hm := fmt.Sprintf("response type for the service [%s | %s | %s] (services only)", jwt.ResponseTypeSingleton, jwt.ResponseTypeStream, jwt.ResponseTypeChunked)
    52  	cmd.Flags().StringVarP(&params.responseType, "response-type", "", jwt.ResponseTypeSingleton, hm)
    53  	params.AccountContextParams.BindFlags(cmd)
    54  
    55  	cmd.Flags().BoolVarP(&params.allowTrace, "allow-trace", "", false, "allow trace requests")
    56  
    57  	cmd.Flags().UintVarP(&params.accountTokenPosition, "account-token-position", "", 0, "subject token position where account is expected (public exports only)")
    58  	cmd.Flags().BoolVarP(&params.advertise, "advertise", "", false, "advertise export")
    59  	cmd.Flag("advertise").Hidden = true
    60  
    61  	return cmd
    62  }
    63  
    64  func init() {
    65  	addCmd.AddCommand(createAddExportCmd())
    66  }
    67  
    68  type AddExportParams struct {
    69  	AccountContextParams
    70  	SignerParams
    71  	claim                *jwt.AccountClaims
    72  	export               jwt.Export
    73  	private              bool
    74  	service              bool
    75  	subject              string
    76  	latSubject           string
    77  	latSampling          string
    78  	responseType         string
    79  	responseThreshold    time.Duration
    80  	accountTokenPosition uint
    81  	advertise            bool
    82  	allowTrace           bool
    83  }
    84  
    85  func (p *AddExportParams) longHelp() string {
    86  	s := `toolName add export -i
    87  toolName add export --subject "a.b.c.>"
    88  toolName add export --service --subject a.b
    89  toolName add export --name myexport --subject a.b --service`
    90  	return strings.Replace(s, "toolName", GetToolName(), -1)
    91  }
    92  
    93  func (p *AddExportParams) SetDefaults(ctx ActionCtx) error {
    94  	if err := p.AccountContextParams.SetDefaults(ctx); err != nil {
    95  		return err
    96  	}
    97  	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, true, ctx)
    98  
    99  	p.export.TokenReq = p.private
   100  	p.export.AccountTokenPosition = p.accountTokenPosition
   101  	p.export.Advertise = p.advertise
   102  	p.export.Subject = jwt.Subject(p.subject)
   103  	p.export.Type = jwt.Stream
   104  	if p.service {
   105  		p.export.Type = jwt.Service
   106  		p.export.ResponseType = jwt.ResponseType(p.responseType)
   107  	}
   108  
   109  	if p.export.Name == "" {
   110  		p.export.Name = p.subject
   111  	}
   112  
   113  	p.export.AllowTrace = p.allowTrace
   114  
   115  	return nil
   116  }
   117  
   118  func (p *AddExportParams) PreInteractive(ctx ActionCtx) error {
   119  	var err error
   120  
   121  	if err = p.AccountContextParams.Edit(ctx); err != nil {
   122  		return err
   123  	}
   124  
   125  	choices := []string{jwt.Stream.String(), jwt.Service.String()}
   126  	i, err := cli.Select("export type", p.export.Type.String(), choices)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	if i == 0 {
   131  		p.export.Type = jwt.Stream
   132  	} else {
   133  		p.export.Type = jwt.Service
   134  	}
   135  
   136  	svFn := func(s string) error {
   137  		p.export.Subject = jwt.Subject(s)
   138  		var vr jwt.ValidationResults
   139  		p.export.Validate(&vr)
   140  		if len(vr.Issues) > 0 {
   141  			return errors.New(vr.Issues[0].Description)
   142  		}
   143  		return nil
   144  	}
   145  
   146  	p.subject, err = cli.Prompt("subject", p.subject, cli.Val(svFn))
   147  	if err != nil {
   148  		return err
   149  	}
   150  	p.export.Subject = jwt.Subject(p.subject)
   151  
   152  	if p.export.Name == "" {
   153  		p.export.Name = p.subject
   154  	}
   155  
   156  	p.export.Name, err = cli.Prompt("name", p.export.Name, cli.NewLengthValidator(1))
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	p.export.TokenReq, err = cli.Confirm(fmt.Sprintf("private %s", p.export.Type.String()), p.export.TokenReq)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	if p.export.IsService() {
   167  		ok, err := cli.Confirm("track service latency", false)
   168  		if err != nil {
   169  			return err
   170  		}
   171  		if ok {
   172  			samp, err := cli.Prompt("sampling percentage [1-100] or `header`", "", cli.Val(SamplingValidator))
   173  			if err != nil {
   174  				return err
   175  			}
   176  			p.latSampling = samp
   177  
   178  			p.latSubject, err = cli.Prompt("latency metrics subject", "", cli.Val(LatencyMetricsSubjectValidator))
   179  			if err != nil {
   180  				return err
   181  			}
   182  		}
   183  
   184  		choices := []string{jwt.ResponseTypeSingleton, jwt.ResponseTypeStream, jwt.ResponseTypeChunked}
   185  		s, err := cli.Select("service response type", string(p.export.ResponseType), choices)
   186  		if err != nil {
   187  			return err
   188  		}
   189  		p.export.ResponseType = jwt.ResponseType(choices[s])
   190  
   191  		p.export.ResponseThreshold, err = promptDuration("response threshold (0 disabled)", p.responseThreshold)
   192  		if err != nil {
   193  			return err
   194  		}
   195  
   196  		ok, err = cli.Confirm("allow tracing", false)
   197  		if err != nil {
   198  			return err
   199  		}
   200  		p.export.AllowTrace = ok
   201  	}
   202  
   203  	if err := p.SignerParams.Edit(ctx); err != nil {
   204  		return err
   205  	}
   206  
   207  	return nil
   208  }
   209  
   210  func SamplingValidator(s string) error {
   211  	if strings.ToLower(s) == "header" {
   212  		return nil
   213  	}
   214  	v, err := strconv.Atoi(s)
   215  	if err != nil {
   216  		return err
   217  	}
   218  	if v < 1 || v > 100 {
   219  		return errors.New("sampling must be between 1 and 100 inclusive")
   220  	}
   221  	return nil
   222  }
   223  
   224  func latSamplingRate(latSampling string) jwt.SamplingRate {
   225  	samp := 0
   226  	if strings.ToLower(latSampling) == "header" {
   227  		samp = int(jwt.Headers)
   228  	} else {
   229  		// cannot fail
   230  		samp, _ = strconv.Atoi(latSampling)
   231  	}
   232  	return jwt.SamplingRate(samp)
   233  }
   234  
   235  func latSamplingRateToString(rate jwt.SamplingRate) string {
   236  	if rate == jwt.Headers {
   237  		return "header"
   238  	} else {
   239  		return fmt.Sprintf("%d", rate)
   240  	}
   241  }
   242  
   243  func LatencyMetricsSubjectValidator(s string) error {
   244  	var lat jwt.ServiceLatency
   245  	// bogus freq just to get a value into the validation
   246  	lat.Sampling = 100
   247  	lat.Results = jwt.Subject(s)
   248  	var vr jwt.ValidationResults
   249  	lat.Validate(&vr)
   250  	if len(vr.Issues) > 0 {
   251  		return errors.New(vr.Issues[0].Description)
   252  	}
   253  	return nil
   254  }
   255  
   256  func (p *AddExportParams) Load(ctx ActionCtx) error {
   257  	var err error
   258  
   259  	if err = p.AccountContextParams.Validate(ctx); err != nil {
   260  		return err
   261  	}
   262  
   263  	p.claim, err = ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name)
   264  	if err != nil {
   265  		return err
   266  	}
   267  
   268  	return nil
   269  }
   270  
   271  func (p *AddExportParams) PostInteractive(_ ActionCtx) error {
   272  	return nil
   273  }
   274  
   275  func (p *AddExportParams) Validate(ctx ActionCtx) error {
   276  	var err error
   277  	if p.subject == "" {
   278  		ctx.CurrentCmd().SilenceUsage = false
   279  		return errors.New("a subject is required")
   280  	}
   281  	if p.private && p.accountTokenPosition != 0 {
   282  		return errors.New("account token position is only valid for public exports")
   283  	}
   284  	// get the old validation results
   285  	var vr jwt.ValidationResults
   286  	if err = p.claim.Exports.Validate(&vr); err != nil {
   287  		return err
   288  	}
   289  
   290  	// if we have a latency report subject create it
   291  	if p.latSubject != "" {
   292  		p.export.Latency = &jwt.ServiceLatency{Results: jwt.Subject(p.latSubject), Sampling: latSamplingRate(p.latSampling)}
   293  	}
   294  
   295  	// add the new export
   296  	p.claim.Exports.Add(&p.export)
   297  
   298  	var vr2 jwt.ValidationResults
   299  	if err = p.claim.Exports.Validate(&vr2); err != nil {
   300  		return err
   301  	}
   302  
   303  	// filter out all the old validations
   304  	uvr := jwt.CreateValidationResults()
   305  	if len(vr.Issues) > 0 {
   306  		for _, nis := range vr.Issues {
   307  			for _, is := range vr2.Issues {
   308  				if nis.Description == is.Description {
   309  					continue
   310  				}
   311  			}
   312  			uvr.Add(nis)
   313  		}
   314  	} else {
   315  		uvr = &vr2
   316  	}
   317  	// fail validation
   318  	if len(uvr.Issues) > 0 {
   319  		return errors.New(uvr.Issues[0].Error())
   320  	}
   321  
   322  	if p.service {
   323  		rt := jwt.ResponseType(p.responseType)
   324  		if rt != jwt.ResponseTypeSingleton &&
   325  			rt != jwt.ResponseTypeStream &&
   326  			rt != jwt.ResponseTypeChunked {
   327  			return fmt.Errorf("unknown response type %q", p.responseType)
   328  		}
   329  		p.export.ResponseType = rt
   330  		p.export.ResponseThreshold = p.responseThreshold
   331  	} else if ctx.AnySet("response-type") {
   332  		return errors.New("response type can only be specified in conjunction with service")
   333  	}
   334  
   335  	if err = p.SignerParams.Resolve(ctx); err != nil {
   336  		return err
   337  	}
   338  
   339  	return nil
   340  }
   341  
   342  func (p *AddExportParams) Run(ctx ActionCtx) (store.Status, error) {
   343  	token, err := p.claim.Encode(p.signerKP)
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	visibility := "public"
   349  	if p.export.TokenReq {
   350  		visibility = "private"
   351  	}
   352  	r := store.NewDetailedReport(false)
   353  	StoreAccountAndUpdateStatus(ctx, token, r)
   354  	if r.HasNoErrors() {
   355  		r.AddOK("added %s %s export %q", visibility, p.export.Type, p.export.Name)
   356  	}
   357  	return r, err
   358  }