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  }