-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: host-based scaffolded function runner
- Loading branch information
Showing
5 changed files
with
226 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters