github.com/fnproject/cli@v0.0.0-20240508150455-e5d88bd86117/objects/trigger/triggers.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 trigger
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"strings"
    26  	"text/tabwriter"
    27  
    28  	"github.com/fnproject/fn_go/clientv2/fns"
    29  
    30  	"github.com/jmoiron/jsonq"
    31  
    32  	"github.com/fnproject/cli/client"
    33  	"github.com/fnproject/cli/common"
    34  	"github.com/fnproject/cli/objects/app"
    35  	"github.com/fnproject/cli/objects/fn"
    36  	fnclient "github.com/fnproject/fn_go/clientv2"
    37  	apiTriggers "github.com/fnproject/fn_go/clientv2/triggers"
    38  	models "github.com/fnproject/fn_go/modelsv2"
    39  	"github.com/fnproject/fn_go/provider"
    40  	"github.com/urfave/cli"
    41  )
    42  
    43  type triggersCmd struct {
    44  	provider provider.Provider
    45  	client   *fnclient.Fn
    46  }
    47  
    48  // TriggerFlags used to create/update triggers
    49  var TriggerFlags = []cli.Flag{
    50  	cli.StringFlag{
    51  		Name:  "source,s",
    52  		Usage: "trigger source",
    53  	},
    54  	cli.StringFlag{
    55  		Name:  "type, t",
    56  		Usage: "Todo",
    57  	},
    58  	cli.StringSliceFlag{
    59  		Name:  "annotation",
    60  		Usage: "fn annotation (can be specified multiple times)",
    61  	},
    62  }
    63  
    64  func (t *triggersCmd) create(c *cli.Context) error {
    65  	appName := c.Args().Get(0)
    66  	fnName := c.Args().Get(1)
    67  	triggerName := c.Args().Get(2)
    68  
    69  	app, err := app.GetAppByName(t.client, appName)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	fn, err := fn.GetFnByName(t.client, app.ID, fnName)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	trigger := &models.Trigger{
    80  		AppID: app.ID,
    81  		FnID:  fn.ID,
    82  	}
    83  
    84  	trigger.Name = triggerName
    85  
    86  	if triggerType := c.String("type"); triggerType != "" {
    87  		trigger.Type = triggerType
    88  	}
    89  
    90  	if triggerSource := c.String("source"); triggerSource != "" {
    91  		trigger.Source = validateTriggerSource(triggerSource)
    92  	}
    93  
    94  	WithFlags(c, trigger)
    95  
    96  	if trigger.Name == "" {
    97  		return errors.New("triggerName path is missing")
    98  	}
    99  
   100  	return CreateTrigger(t.client, trigger)
   101  }
   102  
   103  func validateTriggerSource(ts string) string {
   104  	if !strings.HasPrefix(ts, "/") {
   105  		ts = "/" + ts
   106  	}
   107  	return ts
   108  }
   109  
   110  // CreateTrigger request
   111  func CreateTrigger(client *fnclient.Fn, trigger *models.Trigger) error {
   112  	resp, err := client.Triggers.CreateTrigger(&apiTriggers.CreateTriggerParams{
   113  		Context: context.Background(),
   114  		Body:    trigger,
   115  	})
   116  
   117  	if err != nil {
   118  		switch e := err.(type) {
   119  		case *apiTriggers.CreateTriggerBadRequest:
   120  			fmt.Println(e)
   121  			return fmt.Errorf("%s", e.Payload.Message)
   122  		case *apiTriggers.CreateTriggerConflict:
   123  			return fmt.Errorf("%s", e.Payload.Message)
   124  		default:
   125  			return err
   126  		}
   127  	}
   128  
   129  	fmt.Println("Successfully created trigger:", resp.Payload.Name)
   130  	endpoint := resp.Payload.Annotations["fnproject.io/trigger/httpEndpoint"]
   131  	fmt.Println("Trigger Endpoint:", endpoint)
   132  
   133  	return nil
   134  }
   135  
   136  func (t *triggersCmd) list(c *cli.Context) error {
   137  	resTriggers, err := getTriggers(c, t.client)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	fnName := c.Args().Get(1)
   142  	w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
   143  	if len(fnName) != 0 {
   144  
   145  		fmt.Fprint(w, "NAME", "\t", "ID", "\t", "TYPE", "\t", "SOURCE", "\t", "ENDPOINT", "\n")
   146  		for _, trigger := range resTriggers {
   147  			endpoint := trigger.Annotations["fnproject.io/trigger/httpEndpoint"]
   148  			fmt.Fprint(w, trigger.Name, "\t", trigger.ID, "\t", trigger.Type, "\t", trigger.Source, "\t", endpoint, "\n")
   149  		}
   150  	} else {
   151  		fmt.Fprint(w, "FUNCTION", "\t", "NAME", "\t", "ID", "\t", "TYPE", "\t", "SOURCE", "\t", "ENDPOINT", "\n")
   152  		for _, trigger := range resTriggers {
   153  			endpoint := trigger.Annotations["fnproject.io/trigger/httpEndpoint"]
   154  
   155  			resp, err := t.client.Fns.GetFn(&fns.GetFnParams{
   156  				FnID:    trigger.FnID,
   157  				Context: context.Background(),
   158  			})
   159  			if err != nil {
   160  				return err
   161  			}
   162  			fnName = resp.Payload.Name
   163  			fmt.Fprint(w, fnName, "\t", trigger.Name, "\t", trigger.ID, "\t", trigger.Type, "\t", trigger.Source, "\t", endpoint, "\n")
   164  		}
   165  	}
   166  	w.Flush()
   167  	return nil
   168  }
   169  
   170  func getTriggers(c *cli.Context, client *fnclient.Fn) ([]*models.Trigger, error) {
   171  	appName := c.Args().Get(0)
   172  	fnName := c.Args().Get(1)
   173  	var params *apiTriggers.ListTriggersParams
   174  
   175  	app, err := app.GetAppByName(client, appName)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	if len(fnName) == 0 {
   181  		params = &apiTriggers.ListTriggersParams{
   182  			Context: context.Background(),
   183  			AppID:   &app.ID,
   184  		}
   185  
   186  	} else {
   187  
   188  		fn, err := fn.GetFnByName(client, app.ID, fnName)
   189  		if err != nil {
   190  			return nil, err
   191  		}
   192  		params = &apiTriggers.ListTriggersParams{
   193  			Context: context.Background(),
   194  			AppID:   &app.ID,
   195  			FnID:    &fn.ID,
   196  		}
   197  	}
   198  
   199  	var resTriggers []*models.Trigger
   200  	for {
   201  
   202  		resp, err := client.Triggers.ListTriggers(params)
   203  		if err != nil {
   204  			return nil, err
   205  		}
   206  		n := c.Int64("n")
   207  
   208  		resTriggers = append(resTriggers, resp.Payload.Items...)
   209  		howManyMore := n - int64(len(resTriggers)+len(resp.Payload.Items))
   210  		if howManyMore <= 0 || resp.Payload.NextCursor == "" {
   211  			break
   212  		}
   213  
   214  		params.Cursor = &resp.Payload.NextCursor
   215  	}
   216  
   217  	if len(resTriggers) == 0 {
   218  		if len(fnName) == 0 {
   219  			return nil, fmt.Errorf("no triggers found for app: %s", appName)
   220  		}
   221  		return nil, fmt.Errorf("no triggers found for function: %s", fnName)
   222  	}
   223  	return resTriggers, nil
   224  }
   225  
   226  // BashCompleteTriggers can be called from a BashComplete function
   227  // to provide function completion suggestions (Assumes the
   228  // current context already contains an app name and a function name
   229  // as the first 2 arguments. This should be confirmed before calling this)
   230  func BashCompleteTriggers(c *cli.Context) {
   231  	provider, err := client.CurrentProvider()
   232  	if err != nil {
   233  		return
   234  	}
   235  	resp, err := getTriggers(c, provider.APIClientv2())
   236  	if err != nil {
   237  		return
   238  	}
   239  	for _, t := range resp {
   240  		fmt.Println(t.Name)
   241  	}
   242  }
   243  
   244  func (t *triggersCmd) update(c *cli.Context) error {
   245  	appName := c.Args().Get(0)
   246  	fnName := c.Args().Get(1)
   247  	triggerName := c.Args().Get(2)
   248  
   249  	trigger, err := GetTrigger(t.client, appName, fnName, triggerName)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	WithFlags(c, trigger)
   255  
   256  	err = PutTrigger(t.client, trigger)
   257  	if err != nil {
   258  		return err
   259  	}
   260  
   261  	fmt.Println(appName, fnName, triggerName, "updated")
   262  	return nil
   263  }
   264  
   265  // PutTrigger updates the provided trigger with new values
   266  func PutTrigger(t *fnclient.Fn, trigger *models.Trigger) error {
   267  	_, err := t.Triggers.UpdateTrigger(&apiTriggers.UpdateTriggerParams{
   268  		Context:   context.Background(),
   269  		TriggerID: trigger.ID,
   270  		Body:      trigger,
   271  	})
   272  
   273  	if err != nil {
   274  		switch e := err.(type) {
   275  		case *apiTriggers.UpdateTriggerBadRequest:
   276  			return fmt.Errorf("%s", e.Payload.Message)
   277  		default:
   278  			return err
   279  		}
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  func (t *triggersCmd) inspect(c *cli.Context) error {
   286  	appName := c.Args().Get(0)
   287  	fnName := c.Args().Get(1)
   288  	triggerName := c.Args().Get(2)
   289  	prop := c.Args().Get(3)
   290  
   291  	trigger, err := GetTrigger(t.client, appName, fnName, triggerName)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	if c.Bool("endpoint") {
   297  		endpoint, ok := trigger.Annotations["fnproject.io/trigger/httpEndpoint"].(string)
   298  		if !ok {
   299  			return errors.New("missing or invalid http endpoint on trigger")
   300  		}
   301  		fmt.Println(endpoint)
   302  		return nil
   303  	}
   304  
   305  	enc := json.NewEncoder(os.Stdout)
   306  	enc.SetIndent("", "\t")
   307  
   308  	if prop == "" {
   309  		enc.Encode(trigger)
   310  		return nil
   311  	}
   312  
   313  	data, err := json.Marshal(trigger)
   314  	if err != nil {
   315  		return fmt.Errorf("failed to inspect %s: %s", triggerName, err)
   316  	}
   317  
   318  	var inspect map[string]interface{}
   319  	err = json.Unmarshal(data, &inspect)
   320  	if err != nil {
   321  		return fmt.Errorf("failed to inspect %s: %s", triggerName, err)
   322  	}
   323  
   324  	jq := jsonq.NewQuery(inspect)
   325  	field, err := jq.Interface(strings.Split(prop, ".")...)
   326  	if err != nil {
   327  		return errors.New("failed to inspect %s field names")
   328  	}
   329  	enc.Encode(field)
   330  
   331  	return nil
   332  }
   333  
   334  func (t *triggersCmd) delete(c *cli.Context) error {
   335  	appName := c.Args().Get(0)
   336  	fnName := c.Args().Get(1)
   337  	triggerName := c.Args().Get(2)
   338  
   339  	trigger, err := GetTrigger(t.client, appName, fnName, triggerName)
   340  	if err != nil {
   341  		return err
   342  	}
   343  
   344  	params := apiTriggers.NewDeleteTriggerParams()
   345  	params.TriggerID = trigger.ID
   346  
   347  	_, err = t.client.Triggers.DeleteTrigger(params)
   348  	if err != nil {
   349  		return err
   350  	}
   351  
   352  	fmt.Println(appName, fnName, triggerName, "deleted")
   353  	return nil
   354  }
   355  
   356  // GetTrigger looks up a trigger using the provided client by app, function and trigger name
   357  func GetTrigger(client *fnclient.Fn, appName, fnName, triggerName string) (*models.Trigger, error) {
   358  	app, err := app.GetAppByName(client, appName)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	fn, err := fn.GetFnByName(client, app.ID, fnName)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  
   368  	trigger, err := GetTriggerByName(client, app.ID, fn.ID, triggerName)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  
   373  	return trigger, nil
   374  }
   375  
   376  // GetTriggerByAppFnAndTriggerNames looks up a trigger using app, fn and trigger names
   377  func GetTriggerByAppFnAndTriggerNames(appName, fnName, triggerName string) (*models.Trigger, error) {
   378  	provider, err := client.CurrentProvider()
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  	client := provider.APIClientv2()
   383  	return GetTrigger(client, appName, fnName, triggerName)
   384  }
   385  
   386  // GetTriggerByName looks up a trigger using the provided client by app and function ID and trigger name
   387  func GetTriggerByName(client *fnclient.Fn, appID string, fnID string, triggerName string) (*models.Trigger, error) {
   388  	triggerList, err := client.Triggers.ListTriggers(&apiTriggers.ListTriggersParams{
   389  		Context: context.Background(),
   390  		AppID:   &appID,
   391  		FnID:    &fnID,
   392  		Name:    &triggerName,
   393  	})
   394  
   395  	if err != nil {
   396  		return nil, err
   397  	}
   398  	if len(triggerList.Payload.Items) == 0 {
   399  		return nil, NameNotFoundError{triggerName}
   400  	}
   401  
   402  	return triggerList.Payload.Items[0], nil
   403  }
   404  
   405  // WithFlags returns a trigger with the specified flags
   406  func WithFlags(c *cli.Context, t *models.Trigger) {
   407  	if len(c.StringSlice("annotation")) > 0 {
   408  		t.Annotations = common.ExtractAnnotations(c)
   409  	}
   410  }
   411  
   412  // NameNotFoundError error for app not found when looked up by name
   413  type NameNotFoundError struct {
   414  	Name string
   415  }
   416  
   417  func (n NameNotFoundError) Error() string {
   418  	return fmt.Sprintf("trigger %s not found", n.Name)
   419  }