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 }