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, ¶ms) 39 }, 40 } 41 42 cmd.Flags().StringVarP(¶ms.name, "name", "n", "", "export name") 43 cmd.Flags().StringVarP(¶ms.subject, "subject", "s", "", "subject") 44 cmd.Flags().BoolVarP(¶ms.service, "service", "r", false, "export type service") 45 cmd.Flags().BoolVarP(¶ms.private, "private", "p", false, "private export - requires an activation to access") 46 cmd.Flags().StringVarP(¶ms.latSubject, "latency", "", "", "latency metrics subject (services only)") 47 cmd.Flags().StringVarP(¶ms.latSampling, "sampling", "", "", "latency sampling percentage [1-100] or `header` - 0 disables it (services only)") 48 cmd.Flags().BoolVarP(¶ms.rmLatencySampling, "rm-latency-sampling", "", false, "remove latency sampling") 49 cmd.Flags().StringVarP(¶ms.description, "description", "", "", "Description for this export") 50 cmd.Flags().StringVarP(¶ms.infoUrl, "info-url", "", "", "Link for more info on this export") 51 cmd.Flags().DurationVarP(¶ms.responseThreshold, "response-threshold", "", 0, "response threshold duration (units ms/s/m/h) (services only)") 52 cmd.Flags().BoolVarP(¶ms.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(¶ms.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 }