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

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