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