github.com/fnproject/cli@v0.0.0-20240508150455-e5d88bd86117/objects/fn/fns.go (about) 1 /* 2 * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package fn 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "os" 25 "path" 26 "strings" 27 "text/tabwriter" 28 29 client "github.com/fnproject/cli/client" 30 "github.com/fnproject/cli/common" 31 "github.com/fnproject/cli/objects/app" 32 fnclient "github.com/fnproject/fn_go/clientv2" 33 apifns "github.com/fnproject/fn_go/clientv2/fns" 34 "github.com/fnproject/fn_go/modelsv2" 35 models "github.com/fnproject/fn_go/modelsv2" 36 "github.com/fnproject/fn_go/provider" 37 "github.com/jmoiron/jsonq" 38 "github.com/urfave/cli" 39 ) 40 41 type fnsCmd struct { 42 provider provider.Provider 43 client *fnclient.Fn 44 } 45 46 // FnFlags used to create/update functions 47 var FnFlags = []cli.Flag{ 48 cli.Uint64Flag{ 49 Name: "memory,m", 50 Usage: "Memory in MiB", 51 }, 52 cli.StringSliceFlag{ 53 Name: "config,c", 54 Usage: "Function configuration", 55 }, 56 cli.IntFlag{ 57 Name: "timeout", 58 Usage: "Function timeout (eg. 30)", 59 }, 60 cli.IntFlag{ 61 Name: "idle-timeout", 62 Usage: "Function idle timeout (eg. 30)", 63 }, 64 cli.StringSliceFlag{ 65 Name: "annotation", 66 Usage: "Function annotation (can be specified multiple times)", 67 }, 68 cli.StringFlag{ 69 Name: "image", 70 Usage: "Function image", 71 }, 72 } 73 var updateFnFlags = FnFlags 74 75 // WithSlash appends "/" to function path 76 func WithSlash(p string) string { 77 p = path.Clean(p) 78 79 if !strings.HasPrefix(p, "/") { 80 p = "/" + p 81 } 82 return p 83 } 84 85 // WithoutSlash removes "/" from function path 86 func WithoutSlash(p string) string { 87 p = path.Clean(p) 88 p = strings.TrimPrefix(p, "/") 89 return p 90 } 91 92 func printFunctions(c *cli.Context, fns []*models.Fn) error { 93 outputFormat := strings.ToLower(c.String("output")) 94 if outputFormat == "json" { 95 var newFns []interface{} 96 for _, fn := range fns { 97 newFns = append(newFns, struct { 98 Name string `json:"name"` 99 Image string `json:"image"` 100 ID string `json:"id"` 101 }{ 102 fn.Name, 103 fn.Image, 104 fn.ID, 105 }) 106 } 107 b, err := json.MarshalIndent(newFns, "", " ") 108 if err != nil { 109 return err 110 } 111 fmt.Fprint(os.Stdout, string(b)) 112 } else { 113 w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) 114 fmt.Fprint(w, "NAME", "\t", "IMAGE", "\t", "ID", "\n") 115 116 for _, f := range fns { 117 fmt.Fprint(w, f.Name, "\t", f.Image, "\t", f.ID, "\t", "\n") 118 } 119 if err := w.Flush(); err != nil { 120 return err 121 } 122 } 123 return nil 124 } 125 126 func (f *fnsCmd) list(c *cli.Context) error { 127 resFns, err := getFns(c, f.client) 128 if err != nil { 129 return err 130 } 131 return printFunctions(c, resFns) 132 } 133 134 func getFns(c *cli.Context, client *fnclient.Fn) ([]*modelsv2.Fn, error) { 135 appName := c.Args().Get(0) 136 137 a, err := app.GetAppByName(client, appName) 138 if err != nil { 139 return nil, err 140 } 141 params := &apifns.ListFnsParams{ 142 Context: context.Background(), 143 AppID: &a.ID, 144 } 145 146 var resFns []*models.Fn 147 for { 148 resp, err := client.Fns.ListFns(params) 149 150 if err != nil { 151 return nil, err 152 } 153 n := c.Int64("n") 154 155 resFns = append(resFns, resp.Payload.Items...) 156 howManyMore := n - int64(len(resFns)+len(resp.Payload.Items)) 157 if howManyMore <= 0 || resp.Payload.NextCursor == "" { 158 break 159 } 160 161 params.Cursor = &resp.Payload.NextCursor 162 } 163 164 if len(resFns) == 0 { 165 return nil, fmt.Errorf("no functions found for app: %s", appName) 166 } 167 return resFns, nil 168 } 169 170 // BashCompleteFns can be called from a BashComplete function 171 // to provide function completion suggestions (Assumes the 172 // current context already contains an app name as an argument. 173 // This should be confirmed before calling this) 174 func BashCompleteFns(c *cli.Context) { 175 provider, err := client.CurrentProvider() 176 if err != nil { 177 return 178 } 179 resp, err := getFns(c, provider.APIClientv2()) 180 if err != nil { 181 return 182 } 183 for _, f := range resp { 184 fmt.Println(f.Name) 185 } 186 } 187 188 func getFnByAppAndFnName(appName, fnName string) (*models.Fn, error) { 189 provider, err := client.CurrentProvider() 190 if err != nil { 191 return nil, errors.New("could not get context") 192 } 193 app, err := app.GetAppByName(provider.APIClientv2(), appName) 194 if err != nil { 195 return nil, fmt.Errorf("could not get app %v", appName) 196 } 197 fn, err := GetFnByName(provider.APIClientv2(), app.ID, fnName) 198 if err != nil { 199 return nil, fmt.Errorf("could not get function %v", fnName) 200 } 201 return fn, nil 202 } 203 204 // WithFlags returns a function with specified flags 205 func WithFlags(c *cli.Context, fn *models.Fn) { 206 if i := c.String("image"); i != "" { 207 fn.Image = i 208 } 209 if m := c.Uint64("memory"); m > 0 { 210 fn.Memory = m 211 } 212 213 fn.Config = common.ExtractConfig(c.StringSlice("config")) 214 215 if len(c.StringSlice("annotation")) > 0 { 216 fn.Annotations = common.ExtractAnnotations(c) 217 } 218 if t := c.Int("timeout"); t > 0 { 219 to := int32(t) 220 fn.Timeout = &to 221 } 222 if t := c.Int("idle-timeout"); t > 0 { 223 to := int32(t) 224 fn.IdleTimeout = &to 225 } 226 } 227 228 // WithFuncFileV20180708 used when creating a function from a funcfile 229 func WithFuncFileV20180708(ff *common.FuncFileV20180708, fn *models.Fn) error { 230 var err error 231 if ff == nil { 232 _, ff, err = common.LoadFuncFileV20180708(".") 233 if err != nil { 234 return err 235 } 236 } 237 if ff.ImageNameV20180708() != "" { // args take precedence 238 fn.Image = ff.ImageNameV20180708() 239 } 240 if ff.Timeout != nil { 241 fn.Timeout = ff.Timeout 242 } 243 if ff.Memory != 0 { 244 fn.Memory = ff.Memory 245 } 246 if ff.IDLE_timeout != nil { 247 fn.IdleTimeout = ff.IDLE_timeout 248 } 249 250 if len(ff.Config) != 0 { 251 fn.Config = ff.Config 252 } 253 if len(ff.Annotations) != 0 { 254 fn.Annotations = ff.Annotations 255 } 256 // do something with triggers here 257 258 return nil 259 } 260 261 func (f *fnsCmd) create(c *cli.Context) error { 262 appName := c.Args().Get(0) 263 fnName := c.Args().Get(1) 264 265 fn := &models.Fn{} 266 fn.Name = fnName 267 fn.Image = c.Args().Get(2) 268 269 WithFlags(c, fn) 270 271 if fn.Name == "" { 272 return errors.New("fnName path is missing") 273 } 274 if fn.Image == "" { 275 return errors.New("no image specified") 276 } 277 278 a, err := app.GetAppByName(f.client, appName) 279 if err != nil { 280 return err 281 } 282 283 _, err = CreateFn(f.client, a.ID, fn) 284 return err 285 } 286 287 // CreateFn request 288 func CreateFn(r *fnclient.Fn, appID string, fn *models.Fn) (*models.Fn, error) { 289 fn.AppID = appID 290 err := common.ValidateTagImageName(fn.Image) 291 if err != nil { 292 return nil, err 293 } 294 295 resp, err := r.Fns.CreateFn(&apifns.CreateFnParams{ 296 Context: context.Background(), 297 Body: fn, 298 }) 299 300 if err != nil { 301 switch e := err.(type) { 302 case *apifns.CreateFnBadRequest: 303 err = fmt.Errorf("%s", e.Payload.Message) 304 case *apifns.CreateFnConflict: 305 err = fmt.Errorf("%s", e.Payload.Message) 306 } 307 return nil, err 308 } 309 310 fmt.Println("Successfully created function:", resp.Payload.Name, "with", resp.Payload.Image) 311 return resp.Payload, nil 312 } 313 314 // PutFn updates the fn with the given ID using the content of the provided fn 315 func PutFn(f *fnclient.Fn, fnID string, fn *models.Fn) error { 316 if fn.Image != "" { 317 err := common.ValidateTagImageName(fn.Image) 318 if err != nil { 319 return err 320 } 321 } 322 323 _, err := f.Fns.UpdateFn(&apifns.UpdateFnParams{ 324 Context: context.Background(), 325 FnID: fnID, 326 Body: fn, 327 }) 328 329 if err != nil { 330 switch e := err.(type) { 331 case *apifns.UpdateFnBadRequest: 332 return fmt.Errorf("%s", e.Payload.Message) 333 334 default: 335 return err 336 } 337 } 338 339 return nil 340 } 341 342 // NameNotFoundError error for app not found when looked up by name 343 type NameNotFoundError struct { 344 Name string 345 } 346 347 func (n NameNotFoundError) Error() string { 348 return fmt.Sprintf("function %s not found", n.Name) 349 } 350 351 // GetFnByName looks up a fn by name using the given client 352 func GetFnByName(client *fnclient.Fn, appID, fnName string) (*models.Fn, error) { 353 resp, err := client.Fns.ListFns(&apifns.ListFnsParams{ 354 Context: context.Background(), 355 AppID: &appID, 356 Name: &fnName, 357 }) 358 if err != nil { 359 return nil, err 360 } 361 362 var fn *models.Fn 363 for i := 0; i < len(resp.Payload.Items); i++ { 364 if resp.Payload.Items[i].Name == fnName { 365 fn = resp.Payload.Items[i] 366 } 367 } 368 if fn == nil { 369 return nil, NameNotFoundError{fnName} 370 } 371 372 return fn, nil 373 } 374 375 func (f *fnsCmd) update(c *cli.Context) error { 376 appName := c.Args().Get(0) 377 fnName := c.Args().Get(1) 378 379 app, err := app.GetAppByName(f.client, appName) 380 if err != nil { 381 return err 382 } 383 fn, err := GetFnByName(f.client, app.ID, fnName) 384 if err != nil { 385 return err 386 } 387 388 WithFlags(c, fn) 389 390 err = PutFn(f.client, fn.ID, fn) 391 if err != nil { 392 return err 393 } 394 395 fmt.Println(appName, fnName, "updated") 396 return nil 397 } 398 399 func (f *fnsCmd) setConfig(c *cli.Context) error { 400 appName := c.Args().Get(0) 401 fnName := WithoutSlash(c.Args().Get(1)) 402 key := c.Args().Get(2) 403 value := c.Args().Get(3) 404 405 app, err := app.GetAppByName(f.client, appName) 406 if err != nil { 407 return err 408 } 409 fn, err := GetFnByName(f.client, app.ID, fnName) 410 if err != nil { 411 return err 412 } 413 414 fn.Config = make(map[string]string) 415 fn.Config[key] = value 416 417 if err = PutFn(f.client, fn.ID, fn); err != nil { 418 return fmt.Errorf("Error updating function configuration: %v", err) 419 } 420 421 fmt.Println(appName, fnName, "updated", key, "with", value) 422 return nil 423 } 424 425 func (f *fnsCmd) getConfig(c *cli.Context) error { 426 appName := c.Args().Get(0) 427 fnName := c.Args().Get(1) 428 key := c.Args().Get(2) 429 430 app, err := app.GetAppByName(f.client, appName) 431 if err != nil { 432 return err 433 } 434 fn, err := GetFnByName(f.client, app.ID, fnName) 435 if err != nil { 436 return err 437 } 438 439 val, ok := fn.Config[key] 440 if !ok { 441 return fmt.Errorf("config key does not exist") 442 } 443 444 fmt.Println(val) 445 446 return nil 447 } 448 449 func (f *fnsCmd) listConfig(c *cli.Context) error { 450 appName := c.Args().Get(0) 451 fnName := c.Args().Get(1) 452 453 app, err := app.GetAppByName(f.client, appName) 454 if err != nil { 455 return err 456 } 457 fn, err := GetFnByName(f.client, app.ID, fnName) 458 if err != nil { 459 return err 460 } 461 462 if len(fn.Config) == 0 { 463 fmt.Fprintf(os.Stderr, "No config found for function: %s\n", fnName) 464 return nil 465 } 466 467 w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) 468 fmt.Fprint(w, "KEY", "\t", "VALUE", "\n") 469 for key, val := range fn.Config { 470 fmt.Fprint(w, key, "\t", val, "\n") 471 } 472 w.Flush() 473 474 return nil 475 } 476 477 func (f *fnsCmd) unsetConfig(c *cli.Context) error { 478 appName := c.Args().Get(0) 479 fnName := WithoutSlash(c.Args().Get(1)) 480 key := c.Args().Get(2) 481 482 app, err := app.GetAppByName(f.client, appName) 483 if err != nil { 484 return err 485 } 486 fn, err := GetFnByName(f.client, app.ID, fnName) 487 if err != nil { 488 return err 489 } 490 _, ok := fn.Config[key] 491 if !ok { 492 fmt.Printf("Config key '%s' does not exist. Nothing to do.\n", key) 493 return nil 494 } 495 fn.Config[key] = "" 496 497 err = PutFn(f.client, fn.ID, fn) 498 if err != nil { 499 return err 500 } 501 502 fmt.Printf("Removed key '%s' from the function '%s' \n", key, fnName) 503 return nil 504 } 505 506 func (f *fnsCmd) inspect(c *cli.Context) error { 507 appName := c.Args().Get(0) 508 fnName := WithoutSlash(c.Args().Get(1)) 509 prop := c.Args().Get(2) 510 511 app, err := app.GetAppByName(f.client, appName) 512 if err != nil { 513 return err 514 } 515 fn, err := GetFnByName(f.client, app.ID, fnName) 516 if err != nil { 517 return err 518 } 519 520 if c.Bool("endpoint") { 521 endpoint, ok := fn.Annotations["fnproject.io/fn/invokeEndpoint"].(string) 522 if !ok { 523 return errors.New("missing or invalid endpoint on function") 524 } 525 fmt.Println(endpoint) 526 return nil 527 } 528 529 enc := json.NewEncoder(os.Stdout) 530 enc.SetIndent("", "\t") 531 532 if prop == "" { 533 enc.Encode(fn) 534 return nil 535 } 536 537 data, err := json.Marshal(fn) 538 if err != nil { 539 return fmt.Errorf("failed to inspect %s: %s", fnName, err) 540 } 541 var inspect map[string]interface{} 542 err = json.Unmarshal(data, &inspect) 543 if err != nil { 544 return fmt.Errorf("failed to inspect %s: %s", fnName, err) 545 } 546 547 jq := jsonq.NewQuery(inspect) 548 field, err := jq.Interface(strings.Split(prop, ".")...) 549 if err != nil { 550 return errors.New("failed to inspect that function's field") 551 } 552 enc.Encode(field) 553 554 return nil 555 } 556 557 func (f *fnsCmd) delete(c *cli.Context) error { 558 appName := c.Args().Get(0) 559 fnName := c.Args().Get(1) 560 561 app, err := app.GetAppByName(f.client, appName) 562 if err != nil { 563 return err 564 } 565 fn, err := GetFnByName(f.client, app.ID, fnName) 566 if err != nil { 567 return err 568 } 569 570 //recursive delete of sub-objects 571 if c.Bool("recursive") { 572 triggers, err := common.ListTriggersInFunc(c, f.client, fn) 573 if err != nil { 574 return fmt.Errorf("Failed to get associated objects: %s", err) 575 } 576 577 //Forced delete 578 var shouldContinue bool 579 if c.Bool("force") { 580 shouldContinue = true 581 } else { 582 shouldContinue = common.UserConfirmedMultiResourceDeletion(nil, []*modelsv2.Fn{fn}, triggers) 583 } 584 585 if shouldContinue { 586 err := common.DeleteTriggers(c, f.client, triggers) 587 if err != nil { 588 return fmt.Errorf("Failed to delete associated objects: %s", err) 589 } 590 } else { 591 return nil 592 } 593 } 594 595 params := apifns.NewDeleteFnParams() 596 params.FnID = fn.ID 597 _, err = f.client.Fns.DeleteFn(params) 598 599 if err != nil { 600 return err 601 } 602 603 fmt.Println("Function", fnName, "deleted") 604 return nil 605 }