github.com/fnproject/cli@v0.0.0-20240508150455-e5d88bd86117/commands/invoke.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 commands
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"os"
    26  	"strings"
    27  
    28  	"errors"
    29  
    30  	"github.com/fnproject/cli/client"
    31  	"github.com/fnproject/cli/common"
    32  	"github.com/fnproject/cli/objects/app"
    33  	"github.com/fnproject/cli/objects/fn"
    34  	"github.com/fnproject/fn_go/clientv2"
    35  	"github.com/fnproject/fn_go/provider"
    36  	"github.com/urfave/cli"
    37  )
    38  
    39  // FnInvokeEndpointAnnotation is the annotation that exposes the fn invoke endpoint as defined in models/fn.go
    40  const (
    41  	FnInvokeEndpointAnnotation = "fnproject.io/fn/invokeEndpoint"
    42  	CallIDHeader               = "Fn-Call-Id"
    43  )
    44  
    45  type invokeCmd struct {
    46  	provider provider.Provider
    47  	client   *clientv2.Fn
    48  }
    49  
    50  // InvokeFnFlags used to invoke and fn
    51  var InvokeFnFlags = []cli.Flag{
    52  	cli.StringFlag{
    53  		Name:  "endpoint",
    54  		Usage: "Specify the function invoke endpoint for this function, the app-name and func-name parameters will be ignored",
    55  	},
    56  	cli.StringFlag{
    57  		Name:  "content-type",
    58  		Usage: "The payload Content-Type for the function invocation.",
    59  	},
    60  	cli.BoolFlag{
    61  		Name:  "display-call-id",
    62  		Usage: "whether display call ID or not",
    63  	},
    64  	cli.StringFlag{
    65  		Name:  "output",
    66  		Usage: "Output format (json)",
    67  	},
    68  }
    69  
    70  // InvokeCommand returns call cli.command
    71  func InvokeCommand() cli.Command {
    72  	cl := invokeCmd{}
    73  	return cli.Command{
    74  		Name:    "invoke",
    75  		Usage:   "\tInvoke a remote function",
    76  		Aliases: []string{"iv"},
    77  		Before: func(c *cli.Context) error {
    78  			var err error
    79  			cl.provider, err = client.CurrentProvider()
    80  			if err != nil {
    81  				return err
    82  			}
    83  			cl.client = cl.provider.APIClientv2()
    84  			return nil
    85  		},
    86  		ArgsUsage:   "[app-name] [function-name]",
    87  		Flags:       InvokeFnFlags,
    88  		Category:    "DEVELOPMENT COMMANDS",
    89  		Description: `This command invokes a function. Users may send input to their function by passing input to this command via STDIN.`,
    90  		Action:      cl.Invoke,
    91  		BashComplete: func(c *cli.Context) {
    92  			switch len(c.Args()) {
    93  			case 0:
    94  				app.BashCompleteApps(c)
    95  			case 1:
    96  				fn.BashCompleteFns(c)
    97  			}
    98  		},
    99  	}
   100  }
   101  
   102  func (cl *invokeCmd) Invoke(c *cli.Context) error {
   103  	var contentType string
   104  
   105  	invokeURL := c.String("endpoint")
   106  
   107  	if invokeURL == "" {
   108  
   109  		appName := c.Args().Get(0)
   110  		fnName := c.Args().Get(1)
   111  
   112  		if appName == "" || fnName == "" {
   113  			return errors.New("missing app and function name")
   114  		}
   115  
   116  		app, err := app.GetAppByName(cl.client, appName)
   117  		if err != nil {
   118  			return err
   119  		}
   120  		fn, err := fn.GetFnByName(cl.client, app.ID, fnName)
   121  		if err != nil {
   122  			return err
   123  		}
   124  		var ok bool
   125  		invokeURL, ok = fn.Annotations[FnInvokeEndpointAnnotation].(string)
   126  		if !ok {
   127  			return fmt.Errorf("Fn invoke url annotation not present, %s", FnInvokeEndpointAnnotation)
   128  		}
   129  	}
   130  	content := stdin()
   131  	wd := common.GetWd()
   132  
   133  	if c.String("content-type") != "" {
   134  		contentType = c.String("content-type")
   135  	} else {
   136  		_, ff, err := common.FindAndParseFuncFileV20180708(wd)
   137  		if err == nil && ff.Content_type != "" {
   138  			contentType = ff.Content_type
   139  		}
   140  	}
   141  
   142  	resp, err := client.Invoke(cl.provider,
   143  		client.InvokeRequest{
   144  			URL:         invokeURL,
   145  			Content:     content,
   146  			Env:         c.StringSlice("e"),
   147  			ContentType: contentType,
   148  		},
   149  	)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	defer resp.Body.Close()
   154  
   155  	outputFormat := strings.ToLower(c.String("output"))
   156  	if outputFormat == "json" {
   157  		outputJSON(os.Stdout, resp)
   158  	} else {
   159  		outputNormal(os.Stdout, resp, c.Bool("display-call-id"))
   160  	}
   161  	// TODO we should have a 'raw' option to output the raw http request, it may be useful, idk
   162  
   163  	return nil
   164  }
   165  
   166  func outputJSON(output io.Writer, resp *http.Response) {
   167  	var b bytes.Buffer
   168  	// TODO this is lame
   169  	io.Copy(&b, resp.Body)
   170  
   171  	i := struct {
   172  		Body       string      `json:"body"`
   173  		Headers    http.Header `json:"headers"`
   174  		StatusCode int         `json:"status_code"`
   175  	}{
   176  		Body:       b.String(),
   177  		Headers:    resp.Header,
   178  		StatusCode: resp.StatusCode,
   179  	}
   180  
   181  	enc := json.NewEncoder(output)
   182  	enc.SetIndent("", "    ")
   183  	enc.Encode(i)
   184  }
   185  
   186  func outputNormal(output io.Writer, resp *http.Response, includeCallID bool) {
   187  	if cid, ok := resp.Header[CallIDHeader]; ok && includeCallID {
   188  		fmt.Fprint(output, fmt.Sprintf("Call ID: %v\n", cid[0]))
   189  	}
   190  
   191  	var body io.Reader = resp.Body
   192  	if resp.StatusCode >= 400 {
   193  		// if we don't get json, we need to buffer the input so that we can
   194  		// display the user's function output as it was...
   195  		var b bytes.Buffer
   196  		body = io.TeeReader(resp.Body, &b)
   197  
   198  		var msg struct {
   199  			Message string `json:"message"`
   200  		}
   201  		err := json.NewDecoder(body).Decode(&msg)
   202  		if err == nil && msg.Message != "" {
   203  			// this is likely from fn, so unravel this...
   204  			// TODO this should be stderr maybe? meh...
   205  			fmt.Fprintf(output, "Error invoking function. status: %v message: %v\n", resp.StatusCode, msg.Message)
   206  			return
   207  		}
   208  
   209  		// read anything written to buffer first, then copy out rest of body
   210  		body = io.MultiReader(&b, resp.Body)
   211  	}
   212  
   213  	// at this point, it's not an fn error, so output function output as is
   214  
   215  	lcc := lastCharChecker{reader: body}
   216  	body = &lcc
   217  	io.Copy(output, body)
   218  
   219  	// #1408 - flush stdout
   220  	if lcc.last != '\n' {
   221  		fmt.Fprintln(output)
   222  	}
   223  }
   224  
   225  // lastCharChecker wraps an io.Reader to return the last read character
   226  type lastCharChecker struct {
   227  	reader io.Reader
   228  	last   byte
   229  }
   230  
   231  func (l *lastCharChecker) Read(b []byte) (int, error) {
   232  	n, err := l.reader.Read(b)
   233  	if n > 0 {
   234  		l.last = b[n-1]
   235  	}
   236  	return n, err
   237  }