Skip to content

Commit e8ccb1b

Browse files
authored
feat: add --address option to func run (knative#2887)
1 parent ffd997c commit e8ccb1b

File tree

11 files changed

+127
-38
lines changed

11 files changed

+127
-38
lines changed

cmd/invoke_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func TestInvoke(t *testing.T) {
3030
// Mock Runner
3131
// Starts a service which sets invoked=1 on any request
3232
runner := mock.NewRunner()
33-
runner.RunFn = func(ctx context.Context, f fn.Function, _ time.Duration) (job *fn.Job, err error) {
33+
runner.RunFn = func(ctx context.Context, f fn.Function, _ string, _ time.Duration) (job *fn.Job, err error) {
3434
var (
3535
l net.Listener
3636
h = http.NewServeMux()

cmd/run.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ NAME
2828
SYNOPSIS
2929
{{rootCmdUse}} run [-t|--container] [-r|--registry] [-i|--image] [-e|--env]
3030
[--build] [-b|--builder] [--builder-image] [-c|--confirm]
31-
[-v|--verbose]
31+
[--address] [-v|--verbose]
3232
3333
DESCRIPTION
3434
Run the function locally.
@@ -68,9 +68,12 @@ EXAMPLES
6868
6969
o Run the function locally on the host with no containerization (Go only).
7070
$ {{rootCmdUse}} run --container=false
71+
72+
o Run the function locally on a specific address.
73+
$ {{rootCmdUse}} run --address=0.0.0.0:8081
7174
`,
7275
SuggestFor: []string{"rnu"},
73-
PreRunE: bindEnv("build", "builder", "builder-image", "confirm", "container", "env", "image", "path", "registry", "start-timeout", "verbose"),
76+
PreRunE: bindEnv("address", "build", "builder", "builder-image", "confirm", "container", "env", "image", "path", "registry", "start-timeout", "verbose"),
7477
RunE: func(cmd *cobra.Command, _ []string) error {
7578
return runRun(cmd, newClient)
7679
},
@@ -124,6 +127,8 @@ EXAMPLES
124127
cmd.Flags().String("build", "auto",
125128
"Build the function. [auto|true|false]. ($FUNC_BUILD)")
126129
cmd.Flags().Lookup("build").NoOptDefVal = "true" // register `--build` as equivalient to `--build=true`
130+
cmd.Flags().String("address", "",
131+
"Interface and port on which to bind and listen. Default is 127.0.0.1:8080, or an available port if 8080 is not available. ($FUNC_ADDRESS)")
127132

128133
// Oft-shared flags:
129134
addConfirmFlag(cmd, cfg.Confirm)
@@ -234,7 +239,7 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) {
234239
// For the former, build is required and a container runtime. For the
235240
// latter, scaffolding is first applied and the local host must be
236241
// configured to build/run the language of the function.
237-
job, err := client.Run(cmd.Context(), f)
242+
job, err := client.Run(cmd.Context(), f, fn.RunWithAddress(cfg.Address))
238243
if err != nil {
239244
return
240245
}
@@ -285,6 +290,9 @@ type runConfig struct {
285290
// StartTimeout optionally adjusts the startup timeout from the client's
286291
// default of fn.DefaultStartTimeout.
287292
StartTimeout time.Duration
293+
294+
// Address is the interface and port to bind (e.g. "0.0.0.0:8081")
295+
Address string
288296
}
289297

290298
func newRunConfig(cmd *cobra.Command) (c runConfig) {
@@ -294,6 +302,7 @@ func newRunConfig(cmd *cobra.Command) (c runConfig) {
294302
Env: viper.GetStringSlice("env"),
295303
Container: viper.GetBool("container"),
296304
StartTimeout: viper.GetDuration("start-timeout"),
305+
Address: viper.GetString("address"),
297306
}
298307
// NOTE: .Env should be viper.GetStringSlice, but this returns unparsed
299308
// results and appears to be an open issue since 2017:

cmd/run_test.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func TestRun_Run(t *testing.T) {
107107

108108
runner := mock.NewRunner()
109109
if tt.runError != nil {
110-
runner.RunFn = func(context.Context, fn.Function, time.Duration) (*fn.Job, error) { return nil, tt.runError }
110+
runner.RunFn = func(context.Context, fn.Function, string, time.Duration) (*fn.Job, error) { return nil, tt.runError }
111111
}
112112

113113
builder := mock.NewBuilder()
@@ -220,7 +220,7 @@ func TestRun_Images(t *testing.T) {
220220
runner := mock.NewRunner()
221221

222222
if tt.runError != nil {
223-
runner.RunFn = func(context.Context, fn.Function, time.Duration) (*fn.Job, error) { return nil, tt.runError }
223+
runner.RunFn = func(context.Context, fn.Function, string, time.Duration) (*fn.Job, error) { return nil, tt.runError }
224224
}
225225

226226
builder := mock.NewBuilder()
@@ -324,7 +324,7 @@ func TestRun_CorrectImage(t *testing.T) {
324324
root := FromTempDirectory(t)
325325
runner := mock.NewRunner()
326326

327-
runner.RunFn = func(_ context.Context, f fn.Function, _ time.Duration) (*fn.Job, error) {
327+
runner.RunFn = func(_ context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
328328
// TODO: add if for empty image? -- should fail beforehand
329329
if f.Build.Image != tt.image {
330330
return nil, fmt.Errorf("Expected image: %v but got: %v", tt.image, f.Build.Image)
@@ -394,7 +394,7 @@ func TestRun_DirectOverride(t *testing.T) {
394394
root := FromTempDirectory(t)
395395
runner := mock.NewRunner()
396396

397-
runner.RunFn = func(_ context.Context, f fn.Function, _ time.Duration) (*fn.Job, error) {
397+
runner.RunFn = func(_ context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
398398
if f.Build.Image != overrideImage {
399399
return nil, fmt.Errorf("Expected image to be overridden with '%v' but got: '%v'", overrideImage, f.Build.Image)
400400
}
@@ -462,3 +462,47 @@ func TestRun_DirectOverride(t *testing.T) {
462462
t.Fatal(err)
463463
}
464464
}
465+
466+
// TestRun_Address ensures that the --address flag is passed to the runner.
467+
func TestRun_Address(t *testing.T) {
468+
root := FromTempDirectory(t)
469+
_, err := fn.New().Init(fn.Function{Root: root, Runtime: "go"})
470+
if err != nil {
471+
t.Fatal(err)
472+
}
473+
474+
testAddr := "0.0.0.0:1234"
475+
476+
runner := mock.NewRunner()
477+
runner.RunFn = func(_ context.Context, f fn.Function, addr string, _ time.Duration) (*fn.Job, error) {
478+
if addr != testAddr {
479+
return nil, fmt.Errorf("Expected address '%v' but got: '%v'", testAddr, addr)
480+
}
481+
errs := make(chan error, 1)
482+
stop := func() error { return nil }
483+
return fn.NewJob(f, "127.0.0.1", "8080", errs, stop, false)
484+
}
485+
486+
// RUN THE ACTUAL TESTED COMMAND
487+
cmd := NewRunCmd(NewTestClient(
488+
fn.WithRunner(runner),
489+
fn.WithRegistry("ghcr.com/reg"),
490+
))
491+
cmd.SetArgs([]string{"--address", testAddr})
492+
493+
ctx, cancel := context.WithCancel(context.Background())
494+
runErrCh := make(chan error, 1)
495+
go func() {
496+
_, err := cmd.ExecuteContextC(ctx)
497+
if err != nil {
498+
runErrCh <- err // error was not expected
499+
return
500+
}
501+
close(runErrCh) // release the waiting parent process
502+
}()
503+
cancel() // trigger the return of cmd.ExecuteContextC in the routine
504+
<-ctx.Done()
505+
if err := <-runErrCh; err != nil { // wait for completion of assertions
506+
t.Fatal(err)
507+
}
508+
}

docs/reference/func_run.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ NAME
1111
SYNOPSIS
1212
func run [-t|--container] [-r|--registry] [-i|--image] [-e|--env]
1313
[--build] [-b|--builder] [--builder-image] [-c|--confirm]
14-
[-v|--verbose]
14+
[--address] [-v|--verbose]
1515

1616
DESCRIPTION
1717
Run the function locally.
@@ -52,6 +52,9 @@ EXAMPLES
5252
o Run the function locally on the host with no containerization (Go only).
5353
$ func run --container=false
5454

55+
o Run the function locally on a specific address.
56+
$ func run --address=0.0.0.0:8081
57+
5558

5659
```
5760
func run
@@ -60,6 +63,7 @@ func run
6063
### Options
6164

6265
```
66+
--address string Interface and port on which to bind and listen. Default is 127.0.0.1:8080, or an available port if 8080 is not available. ($FUNC_ADDRESS)
6367
--build string[="true"] Build the function. [auto|true|false]. ($FUNC_BUILD) (default "auto")
6468
-b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". (default "pack")
6569
--builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE)

pkg/docker/runner.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ func NewRunner(verbose bool, out, errOut io.Writer) *Runner {
5050
}
5151

5252
// Run the function.
53-
func (n *Runner) Run(ctx context.Context, f fn.Function, startTimeout time.Duration) (job *fn.Job, err error) {
53+
func (n *Runner) Run(ctx context.Context, f fn.Function, address string, startTimeout time.Duration) (job *fn.Job, err error) {
5454

5555
var (
56-
port = choosePort(DefaultHost, DefaultPort, DefaultDialTimeout)
56+
host = DefaultHost
57+
port = DefaultPort
5758
c client.CommonAPIClient // Docker client
5859
id string // ID of running container
5960
conn net.Conn // Connection to container's stdio
@@ -67,13 +68,25 @@ func (n *Runner) Run(ctx context.Context, f fn.Function, startTimeout time.Durat
6768
runtimeErrCh = make(chan error, 10)
6869
)
6970

71+
// Parse address if provided
72+
if address != "" {
73+
var err error
74+
host, port, err = net.SplitHostPort(address)
75+
if err != nil {
76+
return nil, fmt.Errorf("invalid address format '%s': %w", address, err)
77+
}
78+
}
79+
80+
// Choose an available port
81+
port = choosePort(host, port, DefaultDialTimeout)
82+
7083
if f.Build.Image == "" {
7184
return job, errors.New("Function has no associated image. Has it been built?")
7285
}
7386
if c, _, err = NewClient(client.DefaultDockerHost); err != nil {
7487
return job, errors.Wrap(err, "failed to create Docker API client")
7588
}
76-
if id, err = newContainer(ctx, c, f, port, n.verbose); err != nil {
89+
if id, err = newContainer(ctx, c, f, host, port, n.verbose); err != nil {
7790
return job, errors.Wrap(err, "runner unable to create container")
7891
}
7992
if conn, err = copyStdio(ctx, c, id, copyErrCh, n.out, n.errOut); err != nil {
@@ -136,7 +149,7 @@ func (n *Runner) Run(ctx context.Context, f fn.Function, startTimeout time.Durat
136149
}
137150

138151
// Job reporting port, runtime errors and provides a mechanism for stopping.
139-
return fn.NewJob(f, DefaultHost, port, runtimeErrCh, stop, n.verbose)
152+
return fn.NewJob(f, host, port, runtimeErrCh, stop, n.verbose)
140153
}
141154

142155
// Dial the given (tcp) port on the given interface, returning an error if it is
@@ -178,15 +191,15 @@ func choosePort(host, preferredPort string, dialTimeout time.Duration) string {
178191

179192
}
180193

181-
func newContainer(ctx context.Context, c client.CommonAPIClient, f fn.Function, port string, verbose bool) (id string, err error) {
194+
func newContainer(ctx context.Context, c client.CommonAPIClient, f fn.Function, host, port string, verbose bool) (id string, err error) {
182195
var (
183196
containerCfg container.Config
184197
hostCfg container.HostConfig
185198
)
186199
if containerCfg, err = newContainerConfig(f, port, verbose); err != nil {
187200
return
188201
}
189-
if hostCfg, err = newHostConfig(port); err != nil {
202+
if hostCfg, err = newHostConfig(host, port); err != nil {
190203
return
191204
}
192205
t, err := c.ContainerCreate(ctx, &containerCfg, &hostCfg, nil, nil, "")
@@ -225,14 +238,14 @@ func newContainerConfig(f fn.Function, _ string, verbose bool) (c container.Conf
225238
return
226239
}
227240

228-
func newHostConfig(port string) (c container.HostConfig, err error) {
241+
func newHostConfig(host, port string) (c container.HostConfig, err error) {
229242
// httpPort := nat.Port(fmt.Sprintf("%v/tcp", port))
230243
httpPort := nat.Port("8080/tcp")
231244
ports := map[nat.Port][]nat.PortBinding{
232245
httpPort: {
233246
nat.PortBinding{
234247
HostPort: port,
235-
HostIP: "127.0.0.1",
248+
HostIP: host,
236249
},
237250
},
238251
}

pkg/docker/runner_int_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func TestRun(t *testing.T) {
5454
// Run the function using a docker runner
5555
var out, errOut bytes.Buffer
5656
runner := docker.NewRunner(true, &out, &errOut)
57-
j, err := runner.Run(ctx, f, fn.DefaultStartTimeout)
57+
j, err := runner.Run(ctx, f, "", fn.DefaultStartTimeout)
5858
if err != nil {
5959
t.Fatal(err)
6060
}

pkg/docker/runner_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func TestDockerRun(t *testing.T) {
3636
// NOTE: test requires that the image be built already.
3737

3838
runner := docker.NewRunner(true, os.Stdout, os.Stdout)
39-
if _, err = runner.Run(context.Background(), f, fn.DefaultStartTimeout); err != nil {
39+
if _, err = runner.Run(context.Background(), f, "", fn.DefaultStartTimeout); err != nil {
4040
t.Fatal(err)
4141
}
4242
/* TODO
@@ -50,7 +50,7 @@ func TestDockerRunImagelessError(t *testing.T) {
5050
runner := docker.NewRunner(true, os.Stdout, os.Stderr)
5151
f := fn.NewFunctionWith(fn.Function{})
5252

53-
_, err := runner.Run(context.Background(), f, fn.DefaultStartTimeout)
53+
_, err := runner.Run(context.Background(), f, "", fn.DefaultStartTimeout)
5454
// TODO: switch to typed error:
5555
expectedErrorMessage := "Function has no associated image. Has it been built?"
5656
if err == nil || err.Error() != expectedErrorMessage {

pkg/functions/client.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ type Runner interface {
133133
// a stop function. The process can be stopped by running the returned stop
134134
// function, either on context cancellation or in a defer.
135135
// The duration is the time to wait for the job to start.
136-
Run(context.Context, Function, time.Duration) (*Job, error)
136+
Run(context.Context, Function, string, time.Duration) (*Job, error)
137137
}
138138

139139
// Remover of deployed services.
@@ -176,11 +176,12 @@ type Instance struct {
176176
Route string
177177
// Routes is the primary route plus any other route at which the function
178178
// can be contacted.
179-
Routes []string `json:"routes" yaml:"routes"`
180-
Name string `json:"name" yaml:"name"`
181-
Image string `json:"image" yaml:"image"`
182-
Namespace string `json:"namespace" yaml:"namespace"`
183-
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
179+
Routes []string `json:"routes" yaml:"routes"`
180+
Name string `json:"name" yaml:"name"`
181+
Image string `json:"image" yaml:"image"`
182+
Namespace string `json:"namespace" yaml:"namespace"`
183+
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
184+
Labels map[string]string `json:"labels" yaml:"labels"`
184185
}
185186

186187
// Subscriptions currently active to event sources
@@ -906,6 +907,7 @@ func (c *Client) Route(ctx context.Context, f Function) (string, Function, error
906907

907908
type RunOptions struct {
908909
StartTimeout time.Duration
910+
Address string
909911
}
910912

911913
type RunOption func(c *RunOptions)
@@ -920,6 +922,12 @@ func RunWithStartTimeout(t time.Duration) RunOption {
920922
}
921923
}
922924

925+
func RunWithAddress(address string) RunOption {
926+
return func(c *RunOptions) {
927+
c.Address = address
928+
}
929+
}
930+
923931
// Run the function whose code resides at root.
924932
// On start, the chosen port is sent to the provided started channel
925933
func (c *Client) Run(ctx context.Context, f Function, options ...RunOption) (job *Job, err error) {
@@ -944,7 +952,7 @@ func (c *Client) Run(ctx context.Context, f Function, options ...RunOption) (job
944952

945953
// Run the function, which returns a Job for use interacting (at arms length)
946954
// with that running task (which is likely inside a container process).
947-
if job, err = c.runner.Run(ctx, f, timeout); err != nil {
955+
if job, err = c.runner.Run(ctx, f, oo.Address, timeout); err != nil {
948956
return
949957
}
950958

pkg/functions/client_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,7 +1740,7 @@ func TestClient_Invoke_HTTP(t *testing.T) {
17401740
// Create a client with a mock runner which will report the port at which the
17411741
// interloping function is listening.
17421742
runner := mock.NewRunner()
1743-
runner.RunFn = func(ctx context.Context, f fn.Function, _ time.Duration) (*fn.Job, error) {
1743+
runner.RunFn = func(ctx context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
17441744
_, p, _ := net.SplitHostPort(l.Addr().String())
17451745
errs := make(chan error, 10)
17461746
stop := func() error { return nil }
@@ -1842,7 +1842,7 @@ func TestClient_Invoke_CloudEvent(t *testing.T) {
18421842

18431843
// Create a client with a mock Runner which returns its address.
18441844
runner := mock.NewRunner()
1845-
runner.RunFn = func(ctx context.Context, f fn.Function, _ time.Duration) (*fn.Job, error) {
1845+
runner.RunFn = func(ctx context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
18461846
_, p, _ := net.SplitHostPort(l.Addr().String())
18471847
errs := make(chan error, 10)
18481848
stop := func() error { return nil }
@@ -1896,7 +1896,7 @@ func TestClient_Instances(t *testing.T) {
18961896

18971897
// A mock runner
18981898
runner := mock.NewRunner()
1899-
runner.RunFn = func(_ context.Context, f fn.Function, _ time.Duration) (*fn.Job, error) {
1899+
runner.RunFn = func(_ context.Context, f fn.Function, _ string, _ time.Duration) (*fn.Job, error) {
19001900
errs := make(chan error, 10)
19011901
stop := func() error { return nil }
19021902
return fn.NewJob(f, "127.0.0.1", "8080", errs, stop, false)

0 commit comments

Comments
 (0)