github.com/iron-io/functions@v0.0.0-20180820112432-d59d7d1c40b2/fn/commands/routes.go (about) 1 package commands 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "os" 12 "path" 13 "strconv" 14 "strings" 15 "text/tabwriter" 16 17 f_common "github.com/iron-io/functions/common" 18 image_commands "github.com/iron-io/functions/fn/commands/images" 19 "github.com/iron-io/functions/fn/common" 20 fnclient "github.com/iron-io/functions_go/client" 21 apiroutes "github.com/iron-io/functions_go/client/routes" 22 "github.com/iron-io/functions_go/models" 23 fnmodels "github.com/iron-io/functions_go/models" 24 "github.com/jmoiron/jsonq" 25 "github.com/urfave/cli" 26 ) 27 28 type routesCmd struct { 29 client *fnclient.Functions 30 } 31 32 var routeFlags = []cli.Flag{ 33 cli.StringFlag{ 34 Name: "image,i", 35 Usage: "image name", 36 }, 37 cli.Int64Flag{ 38 Name: "memory,m", 39 Usage: "memory in MiB", 40 }, 41 cli.StringFlag{ 42 Name: "type,t", 43 Usage: "route type - sync or async", 44 }, 45 cli.StringSliceFlag{ 46 Name: "config,c", 47 Usage: "route configuration", 48 }, 49 cli.StringSliceFlag{ 50 Name: "headers", 51 Usage: "route response headers", 52 }, 53 cli.StringFlag{ 54 Name: "format,f", 55 Usage: "hot container IO format - json or http", 56 }, 57 cli.IntFlag{ 58 Name: "max-concurrency,mc", 59 Usage: "maximum concurrency for hot container", 60 }, 61 cli.StringFlag{ 62 Name: "jwt-key,j", 63 Usage: "Signing key for JWT", 64 }, 65 cli.DurationFlag{ 66 Name: "timeout", 67 Usage: "route timeout (eg. 30s)", 68 }, 69 cli.DurationFlag{ 70 Name: "idle-timeout", 71 Usage: "hot func timeout (eg. 30s)", 72 }, 73 } 74 75 func Routes() cli.Command { 76 77 r := routesCmd{client: common.ApiClient()} 78 79 return cli.Command{ 80 Name: "routes", 81 Usage: "manage routes", 82 Subcommands: []cli.Command{ 83 { 84 Name: "call", 85 Usage: "call a route", 86 ArgsUsage: "<app> </path> [image]", 87 Action: r.call, 88 Flags: image_commands.Runflags(), 89 }, 90 { 91 Name: "list", 92 Aliases: []string{"l"}, 93 Usage: "list routes for `app`", 94 ArgsUsage: "<app>", 95 Action: r.list, 96 }, 97 { 98 Name: "create", 99 Aliases: []string{"c"}, 100 Usage: "create a route in an `app`", 101 ArgsUsage: "<app> </path>", 102 Action: r.create, 103 Flags: routeFlags, 104 }, 105 { 106 Name: "update", 107 Aliases: []string{"u"}, 108 Usage: "update a route in an `app`", 109 ArgsUsage: "<app> </path>", 110 Action: r.update, 111 Flags: routeFlags, 112 }, 113 { 114 Name: "config", 115 Usage: "operate a route configuration set", 116 Subcommands: []cli.Command{ 117 { 118 Name: "set", 119 Aliases: []string{"s"}, 120 Usage: "store a configuration key for this route", 121 ArgsUsage: "<app> </path> <key> <value>", 122 Action: r.configSet, 123 }, 124 { 125 Name: "unset", 126 Aliases: []string{"u"}, 127 Usage: "remove a configuration key for this route", 128 ArgsUsage: "<app> </path> <key>", 129 Action: r.configUnset, 130 }, 131 }, 132 }, 133 { 134 Name: "delete", 135 Aliases: []string{"d"}, 136 Usage: "delete a route from `app`", 137 ArgsUsage: "<app> </path>", 138 Action: r.delete, 139 }, 140 { 141 Name: "inspect", 142 Aliases: []string{"i"}, 143 Usage: "retrieve one or all routes properties", 144 ArgsUsage: "<app> </path> [property.[key]]", 145 Action: r.inspect, 146 }, 147 { 148 Name: "token", 149 Aliases: []string{"t"}, 150 Usage: "retrieve jwt for authentication", 151 ArgsUsage: "<app> </path> [expiration(sec)]", 152 Action: r.token, 153 }, 154 }, 155 } 156 } 157 158 func Call() cli.Command { 159 r := routesCmd{client: common.ApiClient()} 160 161 return cli.Command{ 162 Name: "call", 163 Usage: "call a remote function", 164 ArgsUsage: "<app> </path>", 165 Flags: image_commands.Runflags(), 166 Action: r.call, 167 } 168 } 169 170 func cleanRoutePath(p string) string { 171 p = path.Clean(p) 172 if !path.IsAbs(p) { 173 p = "/" + p 174 } 175 return p 176 } 177 178 func (a *routesCmd) list(c *cli.Context) error { 179 appName := c.Args().Get(0) 180 181 resp, err := a.client.Routes.GetAppsAppRoutes(&apiroutes.GetAppsAppRoutesParams{ 182 Context: context.Background(), 183 App: appName, 184 }) 185 186 if err != nil { 187 switch err.(type) { 188 case *apiroutes.GetAppsAppRoutesNotFound: 189 return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesNotFound).Payload.Error.Message) 190 case *apiroutes.GetAppsAppRoutesDefault: 191 return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesDefault).Payload.Error.Message) 192 } 193 return fmt.Errorf("unexpected error: %s", err) 194 } 195 196 w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0) 197 fmt.Fprint(w, "path", "\t", "image", "\t", "endpoint", "\n") 198 for _, route := range resp.Payload.Routes { 199 u, err := url.Parse("../") 200 u.Path = path.Join(u.Path, "r", appName, route.Path) 201 if err != nil { 202 return fmt.Errorf("error parsing functions route path: %s", err) 203 } 204 205 fmt.Fprint(w, route.Path, "\t", route.Image, "\n") 206 } 207 w.Flush() 208 209 return nil 210 } 211 212 func (a *routesCmd) call(c *cli.Context) error { 213 appName := c.Args().Get(0) 214 route := cleanRoutePath(c.Args().Get(1)) 215 216 u := url.URL{ 217 Scheme: common.SCHEME, 218 Host: common.HOST, 219 } 220 u.Path = path.Join(u.Path, "r", appName, route) 221 content := image_commands.Stdin() 222 223 resp, err := a.client.Routes.GetAppsAppRoutesRoute(&apiroutes.GetAppsAppRoutesRouteParams{ 224 Context: context.Background(), 225 App: appName, 226 Route: route, 227 }) 228 229 if err != nil { 230 switch err.(type) { 231 case *apiroutes.GetAppsAppRoutesRouteNotFound: 232 return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesRouteNotFound).Payload.Error.Message) 233 case *apiroutes.GetAppsAppRoutesRouteDefault: 234 return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesRouteDefault).Payload.Error.Message) 235 } 236 return fmt.Errorf("unexpected error: %s", err) 237 } 238 239 rt := resp.Payload.Route 240 241 return callfn(u.String(), rt, content, os.Stdout, c.String("method"), c.StringSlice("e")) 242 } 243 244 func callfn(u string, rt *models.Route, content io.Reader, output io.Writer, method string, env []string) error { 245 if method == "" { 246 if content == nil { 247 method = "GET" 248 } else { 249 method = "POST" 250 } 251 } 252 253 req, err := http.NewRequest(method, u, content) 254 if err != nil { 255 return fmt.Errorf("error running route: %s", err) 256 } 257 258 req.Header.Set("Content-Type", "application/json") 259 260 if len(env) > 0 { 261 envAsHeader(req, env) 262 } 263 264 if rt.JwtKey != "" { 265 ss, err := f_common.GetJwt(rt.JwtKey, 60*60) 266 if err != nil { 267 return fmt.Errorf("unexpected error: %s", err) 268 } 269 req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", ss)) 270 } 271 272 resp, err := http.DefaultClient.Do(req) 273 if err != nil { 274 return fmt.Errorf("error running route: %s", err) 275 } 276 277 io.Copy(output, resp.Body) 278 279 return nil 280 } 281 282 func envAsHeader(req *http.Request, selectedEnv []string) { 283 detectedEnv := os.Environ() 284 if len(selectedEnv) > 0 { 285 detectedEnv = selectedEnv 286 } 287 288 for _, e := range detectedEnv { 289 kv := strings.Split(e, "=") 290 name := kv[0] 291 req.Header.Set(name, os.Getenv(name)) 292 } 293 } 294 295 func routeWithFlags(c *cli.Context, rt *models.Route) { 296 if i := c.String("image"); i != "" { 297 rt.Image = i 298 } 299 300 if f := c.String("format"); f != "" { 301 rt.Format = f 302 } 303 304 if t := c.String("type"); t != "" { 305 rt.Type = t 306 } 307 308 if m := c.Int("max-concurrency"); m > 0 { 309 rt.MaxConcurrency = int32(m) 310 } 311 312 if m := c.Int64("memory"); m > 0 { 313 rt.Memory = m 314 } 315 316 if t := c.Duration("timeout"); t > 0 { 317 to := int64(t.Seconds()) 318 rt.Timeout = &to 319 } 320 321 if t := c.Duration("idle-timeout"); t > 0 { 322 to := int64(t.Seconds()) 323 rt.IDLETimeout = &to 324 } 325 326 if j := c.String("jwt-key"); j != "" { 327 rt.JwtKey = j 328 } 329 330 if len(c.StringSlice("headers")) > 0 { 331 headers := map[string][]string{} 332 for _, header := range c.StringSlice("headers") { 333 parts := strings.Split(header, "=") 334 headers[parts[0]] = strings.Split(parts[1], ";") 335 } 336 rt.Headers = headers 337 } 338 339 if len(c.StringSlice("config")) > 0 { 340 rt.Config = common.ExtractEnvConfig(c.StringSlice("config")) 341 } 342 } 343 344 func routeWithFuncFile(c *cli.Context, rt *models.Route) { 345 ff, err := common.LoadFuncfile() 346 if err == nil { 347 if ff.FullName() != "" { // args take precedence 348 rt.Image = ff.FullName() 349 } 350 if ff.Format != nil { 351 rt.Format = *ff.Format 352 } 353 if ff.MaxConcurrency != nil { 354 rt.MaxConcurrency = int32(*ff.MaxConcurrency) 355 } 356 if ff.Timeout != nil { 357 to := int64(ff.Timeout.Seconds()) 358 rt.Timeout = &to 359 } 360 if ff.IDLETimeout != nil { 361 to := int64(ff.IDLETimeout.Seconds()) 362 rt.IDLETimeout = &to 363 } 364 if ff.JwtKey != nil && *ff.JwtKey != "" { 365 rt.JwtKey = *ff.JwtKey 366 } 367 368 if rt.Path == "" && ff.Path != nil { 369 rt.Path = *ff.Path 370 } 371 } 372 } 373 374 func (a *routesCmd) create(c *cli.Context) error { 375 appName := c.Args().Get(0) 376 route := cleanRoutePath(c.Args().Get(1)) 377 378 rt := &models.Route{} 379 rt.Path = route 380 rt.Image = c.Args().Get(2) 381 382 routeWithFuncFile(c, rt) 383 routeWithFlags(c, rt) 384 385 if rt.Path == "" { 386 return errors.New("error: route path is missing") 387 } 388 if rt.Image == "" { 389 fmt.Println("No image specified, using `iron/hello`") 390 rt.Image = "iron/hello" 391 } 392 393 body := &models.RouteWrapper{ 394 Route: rt, 395 } 396 397 resp, err := a.client.Routes.PostAppsAppRoutes(&apiroutes.PostAppsAppRoutesParams{ 398 Context: context.Background(), 399 App: appName, 400 Body: body, 401 }) 402 403 if err != nil { 404 switch err.(type) { 405 case *apiroutes.PostAppsAppRoutesBadRequest: 406 return fmt.Errorf("error: %s", err.(*apiroutes.PostAppsAppRoutesBadRequest).Payload.Error.Message) 407 case *apiroutes.PostAppsAppRoutesConflict: 408 return fmt.Errorf("error: %s", err.(*apiroutes.PostAppsAppRoutesConflict).Payload.Error.Message) 409 case *apiroutes.PostAppsAppRoutesDefault: 410 return fmt.Errorf("unexpected error: %s", err.(*apiroutes.PostAppsAppRoutesDefault).Payload.Error.Message) 411 } 412 return fmt.Errorf("unexpected error: %s", err) 413 } 414 415 fmt.Println(resp.Payload.Route.Path, "created with", resp.Payload.Route.Image) 416 return nil 417 } 418 419 func (a *routesCmd) patchRoute(appName, routePath string, r *fnmodels.Route) error { 420 resp, err := a.client.Routes.GetAppsAppRoutesRoute(&apiroutes.GetAppsAppRoutesRouteParams{ 421 Context: context.Background(), 422 App: appName, 423 Route: routePath, 424 }) 425 426 if err != nil { 427 switch err.(type) { 428 case *apiroutes.GetAppsAppRoutesRouteNotFound: 429 return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesRouteNotFound).Payload.Error.Message) 430 case *apiroutes.GetAppsAppRoutesDefault: 431 return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesDefault).Payload.Error.Message) 432 } 433 return fmt.Errorf("unexpected error: %s", err) 434 } 435 436 if resp.Payload.Route.Config == nil { 437 resp.Payload.Route.Config = map[string]string{} 438 } 439 440 if resp.Payload.Route.Headers == nil { 441 resp.Payload.Route.Headers = map[string][]string{} 442 } 443 444 resp.Payload.Route.Path = "" 445 if r != nil { 446 if r.Config != nil { 447 for k, v := range r.Config { 448 if string(k[0]) == "-" { 449 delete(resp.Payload.Route.Config, string(k[1:])) 450 continue 451 } 452 resp.Payload.Route.Config[k] = v 453 } 454 } 455 if r.Headers != nil { 456 for k, v := range r.Headers { 457 if string(k[0]) == "-" { 458 delete(resp.Payload.Route.Headers, k) 459 continue 460 } 461 resp.Payload.Route.Headers[k] = v 462 } 463 } 464 if r.Image != "" { 465 resp.Payload.Route.Image = r.Image 466 } 467 if r.Format != "" { 468 resp.Payload.Route.Format = r.Format 469 } 470 if r.Type != "" { 471 resp.Payload.Route.Type = r.Type 472 } 473 if r.MaxConcurrency > 0 { 474 resp.Payload.Route.MaxConcurrency = r.MaxConcurrency 475 } 476 if r.Memory > 0 { 477 resp.Payload.Route.Memory = r.Memory 478 } 479 if r.Timeout != nil { 480 resp.Payload.Route.Timeout = r.Timeout 481 } 482 if r.IDLETimeout != nil { 483 resp.Payload.Route.IDLETimeout = r.IDLETimeout 484 } 485 if r.JwtKey != "" { 486 resp.Payload.Route.JwtKey = r.JwtKey 487 } 488 489 } 490 491 _, err = a.client.Routes.PatchAppsAppRoutesRoute(&apiroutes.PatchAppsAppRoutesRouteParams{ 492 Context: context.Background(), 493 App: appName, 494 Route: routePath, 495 Body: resp.Payload, 496 }) 497 498 if err != nil { 499 switch err.(type) { 500 case *apiroutes.PatchAppsAppRoutesRouteBadRequest: 501 return fmt.Errorf("error: %s", err.(*apiroutes.PatchAppsAppRoutesRouteBadRequest).Payload.Error.Message) 502 case *apiroutes.PatchAppsAppRoutesRouteNotFound: 503 return fmt.Errorf("error: %s", err.(*apiroutes.PatchAppsAppRoutesRouteNotFound).Payload.Error.Message) 504 case *apiroutes.PatchAppsAppRoutesRouteDefault: 505 return fmt.Errorf("unexpected error: %s", err.(*apiroutes.PatchAppsAppRoutesRouteDefault).Payload.Error.Message) 506 } 507 return fmt.Errorf("unexpected error: %s", err) 508 } 509 510 return nil 511 } 512 513 func (a *routesCmd) update(c *cli.Context) error { 514 appName := c.Args().Get(0) 515 route := cleanRoutePath(c.Args().Get(1)) 516 517 rt := &models.Route{} 518 routeWithFuncFile(c, rt) 519 routeWithFlags(c, rt) 520 521 err := a.patchRoute(appName, route, rt) 522 if err != nil { 523 return err 524 } 525 526 fmt.Println(appName, route, "updated") 527 return nil 528 } 529 530 func (a *routesCmd) configSet(c *cli.Context) error { 531 appName := c.Args().Get(0) 532 route := cleanRoutePath(c.Args().Get(1)) 533 key := c.Args().Get(2) 534 value := c.Args().Get(3) 535 536 patchRoute := fnmodels.Route{ 537 Config: make(map[string]string), 538 } 539 540 patchRoute.Config[key] = value 541 542 err := a.patchRoute(appName, route, &patchRoute) 543 if err != nil { 544 return err 545 } 546 547 fmt.Println(appName, route, "updated", key, "with", value) 548 return nil 549 } 550 551 func (a *routesCmd) configUnset(c *cli.Context) error { 552 appName := c.Args().Get(0) 553 route := cleanRoutePath(c.Args().Get(1)) 554 key := c.Args().Get(2) 555 556 patchRoute := fnmodels.Route{ 557 Config: make(map[string]string), 558 } 559 560 patchRoute.Config["-"+key] = "" 561 562 err := a.patchRoute(appName, route, &patchRoute) 563 if err != nil { 564 return err 565 } 566 567 fmt.Printf("removed key '%s' from the route '%s%s'", key, appName, key) 568 return nil 569 } 570 571 func (a *routesCmd) inspect(c *cli.Context) error { 572 appName := c.Args().Get(0) 573 route := cleanRoutePath(c.Args().Get(1)) 574 prop := c.Args().Get(2) 575 576 resp, err := a.client.Routes.GetAppsAppRoutesRoute(&apiroutes.GetAppsAppRoutesRouteParams{ 577 Context: context.Background(), 578 App: appName, 579 Route: route, 580 }) 581 582 if err != nil { 583 switch err.(type) { 584 case *apiroutes.GetAppsAppRoutesRouteNotFound: 585 return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesRouteNotFound).Payload.Error.Message) 586 case *apiroutes.GetAppsAppRoutesRouteDefault: 587 return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesRouteDefault).Payload.Error.Message) 588 } 589 return fmt.Errorf("unexpected error: %s", err) 590 } 591 592 enc := json.NewEncoder(os.Stdout) 593 enc.SetIndent("", "\t") 594 595 if prop == "" { 596 enc.Encode(resp.Payload.Route) 597 return nil 598 } 599 600 data, err := json.Marshal(resp.Payload.Route) 601 if err != nil { 602 return fmt.Errorf("failed to inspect route: %s", err) 603 } 604 var inspect map[string]interface{} 605 err = json.Unmarshal(data, &inspect) 606 if err != nil { 607 return fmt.Errorf("failed to inspect route: %s", err) 608 } 609 610 jq := jsonq.NewQuery(inspect) 611 field, err := jq.Interface(strings.Split(prop, ".")...) 612 if err != nil { 613 return errors.New("failed to inspect that route's field") 614 } 615 enc.Encode(field) 616 617 return nil 618 } 619 620 func (a *routesCmd) delete(c *cli.Context) error { 621 appName := c.Args().Get(0) 622 route := cleanRoutePath(c.Args().Get(1)) 623 624 _, err := a.client.Routes.DeleteAppsAppRoutesRoute(&apiroutes.DeleteAppsAppRoutesRouteParams{ 625 Context: context.Background(), 626 App: appName, 627 Route: route, 628 }) 629 if err != nil { 630 switch err.(type) { 631 case *apiroutes.DeleteAppsAppRoutesRouteNotFound: 632 return fmt.Errorf("error: %s", err.(*apiroutes.DeleteAppsAppRoutesRouteNotFound).Payload.Error.Message) 633 case *apiroutes.DeleteAppsAppRoutesRouteDefault: 634 return fmt.Errorf("unexpected error: %s", err.(*apiroutes.DeleteAppsAppRoutesRouteDefault).Payload.Error.Message) 635 } 636 return fmt.Errorf("unexpected error: %s", err) 637 } 638 639 fmt.Println(appName, route, "deleted") 640 return nil 641 } 642 643 func (a *routesCmd) token(c *cli.Context) error { 644 appName := c.Args().Get(0) 645 route := cleanRoutePath(c.Args().Get(1)) 646 e := c.Args().Get(2) 647 expiration := 60 * 60 648 if e != "" { 649 var err error 650 expiration, err = strconv.Atoi(e) 651 if err != nil { 652 return fmt.Errorf("invalid expiration: %s", err) 653 } 654 } 655 656 resp, err := a.client.Routes.GetAppsAppRoutesRoute(&apiroutes.GetAppsAppRoutesRouteParams{ 657 Context: context.Background(), 658 App: appName, 659 Route: route, 660 }) 661 662 if err != nil { 663 switch err.(type) { 664 case *apiroutes.GetAppsAppRoutesRouteNotFound: 665 return fmt.Errorf("error: %s", err.(*apiroutes.GetAppsAppRoutesRouteNotFound).Payload.Error.Message) 666 case *apiroutes.GetAppsAppRoutesRouteDefault: 667 return fmt.Errorf("unexpected error: %s", err.(*apiroutes.GetAppsAppRoutesRouteDefault).Payload.Error.Message) 668 } 669 return fmt.Errorf("unexpected error: %s", err) 670 } 671 672 enc := json.NewEncoder(os.Stdout) 673 enc.SetIndent("", "\t") 674 jwtKey := resp.Payload.Route.JwtKey 675 if jwtKey == "" { 676 return errors.New("Empty JWT Key") 677 } 678 679 // Create the Claims 680 ss, err := f_common.GetJwt(jwtKey, expiration) 681 if err != nil { 682 return fmt.Errorf("unexpected error: %s", err) 683 } 684 t := struct { 685 Token string `json:"token"` 686 }{Token: ss} 687 enc.Encode(t) 688 689 return nil 690 }