Skip to content

Commit

Permalink
feat: host-based scaffolded function runner
Browse files Browse the repository at this point in the history
  • Loading branch information
lkingland committed May 15, 2023
1 parent 720bd3c commit b05b98a
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 8 deletions.
9 changes: 3 additions & 6 deletions pkg/functions/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ type Describer interface {
// there is a one to many relationship between a given route and processes.
// By default the system creates the 'local' and 'remote' named instances
// when a function is run (locally) and deployed, respectively.
// See the .Instances(f) accessor for the map of named environments to these
// See the .InstanceRefs(f) accessor for the map of named environments to these
// function information structures.
type Instance struct {
// Route is the primary route of a function instance.
Expand Down Expand Up @@ -197,7 +197,6 @@ func New(options ...Option) *Client {
builder: &noopBuilder{output: os.Stdout},
pusher: &noopPusher{output: os.Stdout},
deployer: &noopDeployer{output: os.Stdout},
runner: &noopRunner{output: os.Stdout},
remover: &noopRemover{output: os.Stdout},
lister: &noopLister{output: os.Stdout},
describer: &noopDescriber{output: os.Stdout},
Expand All @@ -206,6 +205,7 @@ func New(options ...Option) *Client {
pipelinesProvider: &noopPipelinesProvider{},
transport: http.DefaultTransport,
}
c.runner = newDefaultRunner(c, os.Stdout, os.Stderr)
for _, o := range options {
o(c)
}
Expand Down Expand Up @@ -561,10 +561,7 @@ func (c *Client) Init(cfg Function) (Function, error) {
}

// Write out the new function's Template files.
// Templates contain values which may result in the function being mutated
// (default builders, etc)
err = c.Templates().Write(&f)
if err != nil {
if err = c.Templates().Write(&f); err != nil {
return f, err
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/functions/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ func TestClient_New_Delegation(t *testing.T) {

// TestClient_Run ensures that the runner is invoked with the absolute path requested.
// Implicitly checks that the stop fn returned also is respected.
// TODO: this test can be replaced with an actual test of testing Run now that
// running outside of a container is supported by Go programs which use the
// "host" builder (are scaffolded). See the unit test TestRunner_Default
// for a first pass at what could probably be the replacement for this test
func TestClient_Run(t *testing.T) {
// Create the root function directory
root := "testdata/example.com/testRun"
Expand Down
164 changes: 164 additions & 0 deletions pkg/functions/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package functions

import (
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"time"
)

const (
defaultRunHost = "127.0.0.1"
defaultRunPort = "8080"
defaultRunDialTimeout = 2 * time.Second
defaultRunStopTimeout = 10 * time.Second
)

type defaultRunner struct {
client *Client
out io.Writer
err io.Writer
}

func newDefaultRunner(client *Client, out, err io.Writer) *defaultRunner {
return &defaultRunner{
client: client,
out: out,
err: err,
}
}

func (r *defaultRunner) Run(ctx context.Context, f Function) (job *Job, err error) {
var (
port = choosePort(defaultRunHost, defaultRunPort, defaultRunDialTimeout)
doneCh = make(chan error, 10)
stopFn = func() error { return nil } // Only needed for continerized runs
verbose = r.client.verbose
)

// NewJob creates .func/runs/PORT,
job, err = NewJob(f, port, doneCh, stopFn, verbose)
if err != nil {
return
}

// Write scaffolding into the build directory
if err = r.client.Scaffold(ctx, f, job.Dir()); err != nil {
return
}

// Start and report any errors or premature exits on the done channel
// NOTE that for host builds, multiple instances of the runner are all
// running with f.Root as their root directory which can lead to FS races
// if the function's implementation is writing to the FS and expects to be
// in a container.
go func() {

// TODO: extract the build command code from the OCI Container Builder
// and have both the runner and OCI Container Builder use the same.
if verbose {
fmt.Printf("cd %v && go build -o f.bin\n", job.Dir())
}
args := []string{"build", "-o", "f.bin"}
if verbose {
args = append(args, "-v")
}
cmd := exec.CommandContext(ctx, "go", args...)
cmd.Dir = job.Dir()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
doneCh <- err
return
}
if verbose {
fmt.Println("build complete")
}

bin := filepath.Join(job.Dir(), "f.bin")
if verbose {
fmt.Printf("cd %v && PORT=%v %v\n", f.Root, port, bin)
}
cmd = exec.CommandContext(ctx, bin)
cmd.Dir = f.Root
cmd.Env = append(cmd.Environ(), "PORT="+port)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// cmd.Cancel = stop // TODO: check this, introduced go 1.20
doneCh <- cmd.Run()
}()

// TODO(lkingland): probably should just run these jobs synchronously and
// allowed the caller to place the task in a separate goroutine should they
// want to background, using ctx.Done() to signal interrupt.
// This will require refactoring the docker.Runner as well, however, so
// sticking with the pattern for now.
return
}

/*
func getRunFunc(f Function) (runner, error) {
notSupportedError := fmt.Errorf("the runtime '%v' is not currently available as a host runner. Perhaps try running containerized.")
switch f.Runtime {
case "":
return nil, fmt.Errorf("runner requires the function have runtime set")
case "go":
return runFunc(runGo(ctx, f))
case "python":
return runPython(ctx, f)
case "java":
return nil, runnerNotImplemeted(f.Runtime)
case "node":
return nil, runnerNotImplemeted(f.Runtime)
case "rust":
return nil, runnerNotImplemeted(f.Runtime)
default:
return nil, fmt.Errorf("runner does not recognized the %q runtime", f.Runtime)
}
}
*/

type runnerNotImplemented struct {
Runtime string
}

func (e runnerNotImplemented) Error() string {
return fmt.Sprintf("the runtime %q is not supported by the host runner. Try running containerized.", e.Runtime)
}

// choosePort returns an unused port
// Note this is not fool-proof becase of a race with any other processes
// looking for a port at the same time. If that is important, we can implement
// a check-lock-check via the filesystem.
// Also note that TCP is presumed.
func choosePort(host string, preferredPort string, dialTimeout time.Duration) string {
var (
port = defaultRunPort
c net.Conn
l net.Listener
err error
)
// Try preferreed
if c, err = net.DialTimeout("tcp", net.JoinHostPort(host, port), dialTimeout); err == nil {
c.Close() // note err==nil
return preferredPort
}

// OS-chosen
if l, err = net.Listen("tcp", net.JoinHostPort(host, "")); err != nil {
fmt.Fprintf(os.Stderr, "unable to check for open ports. using fallback %v. %v", defaultRunPort, err)
return port
}
l.Close() // begins aforementioned race
if _, port, err = net.SplitHostPort(l.Addr().String()); err != nil {
fmt.Fprintf(os.Stderr, "error isolating port from '%v'. %v", l.Addr(), err)
}
return port
}
53 changes: 53 additions & 0 deletions pkg/functions/runner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package functions_test

import (
"context"
"fmt"
"io/ioutil"
"net/http"
"testing"

fn "knative.dev/func/pkg/functions"
"knative.dev/func/pkg/oci"
. "knative.dev/func/pkg/testing"
)

// TestRunner ensures that the default internal runner correctly executes
// a scaffolded function.
func TestRunner(t *testing.T) {
// This integration test explicitly requires the "host" builder due to its
// lack of a dependency on a container runtime, and the other builders not
// taking advantage of Scaffolding (expected by this runner).
// See E2E tests for testing of running functions built using Pack or S2I and
// which are dependent on Podman or Docker.

// TODO: this test likely supercedes TestClient_Run which simply uses a mock.

root, cleanup := Mktemp(t)
defer cleanup()

ctx := context.Background()

var client *fn.Client
client = fn.New(fn.WithBuilder(oci.NewBuilder("", client, true)))
f, err := client.Init(fn.Function{Root: root, Runtime: "go", Registry: TestRegistry})
if f, err = client.Build(ctx, f); err != nil {
t.Fatal(err)
}
job, err := client.Run(ctx, f)
if err != nil {
t.Fatal(err)
}

resp, err := http.Get(fmt.Sprintf("http://localhost:%s", job.Port))
if err != nil {
t.Fatal(err)
}
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
}
defer resp.Body.Close()

fmt.Printf("RUN received: %s\n", bodyBytes)

}
4 changes: 2 additions & 2 deletions pkg/functions/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ func (t template) Write(ctx context.Context, f *Function) error {
f.Invoke = t.config.Invoke
}

isManifest := func(p string) bool {
mask := func(p string) bool {
_, f := path.Split(p)
return f == templateManifest
}

return filesystem.CopyFromFS(".", f.Root, filesystem.NewMaskingFS(isManifest, t.fs)) // copy everything but manifest.yaml
return filesystem.CopyFromFS(".", f.Root, filesystem.NewMaskingFS(mask, t.fs)) // copy everything but manifest.yaml
}

0 comments on commit b05b98a

Please sign in to comment.