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, ¶ms) 42 }, 43 } 44 cmd.Flags().StringVarP(¶ms.export.Name, "name", "n", "", "export name") 45 cmd.Flags().StringVarP(¶ms.subject, "subject", "s", "", "subject") 46 cmd.Flags().BoolVarP(¶ms.service, "service", "r", false, "export type service") 47 cmd.Flags().BoolVarP(¶ms.private, "private", "p", false, "private export - requires an activation to access") 48 cmd.Flags().StringVarP(¶ms.latSubject, "latency", "", "", "latency metrics subject (services only)") 49 cmd.Flags().StringVarP(¶ms.latSampling, "sampling", "", "", "latency sampling percentage [1-100] or `header` (services only)") 50 cmd.Flags().DurationVarP(¶ms.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(¶ms.responseType, "response-type", "", jwt.ResponseTypeSingleton, hm) 53 params.AccountContextParams.BindFlags(cmd) 54 55 cmd.Flags().BoolVarP(¶ms.allowTrace, "allow-trace", "", false, "allow trace requests") 56 57 cmd.Flags().UintVarP(¶ms.accountTokenPosition, "account-token-position", "", 0, "subject token position where account is expected (public exports only)") 58 cmd.Flags().BoolVarP(¶ms.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 }