mirror of https://github.com/knative/func.git
feat!: invoke (#705)
* feat!: rename 'emit' to 'invoke' and default to local This commit renames 'func emit' command to 'func invoke' and makes the default behavior to send an event to localhost. The special '--sink' value 'local' is changed to 'cluster' to indicate that the function should be invoked on the cluster instead of locally. All other behavior has remained the same. BREAKING CHANGE Signed-off-by: Lance Ball <lball@redhat.com> * fixup: update commands.md doc Signed-off-by: Lance Ball <lball@redhat.com> * squash: change Emitter interface to Invoker Changes Emit() to Send() in the (now named) Invoker interface, and changes Emit() to Invoke() in the client. BREAKING CHANGE Signed-off-by: Lance Ball <lball@redhat.com> * squash: use a common Invoker interface for HTTP and events Signed-off-by: Lance Ball <lball@redhat.com> * checkpoint Signed-off-by: Lance Ball <lball@redhat.com> * fixup: change Emitter to EventInvoker Signed-off-by: Lance Ball <lball@redhat.com> * Invoke v2 Draft * feat: client invoke function * static invoke defaults and methods * remove assimilated invoker package * includes an ignored .func directory on create * Instances manager with local and remote defaults Funciton Info is now Instance, representing a Function in a given environment. Describing a Function instance is now Instances().Get(f, environment) Moves Runner to be async with a Stop method to enable returning runtime pid and port for persisting. Instances now have a place for primary Route in addition to all routes slice Running Functions write PID and Port to .func * cascading targets: local vs remote vs ad-hoc endpoint * runner start signals and cancel cleanup * return run on context done or err on channel * async runner Refactors the image runner to start the container asynchronously, reporting back the port on which it started. Errors are communicated back via a provided channel and stop is signaled using context cancelation. * pid neither required nor available * add withTransport option Incorporates addition of custom transport of the emitter into the renamed version invoker. Flag and help text cleanup. Re-additionof the Info accessor. * schema now includes invocation data * loop build msg * run jobs Externally exposed port is now chosen based on availability, with 8080 preferred and falling back to an os-chosen open port. The Client Run method is now async, returning the port assigned to the running Function, a stop/cleanup function and a runtime errors channel. The Runner is internally divided into the runner and its started Jobs. * job metadata Extracts job metadata tracking to a Job object in the core, Handles multiple instances of the same Function by creating a single file for each instances in .func/instances/<port> * remove superfluous error types and flag bindings * feat: enable invoke target remote * feat: preferentially invoke local, remote if running * feat: read --file for invoke * feat: invoke confirm prompts * fixup cli tests - Updates to handle asynchronous Runner - Standardize on the naming convention for selective running * docker runner tests and lint errors * test refactor * feat: invoke format override * comments, spelling and other cleanup * invoke command doc * feat: invoke format interactive option * rename runjob.go to job.go * e2e test flag update * test naming homoginization * silence build activity messages when verbose * test debugging * code review updates - return Job from Client.Run rather than constituent members - Treat .gitignore as contentious, punting on feature to mutate if extant. - docs wording changes - add invocation format to pertinent manifest.yaml files * help text spelling etc. Co-authored-by: Lance Ball <lball@redhat.com>
This commit is contained in:
parent
8ceb325142
commit
e918f74b9e
227
client.go
227
client.go
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
@ -52,10 +53,11 @@ type Client struct {
|
|||
dnsProvider DNSProvider // Provider of DNS services
|
||||
registry string // default registry for OCI image tags
|
||||
progressListener ProgressListener // progress listener
|
||||
emitter Emitter // Emits CloudEvents to functions
|
||||
pipelinesProvider PipelinesProvider // Manages lifecyle of CI/CD pipelines used by a Function
|
||||
repositories *Repositories // Repositories management
|
||||
templates *Templates // Templates management
|
||||
instances *Instances // Function Instances management
|
||||
transport http.RoundTripper // Customizable internal transport
|
||||
pipelinesProvider PipelinesProvider // CI/CD pipelines management
|
||||
}
|
||||
|
||||
// ErrNotBuilt indicates the Function has not yet been built.
|
||||
|
@ -96,8 +98,10 @@ const (
|
|||
|
||||
// Runner runs the Function locally.
|
||||
type Runner interface {
|
||||
// Run the Function locally.
|
||||
Run(context.Context, Function) error
|
||||
// Run the Function, returning a Job with metadata, error channels, and
|
||||
// a stop function.The process can be stopped by running the returned stop
|
||||
// function, either on context cancellation or in a defer.
|
||||
Run(context.Context, Function) (*Job, error)
|
||||
}
|
||||
|
||||
// Remover of deployed services.
|
||||
|
@ -138,18 +142,30 @@ type ProgressListener interface {
|
|||
Done()
|
||||
}
|
||||
|
||||
// Describer of Functions' remote deployed aspect.
|
||||
// Describer of Function instances
|
||||
type Describer interface {
|
||||
// Describe the running state of the service as reported by the underlyng platform.
|
||||
Describe(ctx context.Context, name string) (description Info, err error)
|
||||
// Describe the named Function in the remote environment.
|
||||
Describe(ctx context.Context, name string) (Instance, error)
|
||||
}
|
||||
|
||||
// Info about a given Function
|
||||
type Info struct {
|
||||
// Instance data about the runtime state of a Function in a given environment.
|
||||
//
|
||||
// A Function instance is a logical running Function space, which share
|
||||
// a unique route (or set of routes). Due to autoscaling and load balancing,
|
||||
// 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
|
||||
// Function Information structures.
|
||||
type Instance struct {
|
||||
// Route is the primary route of a Function instance.
|
||||
Route string
|
||||
// Routes is the primary route plus any other route at which the Function
|
||||
// can be contacted.
|
||||
Routes []string `json:"routes" yaml:"routes"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Image string `json:"image" yaml:"image"`
|
||||
Namespace string `json:"namespace" yaml:"namespace"`
|
||||
Routes []string `json:"routes" yaml:"routes"`
|
||||
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
|
||||
}
|
||||
|
||||
|
@ -166,11 +182,6 @@ type DNSProvider interface {
|
|||
Provide(Function) error
|
||||
}
|
||||
|
||||
// Emitter emits CloudEvents to functions
|
||||
type Emitter interface {
|
||||
Emit(ctx context.Context, endpoint string) error
|
||||
}
|
||||
|
||||
// PipelinesProvider manages lifecyle of CI/CD pipelines used by a Function
|
||||
type PipelinesProvider interface {
|
||||
Run(context.Context, Function) error
|
||||
|
@ -186,18 +197,21 @@ func New(options ...Option) *Client {
|
|||
runner: &noopRunner{output: os.Stdout},
|
||||
remover: &noopRemover{output: os.Stdout},
|
||||
lister: &noopLister{output: os.Stdout},
|
||||
describer: &noopDescriber{output: os.Stdout},
|
||||
dnsProvider: &noopDNSProvider{output: os.Stdout},
|
||||
progressListener: &NoopProgressListener{},
|
||||
emitter: &noopEmitter{},
|
||||
pipelinesProvider: &noopPipelinesProvider{},
|
||||
repositoriesPath: filepath.Join(ConfigPath(), "repositories"),
|
||||
transport: http.DefaultTransport,
|
||||
}
|
||||
for _, o := range options {
|
||||
o(c)
|
||||
}
|
||||
|
||||
// Initialize sub-managers using now-fully-initialized client.
|
||||
c.repositories = newRepositories(c)
|
||||
c.templates = newTemplates(c)
|
||||
c.instances = newInstances(c)
|
||||
|
||||
// Trigger the creation of the config and repository paths
|
||||
_ = ConfigPath() // Config is package-global scoped
|
||||
|
@ -355,11 +369,10 @@ func WithRegistry(registry string) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithEmitter sets a CloudEvent emitter on the client which is capable of sending
|
||||
// a CloudEvent to an arbitrary function endpoint
|
||||
func WithEmitter(e Emitter) Option {
|
||||
// WithTransport sets a custom transport to use internally.
|
||||
func WithTransport(t http.RoundTripper) Option {
|
||||
return func(c *Client) {
|
||||
c.emitter = e
|
||||
c.transport = t
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,6 +396,11 @@ func (c *Client) Templates() *Templates {
|
|||
return c.templates
|
||||
}
|
||||
|
||||
// Instances accessor
|
||||
func (c *Client) Instances() *Instances {
|
||||
return c.instances
|
||||
}
|
||||
|
||||
// Runtimes available in totality.
|
||||
// Not all repository/template combinations necessarily exist,
|
||||
// and further validation is performed when a template+runtime is chosen.
|
||||
|
@ -407,8 +425,8 @@ func (c *Client) Runtimes() ([]string, error) {
|
|||
return runtimes.Items(), nil
|
||||
}
|
||||
|
||||
// METHODS
|
||||
// ---------
|
||||
// LIFECYCLE METHODS
|
||||
// -----------------
|
||||
|
||||
// New Function.
|
||||
// Use Create, Build and Deploy independently for lower level control.
|
||||
|
@ -497,12 +515,6 @@ func (c *Client) Create(cfg Function) (err error) {
|
|||
return fmt.Errorf("Function at '%v' already initialized", cfg.Root)
|
||||
}
|
||||
|
||||
// The path for the new Function should not have any contentious files
|
||||
// (hidden files OK, unless it's one used by Func)
|
||||
if err := assertEmptyRoot(cfg.Root); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Path is defaulted to the current working directory
|
||||
if cfg.Root == "" {
|
||||
if cfg.Root, err = os.Getwd(); err != nil {
|
||||
|
@ -515,9 +527,20 @@ func (c *Client) Create(cfg Function) (err error) {
|
|||
cfg.Name = nameFromPath(cfg.Root)
|
||||
}
|
||||
|
||||
// Create a new Function
|
||||
// The path for the new Function should not have any contentious files
|
||||
// (hidden files OK, unless it's one used by Func)
|
||||
if err := assertEmptyRoot(cfg.Root); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new Function (in memory)
|
||||
f := NewFunctionWith(cfg)
|
||||
|
||||
// Create a .func diretory which is also added to a .gitignore
|
||||
if err = createRuntimeDir(f); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Write out the new Function's Template files.
|
||||
// Templates contain values which may result in the Function being mutated
|
||||
// (default builders, etc), so a new (potentially mutated) Function is
|
||||
|
@ -548,33 +571,36 @@ func (c *Client) Create(cfg Function) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// createRuntimeDir creates a .func directory in the root of the given
|
||||
// Function which is also registered as ignored in .gitignore
|
||||
// TODO: Mutate extant .gitignore file if it exists rather than failing
|
||||
// if present (see contentious files in function.go), such that a user
|
||||
// can `git init` a directory prior to `func init` in the same directory).
|
||||
func createRuntimeDir(f Function) error {
|
||||
if err := os.MkdirAll(filepath.Join(f.Root, RunDataDir), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gitignore := `
|
||||
# Functions use the .func directory for local runtime data which should
|
||||
# generally not be tracked in source control:
|
||||
/.func
|
||||
`
|
||||
return os.WriteFile(filepath.Join(f.Root, ".gitignore"), []byte(gitignore), os.ModePerm)
|
||||
|
||||
}
|
||||
|
||||
// Build the Function at path. Errors if the Function is either unloadable or does
|
||||
// not contain a populated Image.
|
||||
func (c *Client) Build(ctx context.Context, path string) (err error) {
|
||||
c.progressListener.Increment("Building function image")
|
||||
|
||||
m := []string{
|
||||
"Still building",
|
||||
"Don't give up on me",
|
||||
"This is taking a while",
|
||||
"Still building"}
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
go func() {
|
||||
for {
|
||||
<-ticker.C
|
||||
if len(m) == 0 {
|
||||
break
|
||||
}
|
||||
c.progressListener.Increment(m[0])
|
||||
m = m[1:] // remove 0th element
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.progressListener.Stopping()
|
||||
}()
|
||||
// If not logging verbosely, the ongoing progress of the build will not
|
||||
// be streaming to stdout, and the lack of activity has been seen to cause
|
||||
// users to prematurely exit due to the sluggishness of pulling large images
|
||||
if !c.verbose {
|
||||
c.printBuildActivity(ctx) // print friendly messages until context is canceled
|
||||
}
|
||||
|
||||
f, err := NewFunction(path)
|
||||
if err != nil {
|
||||
|
@ -606,6 +632,33 @@ func (c *Client) Build(ctx context.Context, path string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (c *Client) printBuildActivity(ctx context.Context) {
|
||||
m := []string{
|
||||
"Still building",
|
||||
"Still building",
|
||||
"Yes, still building",
|
||||
"Don't give up on me",
|
||||
"Still building",
|
||||
"This is taking a while",
|
||||
}
|
||||
i := 0
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.progressListener.Increment(m[i])
|
||||
i++
|
||||
i = i % len(m)
|
||||
case <-ctx.Done():
|
||||
c.progressListener.Stopping()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Deploy the Function at path. Errors if the Function has not been
|
||||
// initialized with an image tag.
|
||||
func (c *Client) Deploy(ctx context.Context, path string) (err error) {
|
||||
|
@ -674,36 +727,39 @@ func (c *Client) Route(path string) (err error) {
|
|||
}
|
||||
|
||||
// Run the Function whose code resides at root.
|
||||
func (c *Client) Run(ctx context.Context, root string) error {
|
||||
// On start, the chosen port is sent to the provided started channel
|
||||
func (c *Client) Run(ctx context.Context, root string) (job *Job, err error) {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.progressListener.Stopping()
|
||||
}()
|
||||
|
||||
// Create an instance of a Function representation at the given root.
|
||||
// Load the Function
|
||||
f, err := NewFunction(root)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
if !f.Initialized() {
|
||||
// TODO: this needs a test.
|
||||
return fmt.Errorf("the given path '%v' does not contain an initialized Function. Please create one at this path in order to run", root)
|
||||
err = fmt.Errorf("the given path '%v' does not contain an initialized "+
|
||||
"Function. Please create one at this path in order to run", root)
|
||||
return
|
||||
}
|
||||
|
||||
// delegate to concrete implementation of runner entirely.
|
||||
return c.runner.Run(ctx, f)
|
||||
}
|
||||
// Run the Function, which returns a Job for use interacting (at arms length)
|
||||
// with that running task (which is likely inside a container process).
|
||||
if job, err = c.runner.Run(ctx, f); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// List currently deployed Functions.
|
||||
func (c *Client) List(ctx context.Context) ([]ListItem, error) {
|
||||
// delegate to concrete implementation of lister entirely.
|
||||
return c.lister.List(ctx)
|
||||
// Return to the caller the effective port, a function to call to trigger
|
||||
// stop, and a channel on which can be received runtime errors.
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// Info for a Function. Name takes precidence. If no name is provided,
|
||||
// the Function defined at root is used.
|
||||
func (c *Client) Info(ctx context.Context, name, root string) (d Info, err error) {
|
||||
func (c *Client) Info(ctx context.Context, name, root string) (d Instance, err error) {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.progressListener.Stopping()
|
||||
|
@ -724,6 +780,12 @@ func (c *Client) Info(ctx context.Context, name, root string) (d Info, err error
|
|||
return c.describer.Describe(ctx, f.Name)
|
||||
}
|
||||
|
||||
// List currently deployed Functions.
|
||||
func (c *Client) List(ctx context.Context) ([]ListItem, error) {
|
||||
// delegate to concrete implementation of lister entirely.
|
||||
return c.lister.List(ctx)
|
||||
}
|
||||
|
||||
// Remove a Function. Name takes precidence. If no name is provided,
|
||||
// the Function defined at root is used if it exists.
|
||||
func (c *Client) Remove(ctx context.Context, cfg Function) error {
|
||||
|
@ -747,13 +809,32 @@ func (c *Client) Remove(ctx context.Context, cfg Function) error {
|
|||
return c.remover.Remove(ctx, f.Name)
|
||||
}
|
||||
|
||||
// Emit a CloudEvent to a function endpoint
|
||||
func (c *Client) Emit(ctx context.Context, endpoint string) error {
|
||||
// Invoke is a convenience method for triggering the execution of a Function
|
||||
// for testing and development.
|
||||
// The target argument is optional, naming the running instance of the Function
|
||||
// which should be invoked. This can be the literal names "local" or "remote",
|
||||
// or can be a URL to an arbitrary endpoint. If not provided, a running local
|
||||
// instance is preferred, with the remote Function triggered if there is no
|
||||
// locally running instance.
|
||||
// Example:
|
||||
// myClient.Invoke(myContext, myFunction, "local", NewInvokeMessage())
|
||||
// The message sent to the Function is defined by the invoke message.
|
||||
// See NewInvokeMessage for its defaults.
|
||||
// Functions are invoked in a manner consistent with the settings defined in
|
||||
// their metadata. For example HTTP vs CloudEvent
|
||||
func (c *Client) Invoke(ctx context.Context, root string, target string, m InvokeMessage) (err error) {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.progressListener.Stopping()
|
||||
}()
|
||||
return c.emitter.Emit(ctx, endpoint)
|
||||
|
||||
f, err := NewFunction(root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// See invoke.go for implementation details
|
||||
return invoke(ctx, c, f, target, m)
|
||||
}
|
||||
|
||||
// Push the image for the named service to the configured registry
|
||||
|
@ -812,7 +893,9 @@ func (n *noopDeployer) Deploy(ctx context.Context, _ Function) (DeploymentResult
|
|||
// Runner
|
||||
type noopRunner struct{ output io.Writer }
|
||||
|
||||
func (n *noopRunner) Run(_ context.Context, _ Function) error { return nil }
|
||||
func (n *noopRunner) Run(context.Context, Function) (job *Job, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Remover
|
||||
type noopRemover struct{ output io.Writer }
|
||||
|
@ -824,10 +907,12 @@ type noopLister struct{ output io.Writer }
|
|||
|
||||
func (n *noopLister) List(context.Context) ([]ListItem, error) { return []ListItem{}, nil }
|
||||
|
||||
// Emitter
|
||||
type noopEmitter struct{}
|
||||
// Describer
|
||||
type noopDescriber struct{ output io.Writer }
|
||||
|
||||
func (n *noopEmitter) Emit(ctx context.Context, endpoint string) error { return nil }
|
||||
func (n *noopDescriber) Describe(context.Context, string) (Instance, error) {
|
||||
return Instance{}, errors.New("no describer provided")
|
||||
}
|
||||
|
||||
// PipelinesProvider
|
||||
type noopPipelinesProvider struct{}
|
||||
|
|
443
client_test.go
443
client_test.go
|
@ -4,16 +4,20 @@
|
|||
package function_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cloudevents "github.com/cloudevents/sdk-go/v2"
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
"knative.dev/kn-plugin-func/mock"
|
||||
. "knative.dev/kn-plugin-func/testing"
|
||||
|
@ -30,11 +34,11 @@ const (
|
|||
TestRuntime = "go"
|
||||
)
|
||||
|
||||
// TestNew Function completes without error using defaults and zero values.
|
||||
// TestClient_New Function completes without error using defaults and zero values.
|
||||
// New is the superset of creating a new fully deployed Function, and
|
||||
// thus implicitly tests Create, Build and Deploy, which are exposed
|
||||
// by the client API for those who prefer manual transmissions.
|
||||
func TestNew(t *testing.T) {
|
||||
func TestClient_New(t *testing.T) {
|
||||
root := "testdata/example.com/testNew"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -45,11 +49,11 @@ func TestNew(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestInstantiationCreatesRepositoriesPath ensures that instantiating the
|
||||
// TestClient_InstantiationCreatesRepositoriesPath ensures that instantiating the
|
||||
// client has the side-effect of ensuring that the repositories path exists
|
||||
// on-disk, and also confirms that the XDG_CONFIG_HOME environment variable is
|
||||
// respected when calculating this home path.
|
||||
func TestInstantiationCreatesRepositoriesPath(t *testing.T) {
|
||||
func TestClient_InstantiationCreatesRepositoriesPath(t *testing.T) {
|
||||
root := "testdata/example.com/testNewCreatesRepositoriesPath"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -74,8 +78,8 @@ func TestInstantiationCreatesRepositoriesPath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRuntimeRequired ensures that the the runtime is an expected value.
|
||||
func TestRuntimeRequired(t *testing.T) {
|
||||
// TestClient_New_RuntimeRequired ensures that the the runtime is an expected value.
|
||||
func TestClient_New_RuntimeRequired(t *testing.T) {
|
||||
// Create a root for the new Function
|
||||
root := "testdata/example.com/testRuntimeRequired"
|
||||
defer Using(t, root)()
|
||||
|
@ -89,9 +93,9 @@ func TestRuntimeRequired(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestNameDefaults ensures that a newly created Function has its name defaulted
|
||||
// TestClient_New_NameDefaults ensures that a newly created Function has its name defaulted
|
||||
// to a name which can be dervied from the last part of the given root path.
|
||||
func TestNameDefaults(t *testing.T) {
|
||||
func TestClient_New_NameDefaults(t *testing.T) {
|
||||
root := "testdata/example.com/testNameDefaults"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -118,9 +122,9 @@ func TestNameDefaults(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestWritesTemplate ensures the config file and files from the template
|
||||
// TestClient_New_WritesTemplate ensures the config file and files from the template
|
||||
// are written on new.
|
||||
func TestWritesTemplate(t *testing.T) {
|
||||
func TestClient_New_WritesTemplate(t *testing.T) {
|
||||
root := "testdata/example.com/testWritesTemplate"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -141,9 +145,9 @@ func TestWritesTemplate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestExtantAborts ensures that a directory which contains an extant
|
||||
// TestClient_New_ExtantAborts ensures that a directory which contains an extant
|
||||
// Function does not reinitialize.
|
||||
func TestExtantAborts(t *testing.T) {
|
||||
func TestClient_New_ExtantAborts(t *testing.T) {
|
||||
root := "testdata/example.com/testExtantAborts"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -160,9 +164,9 @@ func TestExtantAborts(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestNonemptyAborts ensures that a directory which contains any
|
||||
// TestClient_New_NonemptyAborts ensures that a directory which contains any
|
||||
// (visible) files aborts.
|
||||
func TestNonemptyAborts(t *testing.T) {
|
||||
func TestClient_New_NonemptyAborts(t *testing.T) {
|
||||
root := "testdata/example.com/testNonemptyAborts"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -180,12 +184,12 @@ func TestNonemptyAborts(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestHiddenFilesIgnored ensures that initializing in a directory that
|
||||
// TestClient_New_HiddenFilesIgnored ensures that initializing in a directory that
|
||||
// only contains hidden files does not error, protecting against the naieve
|
||||
// implementation of aborting initialization if any files exist, which would
|
||||
// break functions tracked in source control (.git), or when used in
|
||||
// conjunction with other tools (.envrc, etc)
|
||||
func TestHiddenFilesIgnored(t *testing.T) {
|
||||
func TestClient_New_HiddenFilesIgnored(t *testing.T) {
|
||||
// Create a directory for the Function
|
||||
root := "testdata/example.com/testHiddenFilesIgnored"
|
||||
defer Using(t, root)()
|
||||
|
@ -204,7 +208,7 @@ func TestHiddenFilesIgnored(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesExtensible ensures that templates are extensible
|
||||
// TestClient_New_RepositoriesExtensible ensures that templates are extensible
|
||||
// using a custom path to template repositories on disk. The custom repositories
|
||||
// location is not defined herein but expected to be provided because, for
|
||||
// example, a CLI may want to use XDG_CONFIG_HOME. Assuming a repository path
|
||||
|
@ -213,7 +217,7 @@ func TestHiddenFilesIgnored(t *testing.T) {
|
|||
// $FUNC_REPOSITORIES/boson/go/json
|
||||
// See the CLI for full details, but a standard default location is
|
||||
// $HOME/.config/func/repositories/boson/go/json
|
||||
func TestRepositoriesExtensible(t *testing.T) {
|
||||
func TestClient_New_RepositoriesExtensible(t *testing.T) {
|
||||
root := "testdata/example.com/testRepositoriesExtensible"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -234,8 +238,9 @@ func TestRepositoriesExtensible(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRuntimeNotFound generates an error (embedded default repository).
|
||||
func TestRuntimeNotFound(t *testing.T) {
|
||||
// TestRuntime_New_RuntimeNotFoundError generates an error when the provided
|
||||
// runtime is not fo0und (embedded default repository).
|
||||
func TestClient_New_RuntimeNotFoundError(t *testing.T) {
|
||||
root := "testdata/example.com/testRuntimeNotFound"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -249,9 +254,9 @@ func TestRuntimeNotFound(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRuntimeNotFoundCustom ensures that the correct error is returned
|
||||
// TestClient_New_RuntimeNotFoundCustom ensures that the correct error is returned
|
||||
// when the requested runtime is not found in a given custom repository
|
||||
func TestRuntimeNotFoundCustom(t *testing.T) {
|
||||
func TestClient_New_RuntimeNotFoundCustom(t *testing.T) {
|
||||
root := "testdata/example.com/testRuntimeNotFoundCustom"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -271,8 +276,8 @@ func TestRuntimeNotFoundCustom(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateNotFound generates an error (embedded default repository).
|
||||
func TestTemplateNotFound(t *testing.T) {
|
||||
// TestClient_New_TemplateNotFoundError generates an error (embedded default repository).
|
||||
func TestClient_New_TemplateNotFoundError(t *testing.T) {
|
||||
root := "testdata/example.com/testTemplateNotFound"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -287,9 +292,9 @@ func TestTemplateNotFound(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateNotFoundCustom ensures that the correct error is returned
|
||||
// TestClient_New_TemplateNotFoundCustom ensures that the correct error is returned
|
||||
// when the requested template is not found in the given custom repository.
|
||||
func TestTemplateNotFoundCustom(t *testing.T) {
|
||||
func TestClient_New_TemplateNotFoundCustom(t *testing.T) {
|
||||
root := "testdata/example.com/testTemplateNotFoundCustom"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -308,9 +313,9 @@ func TestTemplateNotFoundCustom(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestNamed ensures that an explicitly passed name is used in leau of the
|
||||
// TestClient_New_Named ensures that an explicitly passed name is used in leau of the
|
||||
// path derived name when provided, and persists through instantiations.
|
||||
func TestNamed(t *testing.T) {
|
||||
func TestClient_New_Named(t *testing.T) {
|
||||
// Explicit name to use
|
||||
name := "service.example.com"
|
||||
|
||||
|
@ -335,7 +340,7 @@ func TestNamed(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRegistryRequired ensures that a registry is required, and is
|
||||
// TestClient_New_RegistryRequired ensures that a registry is required, and is
|
||||
// prepended with the DefaultRegistry if a single token.
|
||||
// Registry is the namespace at the container image registry.
|
||||
// If not prepended with the registry, it will be defaulted:
|
||||
|
@ -345,7 +350,7 @@ func TestNamed(t *testing.T) {
|
|||
// At this time a registry namespace is required as we rely on a third-party
|
||||
// registry in all cases. When we support in-cluster container registries,
|
||||
// this configuration parameter will become optional.
|
||||
func TestRegistryRequired(t *testing.T) {
|
||||
func TestClient_New_RegistryRequired(t *testing.T) {
|
||||
// Create a root for the Function
|
||||
root := "testdata/example.com/testRegistryRequired"
|
||||
defer Using(t, root)()
|
||||
|
@ -358,10 +363,10 @@ func TestRegistryRequired(t *testing.T) {
|
|||
fmt.Println(err)
|
||||
}
|
||||
|
||||
// TestDeriveImage ensures that the full image (tag) of the resultant OCI
|
||||
// TestClient_New_ImageNameDerived ensures that the full image (tag) of the resultant OCI
|
||||
// container is populated based of a derivation using configured registry
|
||||
// plus the service name.
|
||||
func TestDeriveImage(t *testing.T) {
|
||||
func TestClient_New_ImageNameDerived(t *testing.T) {
|
||||
// Create the root Function directory
|
||||
root := "testdata/example.com/testDeriveImage"
|
||||
defer Using(t, root)()
|
||||
|
@ -385,10 +390,10 @@ func TestDeriveImage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestDeriveImageDefaultRegistry ensures that a Registry which does not have
|
||||
// TestCleint_New_ImageRegistryDefaults ensures that a Registry which does not have
|
||||
// a registry prefix has the DefaultRegistry prepended.
|
||||
// For example "alice" becomes "docker.io/alice"
|
||||
func TestDeriveImageDefaultRegistry(t *testing.T) {
|
||||
func TestClient_New_ImageRegistryDefaults(t *testing.T) {
|
||||
// Create the root Function directory
|
||||
root := "testdata/example.com/testDeriveImageDefaultRegistry"
|
||||
defer Using(t, root)()
|
||||
|
@ -414,10 +419,10 @@ func TestDeriveImageDefaultRegistry(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestDelegation ensures that Create invokes each of the individual
|
||||
// TestClient_New_Delegation ensures that Create invokes each of the individual
|
||||
// subcomponents via delegation through Build, Push and
|
||||
// Deploy (and confirms expected fields calculated).
|
||||
func TestNewDelegates(t *testing.T) {
|
||||
func TestClient_New_Delegation(t *testing.T) {
|
||||
var (
|
||||
root = "testdata/example.com/testNewDelegates" // .. in which to initialize
|
||||
expectedName = "testNewDelegates" // expected to be derived
|
||||
|
@ -488,13 +493,14 @@ func TestNewDelegates(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRun ensures that the runner is invoked with the absolute path requested.
|
||||
func TestRun(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.
|
||||
func TestClient_Run(t *testing.T) {
|
||||
// Create the root Function directory
|
||||
root := "testdata/example.com/testRun"
|
||||
defer Using(t, root)()
|
||||
|
||||
// Create a client with the mock runner and the new test Function
|
||||
// client with the mock runner and the new test Function
|
||||
runner := mock.NewRunner()
|
||||
client := fn.New(fn.WithRegistry(TestRegistry), fn.WithRunner(runner))
|
||||
if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil {
|
||||
|
@ -502,9 +508,14 @@ func TestRun(t *testing.T) {
|
|||
}
|
||||
|
||||
// Run the newly created function
|
||||
if err := client.Run(context.Background(), root); err != nil {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
job, err := client.Run(ctx, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer job.Stop()
|
||||
|
||||
// Assert the runner was invoked, and with the expected root.
|
||||
if !runner.RunInvoked {
|
||||
|
@ -515,9 +526,54 @@ func TestRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestUpdate ensures that the deployer properly invokes the build/push/deploy
|
||||
// TestClient_Run_DataDir ensures that when a Function is created, it also
|
||||
// includes a .func (runtime data) directory which is registered as ignored for
|
||||
// Functions which will be tracked in git source control.
|
||||
// Note that this test is somewhat testing an implementation detail of `.Run(`
|
||||
// (it writes runtime data to files in .func) but since the feature of adding
|
||||
// .func to .gitignore is an important externally visible "feature", an explicit
|
||||
// test is warranted.
|
||||
func TestClient_Run_DataDir(t *testing.T) {
|
||||
root := "testdata/example.com/testRunDataDir"
|
||||
defer Using(t, root)()
|
||||
|
||||
// Create a function at root
|
||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||
if err := client.New(context.Background(), fn.Function{Root: root, Runtime: TestRuntime}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Assert the directory exists
|
||||
if _, err := os.Stat(filepath.Join(root, fn.RunDataDir)); os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Assert that .gitignore was also created and includes an ignore directove
|
||||
// for the .func directory
|
||||
if _, err := os.Stat(filepath.Join(root, ".gitignore")); os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Assert that .func is ignored
|
||||
file, err := os.Open(filepath.Join(root, ".gitignore"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Assert the directive exists
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
if scanner.Text() == "/"+fn.RunDataDir {
|
||||
return // success
|
||||
}
|
||||
}
|
||||
t.Errorf(".gitignore does not include '/%v' ignore directive", fn.RunDataDir)
|
||||
}
|
||||
|
||||
// TestClient_Update ensures that the deployer properly invokes the build/push/deploy
|
||||
// process, erroring if run on a directory uncreated.
|
||||
func TestUpdate(t *testing.T) {
|
||||
func TestClient_Update(t *testing.T) {
|
||||
var (
|
||||
root = "testdata/example.com/testUpdate"
|
||||
expectedName = "testUpdate"
|
||||
|
@ -591,9 +647,9 @@ func TestUpdate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRemoveByPath ensures that the remover is invoked to remove
|
||||
// TestClient_Remove_ByPath ensures that the remover is invoked to remove
|
||||
// the Function with the name of the function at the provided root.
|
||||
func TestRemoveByPath(t *testing.T) {
|
||||
func TestClient_Remove_ByPath(t *testing.T) {
|
||||
var (
|
||||
root = "testdata/example.com/testRemoveByPath"
|
||||
expectedName = "testRemoveByPath"
|
||||
|
@ -627,9 +683,9 @@ func TestRemoveByPath(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
// TestRemoveByName ensures that the remover is invoked to remove the function
|
||||
// TestClient_Remove_ByName ensures that the remover is invoked to remove the function
|
||||
// of the name provided, with precidence over a provided root path.
|
||||
func TestRemoveByName(t *testing.T) {
|
||||
func TestClient_Remove_ByName(t *testing.T) {
|
||||
var (
|
||||
root = "testdata/example.com/testRemoveByName"
|
||||
expectedName = "explicitName.example.com"
|
||||
|
@ -668,11 +724,11 @@ func TestRemoveByName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRemoveUninitializedFails ensures that attempting to remove a Function
|
||||
// TestClient_Remove_UninitializedFails ensures that removing a Function
|
||||
// by path only (no name) fails unless the Function has been initialized. I.e.
|
||||
// the name will not be derived from path and the Function removed by this
|
||||
// derived name; which could be unexpected and destructive.
|
||||
func TestRemoveUninitializedFails(t *testing.T) {
|
||||
func TestClient_Remove_UninitializedFails(t *testing.T) {
|
||||
var (
|
||||
root = "testdata/example.com/testRemoveUninitializedFails"
|
||||
remover = mock.NewRemover()
|
||||
|
@ -695,8 +751,8 @@ func TestRemoveUninitializedFails(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestList merely ensures that the client invokes the configured lister.
|
||||
func TestList(t *testing.T) {
|
||||
// TestClient_List merely ensures that the client invokes the configured lister.
|
||||
func TestClient_List(t *testing.T) {
|
||||
lister := mock.NewLister()
|
||||
|
||||
client := fn.New(fn.WithLister(lister)) // lists deployed Functions.
|
||||
|
@ -710,11 +766,11 @@ func TestList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestListOutsideRoot ensures that a call to a Function (in this case list)
|
||||
// TestClient_List_OutsideRoot ensures that a call to a Function (in this case list)
|
||||
// that is not contextually dependent on being associated with a Function,
|
||||
// can be run from anywhere, thus ensuring that the client itself makes
|
||||
// a distinction between Function-scoped methods and not.
|
||||
func TestListOutsideRoot(t *testing.T) {
|
||||
func TestClient_List_OutsideRoot(t *testing.T) {
|
||||
lister := mock.NewLister()
|
||||
|
||||
// Instantiate in the current working directory, with no name.
|
||||
|
@ -729,10 +785,10 @@ func TestListOutsideRoot(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestDeployUnbuilt ensures that a call to deploy a Function which was not
|
||||
// TestClient_Deploy_UnbuiltErrors ensures that a call to deploy a Function which was not
|
||||
// fully created (ie. was only initialized, not actually built and deploys)
|
||||
// yields an expected, and informative, error.
|
||||
func TestDeployUnbuilt(t *testing.T) {
|
||||
func TestClient_Deploy_UnbuiltErrors(t *testing.T) {
|
||||
root := "testdata/example.com/testDeployUnbuilt" // Root from which to run the test
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -755,34 +811,10 @@ func TestDeployUnbuilt(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestEmit ensures that the client properly invokes the emitter when provided
|
||||
func TestEmit(t *testing.T) {
|
||||
sink := "http://testy.mctestface.com"
|
||||
emitter := mock.NewEmitter()
|
||||
|
||||
// Ensure sink passthrough from client
|
||||
emitter.EmitFn = func(s string) error {
|
||||
if s != sink {
|
||||
t.Fatalf("Unexpected sink %v\n", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Instantiate in the current working directory, with no name.
|
||||
client := fn.New(fn.WithEmitter(emitter))
|
||||
|
||||
if err := client.Emit(context.Background(), sink); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !emitter.EmitInvoked {
|
||||
t.Fatal("Client did not invoke emitter.Emit()")
|
||||
}
|
||||
}
|
||||
|
||||
// Asserts that the client properly writes user provided Builders
|
||||
// to the Function configuration but uses internal default if
|
||||
// not provided.
|
||||
func TestWithConfiguredBuilders(t *testing.T) {
|
||||
// TestClient_New_BuildersPersisted Asserts that the client preserves user-
|
||||
// provided Builders on the Function configuration with the internal default
|
||||
// if not provided.
|
||||
func TestClient_New_BuildersPersisted(t *testing.T) {
|
||||
root := "testdata/example.com/testConfiguredBuilders" // Root from which to run the test
|
||||
defer Using(t, root)()
|
||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||
|
@ -796,7 +828,7 @@ func TestWithConfiguredBuilders(t *testing.T) {
|
|||
}}
|
||||
|
||||
// Create the Function, which should preserve custom builders
|
||||
if err := client.Create(f0); err != nil {
|
||||
if err := client.New(context.Background(), f0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -817,10 +849,10 @@ func TestWithConfiguredBuilders(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Asserts that the client properly sets user provided Builders
|
||||
// in the Function configuration, and if one of the provided is
|
||||
// keyed as "default", this is set as the default Builder.
|
||||
func TestWithConfiguredBuildersWithDefault(t *testing.T) {
|
||||
// TestClient_New_BuilderDefault ensures that if a custom builder is
|
||||
// provided of name "default", this is chosen as the default builder instead
|
||||
// of the inbuilt static default.
|
||||
func TestClient_New_BuilderDefault(t *testing.T) {
|
||||
root := "testdata/example.com/testConfiguredBuildersWithDefault" // Root from which to run the test
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -829,7 +861,7 @@ func TestWithConfiguredBuildersWithDefault(t *testing.T) {
|
|||
"default": "docker.io/example/default",
|
||||
}
|
||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||
if err := client.Create(fn.Function{
|
||||
if err := client.New(context.Background(), fn.Function{
|
||||
Runtime: TestRuntime,
|
||||
Root: root,
|
||||
Builders: builders,
|
||||
|
@ -852,9 +884,9 @@ func TestWithConfiguredBuildersWithDefault(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Asserts that the client properly sets the Buildpacks property
|
||||
// in the Function configuration when it is provided.
|
||||
func TestWithConfiguredBuildpacks(t *testing.T) {
|
||||
// TestClient_New_BuildpacksPersisted ensures that provided buildpacks are
|
||||
// persisted on new Functions.
|
||||
func TestClient_New_BuildpacksPersisted(t *testing.T) {
|
||||
root := "testdata/example.com/testConfiguredBuildpacks" // Root from which to run the test
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -862,7 +894,7 @@ func TestWithConfiguredBuildpacks(t *testing.T) {
|
|||
"docker.io/example/custom-buildpack",
|
||||
}
|
||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||
if err := client.Create(fn.Function{
|
||||
if err := client.New(context.Background(), fn.Function{
|
||||
Runtime: TestRuntime,
|
||||
Root: root,
|
||||
Buildpacks: buildpacks,
|
||||
|
@ -880,8 +912,8 @@ func TestWithConfiguredBuildpacks(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRuntimes ensures that the total set of runtimes are returned.
|
||||
func TestRuntimes(t *testing.T) {
|
||||
// TestClient_Runtimes ensures that the total set of runtimes are returned.
|
||||
func TestClient_Runtimes(t *testing.T) {
|
||||
// TODO: test when a specific repo override is indicated
|
||||
// (remote repo which takes precidence over embedded and extended)
|
||||
|
||||
|
@ -925,9 +957,9 @@ func TestRuntimes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCreateStamp ensures that the creation timestamp is set on functions
|
||||
// TestClient_New_Timestamp ensures that the creation timestamp is set on functions
|
||||
// which are successfully initialized using the client library.
|
||||
func TestCreateStamp(t *testing.T) {
|
||||
func TestClient_New_Timestamp(t *testing.T) {
|
||||
root := "testdata/example.com/testCreateStamp"
|
||||
defer Using(t, root)()
|
||||
|
||||
|
@ -947,3 +979,222 @@ func TestCreateStamp(t *testing.T) {
|
|||
t.Fatalf("expected function timestamp to be after '%v', got '%v'", start, f.Created)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_Invoke_HTTP ensures that the client will attempt to invoke a default HTTP
|
||||
// Function using a simple HTTP POST method with the invoke message as form
|
||||
// field values (as though a simple form were posted).
|
||||
func TestClient_Invoke_HTTP(t *testing.T) {
|
||||
root := "testdata/example.com/testInvokeHTTP"
|
||||
defer Using(t, root)()
|
||||
|
||||
// Flag indicating the Function was invoked
|
||||
var invoked bool
|
||||
|
||||
// The message to send to the Function
|
||||
// Individual fields can be overridden, by default all fields are populeted
|
||||
// with values intended as illustrative examples plus a unique request ID.
|
||||
message := fn.NewInvokeMessage()
|
||||
|
||||
// An HTTP handler which masquarades as a running Function and verifies the
|
||||
// invoker POSTed the invocation message.
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
|
||||
invoked = true
|
||||
if err := req.ParseForm(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Verify that we POST to HTTP endpoints by default
|
||||
if req.Method != "POST" {
|
||||
t.Fatalf("expected 'POST' request, got '%v'", req.Method)
|
||||
}
|
||||
// Verify the values came through via a spot-check of the unique ID
|
||||
if req.Form.Get("ID") != message.ID {
|
||||
t.Fatalf("expected message ID '%v', got '%v'", message.ID, req.Form.Get("ID"))
|
||||
}
|
||||
})
|
||||
|
||||
// Expose the masquarading Function on an OS-chosen port.
|
||||
l, err := net.Listen("tcp4", "127.0.0.1:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := http.Server{Handler: handler}
|
||||
go func() {
|
||||
if err = s.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "error serving: %v", err)
|
||||
}
|
||||
}()
|
||||
defer s.Close()
|
||||
|
||||
// Create a client with a mock runner which will report the port at which the
|
||||
// interloping Function is listening.
|
||||
runner := mock.NewRunner()
|
||||
runner.RunFn = func(ctx context.Context, f fn.Function) (*fn.Job, error) {
|
||||
_, p, _ := net.SplitHostPort(l.Addr().String())
|
||||
errs := make(chan error, 10)
|
||||
stop := func() {}
|
||||
return fn.NewJob(f, p, errs, stop)
|
||||
}
|
||||
client := fn.New(fn.WithRegistry(TestRegistry), fn.WithRunner(runner))
|
||||
|
||||
// Create a new default HTTP function
|
||||
f := fn.Function{Runtime: TestRuntime, Root: root, Template: "http"}
|
||||
if err := client.New(context.Background(), f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run the Function
|
||||
job, err := client.Run(context.Background(), root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer job.Stop()
|
||||
|
||||
// Invoke the Function, which will use the mock Runner
|
||||
if err := client.Invoke(context.Background(), f.Root, "", message); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fail if the Function was never invoked.
|
||||
if !invoked {
|
||||
t.Fatal("Function was not invoked")
|
||||
}
|
||||
|
||||
// Also fail if the mock runner was never invoked.
|
||||
if !runner.RunInvoked {
|
||||
t.Fatal("the runner was not")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_Invoke_CloudEvent ensures that the client will attempt to invoke a
|
||||
// default CloudEvent Function. This also uses the HTTP protocol but asserts
|
||||
// the invoker is sending the invocation message as a CloudEvent rather than
|
||||
// a standard HTTP form POST.
|
||||
func TestClient_Invoke_CloudEvent(t *testing.T) {
|
||||
root := "testdata/example.com/testInvokeCloudEvent"
|
||||
defer Using(t, root)()
|
||||
|
||||
var (
|
||||
invoked bool // flag the Function was invoked
|
||||
ctx = context.Background()
|
||||
message = fn.NewInvokeMessage() // message to send to the Function
|
||||
)
|
||||
|
||||
// A CloudEvent Receiver which masquarades as a running Function and
|
||||
// verifies the invoker sent the message as a populated CloudEvent.
|
||||
receiver := func(ctx context.Context, event cloudevents.Event) {
|
||||
invoked = true
|
||||
if event.ID() != message.ID {
|
||||
t.Fatalf("expected event ID '%v', got '%v'", message.ID, event.ID())
|
||||
}
|
||||
}
|
||||
|
||||
// A cloudevent receive handler which will expect the HTTP protocol
|
||||
protocol, err := cloudevents.NewHTTP() // Use HTTP protocol when receiving
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
handler, err := cloudevents.NewHTTPReceiveHandler(ctx, protocol, receiver)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Listen and serve on an OS-chosen port
|
||||
l, err := net.Listen("tcp4", "127.0.0.1:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := http.Server{Handler: handler}
|
||||
go func() {
|
||||
if err := s.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "error serving: %v", err)
|
||||
}
|
||||
}()
|
||||
defer s.Close()
|
||||
|
||||
// Create a client with a mock Runner which returns its address.
|
||||
runner := mock.NewRunner()
|
||||
runner.RunFn = func(ctx context.Context, f fn.Function) (*fn.Job, error) {
|
||||
_, p, _ := net.SplitHostPort(l.Addr().String())
|
||||
errs := make(chan error, 10)
|
||||
stop := func() {}
|
||||
return fn.NewJob(f, p, errs, stop)
|
||||
}
|
||||
client := fn.New(fn.WithRegistry(TestRegistry), fn.WithRunner(runner))
|
||||
|
||||
// Create a new default CloudEvents function
|
||||
f := fn.Function{Runtime: TestRuntime, Root: root, Template: "cloudevents"}
|
||||
if err := client.New(context.Background(), f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run the Function
|
||||
job, err := client.Run(context.Background(), root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer job.Stop()
|
||||
|
||||
// Invoke the Function, which will use the mock Runner
|
||||
if err := client.Invoke(context.Background(), f.Root, "", message); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fail if the Function was never invoked.
|
||||
if !invoked {
|
||||
t.Fatal("Function was not invoked")
|
||||
}
|
||||
|
||||
// Also fail if the mock runner was never invoked.
|
||||
if !runner.RunInvoked {
|
||||
t.Fatal("the runner was not invoked")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_Instances ensures that when a Function is run (locally) its metadata
|
||||
// is available to other clients inspecting the same Function using .Instances
|
||||
func TestClient_Instances(t *testing.T) {
|
||||
root := "testdata/example.com/testInstances"
|
||||
defer Using(t, root)()
|
||||
|
||||
// A mock runner
|
||||
runner := mock.NewRunner()
|
||||
runner.RunFn = func(_ context.Context, f fn.Function) (*fn.Job, error) {
|
||||
errs := make(chan error, 10)
|
||||
stop := func() {}
|
||||
return fn.NewJob(f, "8080", errs, stop)
|
||||
}
|
||||
|
||||
// Client with the mock runner
|
||||
client := fn.New(fn.WithRegistry(TestRegistry), fn.WithRunner(runner))
|
||||
|
||||
// Create the new Function
|
||||
if err := client.New(context.Background(), fn.Function{Root: root, Runtime: TestRuntime}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Run the function, awaiting start and then canceling
|
||||
job, err := client.Run(context.Background(), root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer job.Stop()
|
||||
|
||||
// Load the (now fully initialized) Function metadata
|
||||
f, err := fn.NewFunction(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get the local function instance info
|
||||
instance, err := client.Instances().Local(context.Background(), f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Assert the endpoint route is as expected
|
||||
expectedEndpoint := "http://localhost:8080/"
|
||||
if instance.Route != expectedEndpoint {
|
||||
t.Fatalf("Expected endpoint '%v', got '%v'", expectedEndpoint, instance.Route)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
package cloudevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
nethttp "net/http"
|
||||
|
||||
cloudevents "github.com/cloudevents/sdk-go/v2"
|
||||
"github.com/cloudevents/sdk-go/v2/client"
|
||||
"github.com/cloudevents/sdk-go/v2/event"
|
||||
"github.com/cloudevents/sdk-go/v2/protocol/http"
|
||||
"github.com/cloudevents/sdk-go/v2/types"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSource = "/boson/fn"
|
||||
DefaultType = "boson.fn"
|
||||
)
|
||||
|
||||
type Emitter struct {
|
||||
Endpoint string
|
||||
Source string
|
||||
Type string
|
||||
Id string
|
||||
Data string
|
||||
ContentType string
|
||||
Transport nethttp.RoundTripper
|
||||
}
|
||||
|
||||
func NewEmitter() *Emitter {
|
||||
return &Emitter{
|
||||
Source: DefaultSource,
|
||||
Type: DefaultType,
|
||||
Id: uuid.NewString(),
|
||||
Data: "",
|
||||
ContentType: event.TextPlain,
|
||||
Transport: nethttp.DefaultTransport,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Emitter) Emit(ctx context.Context, endpoint string) (err error) {
|
||||
p, err := http.New(http.WithTarget(endpoint), http.WithRoundTripper(e.Transport))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := client.New(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evt := event.Event{
|
||||
Context: event.EventContextV1{
|
||||
Type: e.Type,
|
||||
Source: *types.ParseURIRef(e.Source),
|
||||
ID: e.Id,
|
||||
}.AsV1(),
|
||||
}
|
||||
if err = evt.SetData(e.ContentType, e.Data); err != nil {
|
||||
return
|
||||
}
|
||||
event, result := c.Request(ctx, evt)
|
||||
if !cloudevents.IsACK(result) {
|
||||
return fmt.Errorf(result.Error())
|
||||
}
|
||||
if event != nil {
|
||||
fmt.Printf("%v", event)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
//go:build !integration
|
||||
// +build !integration
|
||||
|
||||
package cloudevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudevents/sdk-go/v2/client"
|
||||
"github.com/cloudevents/sdk-go/v2/event"
|
||||
"github.com/cloudevents/sdk-go/v2/protocol/http"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func makeClient(t *testing.T) (c client.Client, p *http.Protocol) {
|
||||
p, err := http.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err = client.New(p)
|
||||
if err != nil {
|
||||
t.Errorf("failed to make client %s", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func receiveEvents(t *testing.T, ctx context.Context, events chan<- event.Event) (p *http.Protocol) {
|
||||
c, p := makeClient(t)
|
||||
go func() {
|
||||
err := c.StartReceiver(ctx, func(ctx context.Context, event event.Event) error {
|
||||
go func() {
|
||||
events <- event
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to start receiver %s", err.Error())
|
||||
}
|
||||
}()
|
||||
time.Sleep(1 * time.Second) // let the server start
|
||||
return
|
||||
}
|
||||
|
||||
func TestEmitterDefaults(t *testing.T) {
|
||||
events := make(chan event.Event)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// start a cloudevent client that receives events
|
||||
// and sends them to a channel
|
||||
p := receiveEvents(t, ctx, events)
|
||||
|
||||
emitter := NewEmitter()
|
||||
if err := emitter.Emit(ctx, fmt.Sprintf("http://localhost:%v", p.GetListeningPort())); err != nil {
|
||||
t.Fatalf("Error emitting event: %v\n", err)
|
||||
}
|
||||
|
||||
// received event
|
||||
got := <-events
|
||||
|
||||
cancel() // stop the client
|
||||
time.Sleep(1 * time.Second) // let the server stop
|
||||
|
||||
if got.Source() != "/boson/fn" {
|
||||
t.Fatal("Expected /boson/fn as default source")
|
||||
}
|
||||
if got.Type() != "boson.fn" {
|
||||
t.Fatal("Expected boson.fn as default type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitter(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
cesource string
|
||||
cetype string
|
||||
ceid string
|
||||
cedata string
|
||||
}{
|
||||
"with-source": {
|
||||
cesource: "/my/source",
|
||||
},
|
||||
"with-type": {
|
||||
cetype: "my.type",
|
||||
},
|
||||
"with-id": {
|
||||
ceid: "11223344",
|
||||
},
|
||||
"with-data": {
|
||||
cedata: "Some event data",
|
||||
},
|
||||
}
|
||||
for n, tc := range testCases {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
events := make(chan event.Event)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// start a cloudevent client that receives events
|
||||
// and sends them to a channel
|
||||
p := receiveEvents(t, ctx, events)
|
||||
|
||||
emitter := NewEmitter()
|
||||
|
||||
if tc.cesource != "" {
|
||||
emitter.Source = tc.cesource
|
||||
}
|
||||
if tc.cetype != "" {
|
||||
emitter.Type = tc.cetype
|
||||
}
|
||||
if tc.ceid != "" {
|
||||
emitter.Id = tc.ceid
|
||||
}
|
||||
if tc.cedata != "" {
|
||||
emitter.Data = tc.cedata
|
||||
}
|
||||
if err := emitter.Emit(ctx, fmt.Sprintf("http://localhost:%v", p.GetListeningPort())); err != nil {
|
||||
t.Fatalf("Error emitting event: %v\n", err)
|
||||
}
|
||||
|
||||
// received event
|
||||
got := <-events
|
||||
|
||||
cancel() // stop the client
|
||||
time.Sleep(100 * time.Millisecond) // let the server stop
|
||||
|
||||
if tc.cesource != "" && got.Source() != tc.cesource {
|
||||
t.Fatalf("%s: Expected %s as source, got %s", n, tc.cesource, got.Source())
|
||||
}
|
||||
if tc.cetype != "" && got.Type() != tc.cetype {
|
||||
t.Fatalf("%s: Expected %s as type, got %s", n, tc.cetype, got.Type())
|
||||
}
|
||||
if tc.ceid != "" && got.ID() != tc.ceid {
|
||||
t.Fatalf("%s: Expected %s as id, got %s", n, tc.ceid, got.ID())
|
||||
}
|
||||
if tc.cedata != "" {
|
||||
if diff := cmp.Diff(tc.cedata, string(got.Data())); diff != "" {
|
||||
t.Errorf("Unexpected difference (-want, +got): %v", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -12,9 +12,9 @@ import (
|
|||
"knative.dev/kn-plugin-func/mock"
|
||||
)
|
||||
|
||||
// TestBuildInvalidRegistry ensures that running build specifying the name of the
|
||||
// TestBuild_InvalidRegistry ensures that running build specifying the name of the
|
||||
// registry explicitly as an argument invokes the registry validation code.
|
||||
func TestBuildInvalidRegistry(t *testing.T) {
|
||||
func TestBuild_InvalidRegistry(t *testing.T) {
|
||||
var (
|
||||
args = []string{"--registry", "foo/bar/foobar/boofar"} // provide an invalid registry name
|
||||
builder = mock.NewBuilder() // with a mock builder
|
||||
|
@ -54,7 +54,7 @@ created: 2021-01-01T00:00:00+00:00
|
|||
}
|
||||
}
|
||||
|
||||
func Test_runBuild(t *testing.T) {
|
||||
func TestBuild_runBuild(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pushFlag bool
|
||||
|
@ -142,7 +142,7 @@ created: 2009-11-10 23:00:00`,
|
|||
}
|
||||
}
|
||||
|
||||
func Test_newBuildClient(t *testing.T) {
|
||||
func TestBuild_newBuildClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg buildConfig
|
||||
|
|
|
@ -56,7 +56,7 @@ NAME
|
|||
{{.Prefix}}func create - Create a Function project.
|
||||
|
||||
SYNOPSIS
|
||||
func create [-l|--language] [-t|--template] [-r|--repository]
|
||||
{{.Prefix}}func create [-l|--language] [-t|--template] [-r|--repository]
|
||||
[-c|--confirm] [-v|--verbose] [path]
|
||||
|
||||
DESCRIPTION
|
||||
|
@ -292,6 +292,53 @@ func newCreateConfig(args []string, clientFn createClientFn) (cfg createConfig,
|
|||
return
|
||||
}
|
||||
|
||||
// Validate the current state of the config, returning any errors.
|
||||
// Note this is a deeper validation using a client already configured with a
|
||||
// preliminary config object from flags/config, such that the client instance
|
||||
// can be used to determine possible values for runtime, templates, etc. a
|
||||
// pre-client validation should not be required, as the Client does its own
|
||||
// validation.
|
||||
func (c createConfig) Validate(client *fn.Client) (err error) {
|
||||
|
||||
// Confirm Name is valid
|
||||
// Note that this is highly constricted, as it must currently adhere to the
|
||||
// naming of a Knative Service, which itself is constrained to a Kubernetes
|
||||
// Service, which itself is constrained to a DNS label (a subdomain).
|
||||
// TODO: refactor to be git-like with no name at time of creation, but rather
|
||||
// with named deployment targets in a one-to-many configuration.
|
||||
dirName, _ := deriveNameAndAbsolutePathFromPath(c.Path)
|
||||
if err = utils.ValidateFunctionName(dirName); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Runtime and Template Name
|
||||
//
|
||||
// Perhaps additional validation would be of use here in the CLI, but
|
||||
// the client libray itself is ultimately responsible for validating all input
|
||||
// prior to exeuting any requests.
|
||||
// Client validates both language runtime and template exist, with language runtime
|
||||
// being a mandatory flag while defaulting template if not present to 'http'.
|
||||
// However, if either of them are invalid, or the chosen combination does not exist,
|
||||
// the error message is a rather terse one-liner. This is suitable for libraries, but
|
||||
// for a CLI it behooves us to be more verbose, including valid options for
|
||||
// each. So here, we check that the values entered (if any) are both valid
|
||||
// and valid together.
|
||||
if c.Runtime == "" {
|
||||
return noRuntimeError(client)
|
||||
}
|
||||
if c.Runtime != "" && c.Repository == "" &&
|
||||
!isValidRuntime(client, c.Runtime) {
|
||||
return newInvalidRuntimeError(client, c.Runtime)
|
||||
}
|
||||
|
||||
if c.Template != "" && c.Repository == "" &&
|
||||
!isValidTemplate(client, c.Runtime, c.Template) {
|
||||
return newInvalidTemplateError(client, c.Runtime, c.Template)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// isValidRuntime determines if the given language runtime is a valid choice.
|
||||
func isValidRuntime(client *fn.Client, runtime string) bool {
|
||||
runtimes, err := client.Runtimes()
|
||||
|
@ -373,53 +420,6 @@ func newInvalidTemplateError(client *fn.Client, runtime, template string) error
|
|||
return ErrInvalidTemplate(errors.New(b.String()))
|
||||
}
|
||||
|
||||
// Validate the current state of the config, returning any errors.
|
||||
// Note this is a deeper validation using a client already configured with a
|
||||
// preliminary config object from flags/config, such that the client instance
|
||||
// can be used to determine possible values for runtime, templates, etc. a
|
||||
// pre-client validation should not be required, as the Client does its own
|
||||
// validation.
|
||||
func (c createConfig) Validate(client *fn.Client) (err error) {
|
||||
|
||||
// Confirm Name is valid
|
||||
// Note that this is highly constricted, as it must currently adhere to the
|
||||
// naming of a Knative Service, which itself is constrained to a Kubernetes
|
||||
// Service, which itself is constrained to a DNS label (a subdomain).
|
||||
// TODO: refactor to be git-like with no name at time of creation, but rather
|
||||
// with named deployment targets in a one-to-many configuration.
|
||||
dirName, _ := deriveNameAndAbsolutePathFromPath(c.Path)
|
||||
if err = utils.ValidateFunctionName(dirName); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Runtime and Template Name
|
||||
//
|
||||
// Perhaps additional validation would be of use here in the CLI, but
|
||||
// the client libray itself is ultimately responsible for validating all input
|
||||
// prior to exeuting any requests.
|
||||
// Client validates both language runtime and template exist, with language runtime
|
||||
// being a mandatory flag while defaulting template if not present to 'http'.
|
||||
// However, if either of them are invalid, or the chosen combination does not exist,
|
||||
// the error message is a rather terse one-liner. This is suitable for libraries, but
|
||||
// for a CLI it behooves us to be more verbose, including valid options for
|
||||
// each. So here, we check that the values entered (if any) are both valid
|
||||
// and valid together.
|
||||
if c.Runtime == "" {
|
||||
return noRuntimeError(client)
|
||||
}
|
||||
if c.Runtime != "" && c.Repository == "" &&
|
||||
!isValidRuntime(client, c.Runtime) {
|
||||
return newInvalidRuntimeError(client, c.Runtime)
|
||||
}
|
||||
|
||||
if c.Template != "" && c.Repository == "" &&
|
||||
!isValidTemplate(client, c.Runtime, c.Template) {
|
||||
return newInvalidTemplateError(client, c.Runtime, c.Template)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// prompt the user with value of config members, allowing for interactively
|
||||
// mutating the values. The provided clientFn is used to construct a transient
|
||||
// client for use during prompt autocompletion/suggestions (such as suggesting
|
||||
|
|
|
@ -12,9 +12,9 @@ import (
|
|||
"knative.dev/kn-plugin-func/utils"
|
||||
)
|
||||
|
||||
// TestCreate ensures that an invocation of create with minimal settings
|
||||
// TestCreate_Execute ensures that an invocation of create with minimal settings
|
||||
// and valid input completes without error; degenerate case.
|
||||
func TestCreate(t *testing.T) {
|
||||
func TestCreate_Execute(t *testing.T) {
|
||||
defer fromTempDir(t)()
|
||||
|
||||
// command with a client factory which yields a fully default client.
|
||||
|
@ -28,9 +28,9 @@ func TestCreate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCreateWithNoRuntime ensures that an invocation of create must be
|
||||
// TestCreate_NoRuntime ensures that an invocation of create must be
|
||||
// done with a runtime.
|
||||
func TestCreateWithNoRuntime(t *testing.T) {
|
||||
func TestCreate_NoRuntime(t *testing.T) {
|
||||
defer fromTempDir(t)()
|
||||
|
||||
// command with a client factory which yields a fully default client.
|
||||
|
@ -44,9 +44,9 @@ func TestCreateWithNoRuntime(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCreateWithNoRuntime ensures that an invocation of create must be
|
||||
// TestCreate_WithNoRuntime ensures that an invocation of create must be
|
||||
// done with one of the valid runtimes only.
|
||||
func TestCreateWithInvalidRuntime(t *testing.T) {
|
||||
func TestCreate_WithInvalidRuntime(t *testing.T) {
|
||||
defer fromTempDir(t)()
|
||||
|
||||
// command with a client factory which yields a fully default client.
|
||||
|
@ -62,9 +62,9 @@ func TestCreateWithInvalidRuntime(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCreateWithInvalidTemplate ensures that an invocation of create must be
|
||||
// TestCreate_InvalidTemplate ensures that an invocation of create must be
|
||||
// done with one of the valid templates only.
|
||||
func TestCreateWithInvalidTemplate(t *testing.T) {
|
||||
func TestCreate_InvalidTemplate(t *testing.T) {
|
||||
defer fromTempDir(t)()
|
||||
|
||||
// command with a client factory which yields a fully default client.
|
||||
|
@ -81,9 +81,9 @@ func TestCreateWithInvalidTemplate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCreateValidatesName ensures that the create command only accepts
|
||||
// TestCreate_ValidatesName ensures that the create command only accepts
|
||||
// DNS-1123 labels for Function name.
|
||||
func TestCreateValidatesName(t *testing.T) {
|
||||
func TestCreate_ValidatesName(t *testing.T) {
|
||||
defer fromTempDir(t)()
|
||||
|
||||
// Create a new Create command with a fn.Client construtor
|
||||
|
@ -100,10 +100,10 @@ func TestCreateValidatesName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCreateRepositoriesPath ensures that the create command utilizes the
|
||||
// TestCreate_RepositoriesPath ensures that the create command utilizes the
|
||||
// expected repositories path, respecting the setting for XDG_CONFIG_PATH
|
||||
// when deriving the default
|
||||
func TestCreateRepositoriesPath(t *testing.T) {
|
||||
func TestCreate_RepositoriesPath(t *testing.T) {
|
||||
defer fromTempDir(t)()
|
||||
|
||||
// Update XDG_CONFIG_HOME to point to some arbitrary location.
|
||||
|
|
|
@ -8,9 +8,9 @@ import (
|
|||
"knative.dev/kn-plugin-func/mock"
|
||||
)
|
||||
|
||||
// TestDeleteByName ensures that running delete specifying the name of the
|
||||
// TestDelete_ByName ensures that running delete specifying the name of the
|
||||
// Function explicitly as an argument invokes the remover appropriately.
|
||||
func TestDeleteByName(t *testing.T) {
|
||||
func TestDelete_ByName(t *testing.T) {
|
||||
var (
|
||||
testname = "testname" // explicit name for the Function
|
||||
args = []string{testname} // passed as the lone argument
|
||||
|
@ -45,9 +45,9 @@ func TestDeleteByName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestDeleteByProject ensures that running delete with a valid project as its
|
||||
// TestDelete_ByProject ensures that running delete with a valid project as its
|
||||
// context invokes remove and with the correct name (reads name from func.yaml)
|
||||
func TestDeleteByProject(t *testing.T) {
|
||||
func TestDelete_ByProject(t *testing.T) {
|
||||
// from within a new temporary directory
|
||||
defer fromTempDir(t)()
|
||||
|
||||
|
@ -97,12 +97,12 @@ created: 2021-01-01T00:00:00+00:00
|
|||
}
|
||||
}
|
||||
|
||||
// TestDeleteNameAndPathExclusivity ensures that providing both a name and a
|
||||
// TestDelete_NameAndPathExclusivity ensures that providing both a name and a
|
||||
// path generates an error.
|
||||
// Providing the --path (-p) flag indicates the name of the function to delete
|
||||
// is to be taken from the Function at the given path. Providing the name as
|
||||
// an argument as well is therefore redundant and an error.
|
||||
func TestDeleteNameAndPathExclusivity(t *testing.T) {
|
||||
func TestDelete_NameAndPathExclusivity(t *testing.T) {
|
||||
|
||||
// A mock remover which will be sampled to ensure it is not invoked.
|
||||
remover := mock.NewRemover()
|
||||
|
|
209
cmd/emit.go
209
cmd/emit.go
|
@ -1,209 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/viper"
|
||||
"github.com/spf13/cobra"
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
"knative.dev/kn-plugin-func/cloudevents"
|
||||
fnhttp "knative.dev/kn-plugin-func/http"
|
||||
"knative.dev/kn-plugin-func/knative"
|
||||
)
|
||||
|
||||
func init() {
|
||||
root.AddCommand(NewEmitCmd(newEmitClient))
|
||||
}
|
||||
|
||||
// create a fn.Client with an instance of a
|
||||
func newEmitClient(cfg emitConfig) (*fn.Client, error) {
|
||||
e := cloudevents.NewEmitter()
|
||||
e.Id = cfg.Id
|
||||
e.Source = cfg.Source
|
||||
e.Type = cfg.Type
|
||||
e.ContentType = cfg.ContentType
|
||||
e.Data = cfg.Data
|
||||
if e.Transport != nil {
|
||||
e.Transport = cfg.Transport
|
||||
}
|
||||
if cfg.File != "" {
|
||||
// See config.Validate for --Data and --file exclusivity enforcement
|
||||
b, err := ioutil.ReadFile(cfg.File)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.Data = string(b)
|
||||
}
|
||||
|
||||
return fn.New(fn.WithEmitter(e)), nil
|
||||
}
|
||||
|
||||
type emitClientFn func(emitConfig) (*fn.Client, error)
|
||||
|
||||
func NewEmitCmd(clientFn emitClientFn) *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "emit",
|
||||
Short: "Emit a CloudEvent to a function endpoint",
|
||||
Long: `Emit event
|
||||
|
||||
Emits a CloudEvent, sending it to the deployed function.
|
||||
`,
|
||||
Example: `
|
||||
# Send a CloudEvent to the deployed function with no data and default values
|
||||
# for source, type and ID
|
||||
kn func emit
|
||||
|
||||
# Send a CloudEvent to the deployed function with the data found in ./test.json
|
||||
kn func emit --file ./test.json
|
||||
|
||||
# Send a CloudEvent to the function running locally with a CloudEvent containing
|
||||
# "Hello World!" as the data field, with a content type of "text/plain"
|
||||
kn func emit --data "Hello World!" --content-type "text/plain" -s local
|
||||
|
||||
# Send a CloudEvent to the function running locally with an event type of "my.event"
|
||||
kn func emit --type my.event --sink local
|
||||
|
||||
# Send a CloudEvent to the deployed function found at /path/to/fn with an id of "fn.test"
|
||||
kn func emit --path /path/to/fn -i fn.test
|
||||
|
||||
# Send a CloudEvent to an arbitrary endpoint
|
||||
kn func emit --sink "http://my.event.broker.com"
|
||||
`,
|
||||
SuggestFor: []string{"meit", "emti", "send"},
|
||||
PreRunE: bindEnv("source", "type", "id", "data", "file", "path", "sink", "content-type"),
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("sink", "k", "", "Send the CloudEvent to the function running at [sink]. The special value \"local\" can be used to send the event to a function running on the local host. When provided, the --path flag is ignored (Env: $FUNC_SINK)")
|
||||
cmd.Flags().StringP("source", "s", cloudevents.DefaultSource, "CloudEvent source (Env: $FUNC_SOURCE)")
|
||||
cmd.Flags().StringP("type", "t", cloudevents.DefaultType, "CloudEvent type (Env: $FUNC_TYPE)")
|
||||
cmd.Flags().StringP("id", "i", uuid.NewString(), "CloudEvent ID (Env: $FUNC_ID)")
|
||||
cmd.Flags().StringP("data", "d", "", "Any arbitrary string to be sent as the CloudEvent data. Ignored if --file is provided (Env: $FUNC_DATA)")
|
||||
cmd.Flags().StringP("file", "f", "", "Path to a local file containing CloudEvent data to be sent (Env: $FUNC_FILE)")
|
||||
cmd.Flags().StringP("content-type", "c", "application/json", "The MIME Content-Type for the CloudEvent data (Env: $FUNC_CONTENT_TYPE)")
|
||||
setPathFlag(cmd)
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return runEmit(cmd, args, clientFn)
|
||||
}
|
||||
|
||||
return cmd
|
||||
|
||||
}
|
||||
|
||||
func runEmit(cmd *cobra.Command, _ []string, clientFn emitClientFn) (err error) {
|
||||
config := newEmitConfig()
|
||||
|
||||
// Validate things like invalid config combinations.
|
||||
if err := config.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine the final endpoint, taking into account the special value "local",
|
||||
// and sampling the function's current route if not explicitly provided
|
||||
endpoint, err := endpoint(cmd.Context(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Instantiate a client based on the final value of config
|
||||
transport := fnhttp.NewRoundTripper()
|
||||
defer transport.Close()
|
||||
config.Transport = transport
|
||||
client, err := clientFn(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Emit the event to the endpoint
|
||||
return client.Emit(cmd.Context(), endpoint)
|
||||
}
|
||||
|
||||
// endpoint returns the final effective endpoint.
|
||||
// By default, the contextually active Function is queried for it's current
|
||||
// address (route).
|
||||
// If "local" is specified in cfg.Sink, localhost is used.
|
||||
// Otherwise the value of Sink is used verbatim if defined.
|
||||
func endpoint(ctx context.Context, cfg emitConfig) (url string, err error) {
|
||||
var (
|
||||
f fn.Function
|
||||
d fn.Describer
|
||||
i fn.Info
|
||||
)
|
||||
|
||||
// If the special value "local" was requested,
|
||||
// use localhost.
|
||||
if cfg.Sink == "local" {
|
||||
return "http://localhost:8080", nil
|
||||
}
|
||||
|
||||
// If a sink was expressly provided, use that verbatim
|
||||
if cfg.Sink != "" {
|
||||
return cfg.Sink, nil
|
||||
}
|
||||
|
||||
// If no sink was specified, use the route to the currently
|
||||
// contectually active function
|
||||
if f, err = fn.NewFunction(cfg.Path); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Decide what happens if the function hasn't been deployed but they
|
||||
// don't run with --local=true. Perhaps an error in .Validate()?
|
||||
if d, err = knative.NewDescriber(""); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the current state of the function.
|
||||
if i, err = d.Describe(ctx, f.Name); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Probably wise to be defensive here:
|
||||
if len(i.Routes) == 0 {
|
||||
err = errors.New("function has no active routes")
|
||||
return
|
||||
}
|
||||
|
||||
// The first route should be the destination.
|
||||
return i.Routes[0], nil
|
||||
}
|
||||
|
||||
type emitConfig struct {
|
||||
Path string
|
||||
Source string
|
||||
Type string
|
||||
Id string
|
||||
Data string
|
||||
File string
|
||||
ContentType string
|
||||
Sink string
|
||||
Verbose bool
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
func newEmitConfig() emitConfig {
|
||||
return emitConfig{
|
||||
Path: viper.GetString("path"),
|
||||
Source: viper.GetString("source"),
|
||||
Type: viper.GetString("type"),
|
||||
Id: viper.GetString("id"),
|
||||
Data: viper.GetString("data"),
|
||||
File: viper.GetString("file"),
|
||||
ContentType: viper.GetString("content-type"),
|
||||
Sink: viper.GetString("sink"),
|
||||
Verbose: viper.GetBool("verbose"),
|
||||
}
|
||||
}
|
||||
|
||||
func (c emitConfig) Validate() error {
|
||||
if c.Data != "" && c.File != "" {
|
||||
return errors.New("Only one of --data or --file may be specified")
|
||||
}
|
||||
// TODO: should we verify that sink is a url or "local"?
|
||||
return nil
|
||||
}
|
|
@ -129,7 +129,7 @@ func newInfoConfig(args []string) infoConfig {
|
|||
// Output Formatting (serializers)
|
||||
// -------------------------------
|
||||
|
||||
type info fn.Info
|
||||
type info fn.Instance
|
||||
|
||||
func (i info) Human(w io.Writer) error {
|
||||
fmt.Fprintln(w, "Function name:")
|
||||
|
|
|
@ -0,0 +1,404 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/viper"
|
||||
"github.com/spf13/cobra"
|
||||
"knative.dev/kn-plugin-func/utils"
|
||||
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
fnhttp "knative.dev/kn-plugin-func/http"
|
||||
knative "knative.dev/kn-plugin-func/knative"
|
||||
)
|
||||
|
||||
func init() {
|
||||
root.AddCommand(NewInvokeCmd(newInvokeClient))
|
||||
}
|
||||
|
||||
type invokeClientFn func(invokeConfig) (*fn.Client, error)
|
||||
|
||||
func newInvokeClient(cfg invokeConfig) (*fn.Client, error) {
|
||||
describer, err := knative.NewDescriber(cfg.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
describer.Verbose = cfg.Verbose
|
||||
|
||||
return fn.New(
|
||||
fn.WithDescriber(describer),
|
||||
fn.WithTransport(fnhttp.NewRoundTripper()),
|
||||
fn.WithVerbose(cfg.Verbose),
|
||||
), nil
|
||||
}
|
||||
|
||||
func NewInvokeCmd(clientFn invokeClientFn) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "invoke",
|
||||
Short: "Invoke a Function",
|
||||
Long: `
|
||||
NAME
|
||||
{{.Prefix}}func invoke - Invoke a Function.
|
||||
|
||||
SYNOPSIS
|
||||
{{.Prefix}}func invoke [-t|--target] [-f|--format]
|
||||
[--id] [--source] [--type] [--data] [--file] [--content-type]
|
||||
[-s|--save] [-p|--path] [-c|--confirm] [-v|--verbose]
|
||||
|
||||
DESCRIPTION
|
||||
Invokes the Function by sending a test request to the currently running
|
||||
Function instance, either locally or remote. If the Function is running
|
||||
both locally and remote, the local instance will be invoked. This behavior
|
||||
can be manually overridden using the --target flag.
|
||||
|
||||
Functions are invoked with a test data structure consisting of five values:
|
||||
id: A unique identifier for the request.
|
||||
source: A sender name for the request (sender).
|
||||
type: A type for the request.
|
||||
data: Data (content) for this request.
|
||||
content-type: The MIME type of the value contained in 'data'.
|
||||
|
||||
The values of these parameters can be individually altered from their defaults
|
||||
using their associated flags. Data can also be provided from a file using the
|
||||
--file flag.
|
||||
|
||||
Invocation Target
|
||||
The Function instance to invoke can be specified using the --target flag
|
||||
which accepts the values "local", "remote", or <URL>. By default the
|
||||
local Function instance is chosen if running (see {{.Prefix}}func run).
|
||||
To explicitly target the remote (deployed) Function:
|
||||
{{.Prefix}}func invoke --target=remote
|
||||
To target an arbitrary endpoint, provide a URL:
|
||||
{{.Prefix}}func invoke --target=https://myfunction.example.com
|
||||
|
||||
Invocation Data
|
||||
Providing a filename in the --file flag will base64 encode its contents
|
||||
as the "data" parameter sent to the Function. The value of --content-type
|
||||
should be set to the type from the source file. For example, the following
|
||||
would send a JPEG base64 encoded in the "data" POST parameter:
|
||||
{{.Prefix}}func invoke --file=example.jpeg --content-type=image/jpeg
|
||||
|
||||
Message Format
|
||||
By default Functions are sent messages which match the invocation format
|
||||
of the template they were created using; for example "http" or "cloudevent".
|
||||
To override this behavior, use the --format (-f) flag.
|
||||
{{.Prefix}}func invoke -f=cloudevent -t=http://my-sink.my-cluster
|
||||
|
||||
EXAMPLES
|
||||
|
||||
o Invoke the default (local or remote) running Function with default values
|
||||
$ {{.Prefix}}func invoke
|
||||
|
||||
o Run the Function locally and then invoke it with a test request:
|
||||
(run in two terminals or by running the first in the background)
|
||||
$ {{.Prefix}}func run
|
||||
$ {{.Prefix}}func invoke
|
||||
|
||||
o Deploy and then invoke the remote Function:
|
||||
$ {{.Prefix}}func deploy
|
||||
$ {{.Prefix}}func invoke
|
||||
|
||||
o Invoke a remote (deployed) Function when it is already running locally:
|
||||
(overrides the default behavior of preferring locally running instances)
|
||||
$ {{.Prefix}}func invoke --target=remote
|
||||
|
||||
o Specify the data to send to the Function as a flag
|
||||
$ {{.Prefix}}func invoke --data="Hello World!"
|
||||
|
||||
o Send a JPEG to the Function
|
||||
$ {{.Prefix}}func invoke --file=example.jpeg --content-type=image/jpeg
|
||||
|
||||
o Invoke an arbitrary endpoint (HTTP POST)
|
||||
$ {{.Prefix}}func invoke --target="https://my-http-handler.example.com"
|
||||
|
||||
o Invoke an arbitrary endpoint (CloudEvent)
|
||||
$ {{.Prefix}}func invoke -f=cloudevent -t="https://my-event-broker.example.com"
|
||||
|
||||
`,
|
||||
SuggestFor: []string{"emit", "emti", "send", "emit", "exec", "nivoke", "onvoke", "unvoke", "knvoke", "imvoke", "ihvoke", "ibvoke"},
|
||||
PreRunE: bindEnv("path", "format", "target", "id", "source", "type", "data", "content-type", "file", "confirm", "namespace"),
|
||||
}
|
||||
|
||||
// Flags
|
||||
cmd.Flags().StringP("path", "p", cwd(), "Path to the Function which should have its instance invoked. (Env: $FUNC_PATH)")
|
||||
cmd.Flags().StringP("format", "f", "", "Format of message to send, 'http' or 'cloudevent'. Default is to choose automatically. (Env: $FUNC_FORMAT)")
|
||||
cmd.Flags().StringP("target", "t", "", "Function instance to invoke. Can be 'local', 'remote' or a URL. Defaults to auto-discovery if not provided. (Env: $FUNC_TARGET)")
|
||||
cmd.Flags().StringP("id", "", uuid.NewString(), "ID for the request data. (Env: $FUNC_ID)")
|
||||
cmd.Flags().StringP("source", "", fn.DefaultInvokeSource, "Source value for the request data. (Env: $FUNC_SOURCE)")
|
||||
cmd.Flags().StringP("type", "", fn.DefaultInvokeType, "Type value for the request data. (Env: $FUNC_TYPE)")
|
||||
cmd.Flags().StringP("content-type", "", fn.DefaultInvokeContentType, "Content Type of the data. (Env: $FUNC_CONTENT_TYPE)")
|
||||
cmd.Flags().StringP("data", "", fn.DefaultInvokeData, "Data to send in the request. (Env: $FUNC_DATA)")
|
||||
cmd.Flags().StringP("file", "", "", "Path to a file to use as data. Overrides --data flag and should be sent with a correct --content-type. (Env: $FUNC_FILE)")
|
||||
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all options interactively. (Env: $FUNC_CONFIRM)")
|
||||
setNamespaceFlag(cmd)
|
||||
|
||||
// Help Action
|
||||
cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||
runInvokeHelp(cmd, args, clientFn)
|
||||
})
|
||||
|
||||
// Run Action
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return runInvoke(cmd, args, clientFn)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Run
|
||||
func runInvoke(cmd *cobra.Command, args []string, clientFn invokeClientFn) (err error) {
|
||||
// Gather flag values for the invocation
|
||||
cfg, err := newInvokeConfig(clientFn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Client instance from env vars, flags, args and user prompts (if --confirm)
|
||||
client, err := clientFn(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Message to send the running Function built from parameters gathered
|
||||
// from the user (or defaults)
|
||||
m := fn.InvokeMessage{
|
||||
ID: cfg.ID,
|
||||
Source: cfg.Source,
|
||||
Type: cfg.Type,
|
||||
ContentType: cfg.ContentType,
|
||||
Data: cfg.Data,
|
||||
Format: cfg.Format,
|
||||
}
|
||||
|
||||
// If --file was specified, use its content for message data
|
||||
if cfg.File != "" {
|
||||
content, err := os.ReadFile(cfg.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Data = base64.StdEncoding.EncodeToString(content)
|
||||
}
|
||||
|
||||
// Invoke
|
||||
err = client.Invoke(cmd.Context(), cfg.Path, cfg.Target, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(cmd.OutOrStderr(), "Invoked %v\n", cfg.Target)
|
||||
return
|
||||
}
|
||||
|
||||
func runInvokeHelp(cmd *cobra.Command, args []string, clientFn invokeClientFn) {
|
||||
var (
|
||||
body = cmd.Long + "\n\n" + cmd.UsageString()
|
||||
t = template.New("invoke")
|
||||
tpl = template.Must(t.Parse(body))
|
||||
)
|
||||
|
||||
var data = struct {
|
||||
Prefix string
|
||||
}{
|
||||
Prefix: pluginPrefix(),
|
||||
}
|
||||
|
||||
if err := tpl.Execute(cmd.OutOrStdout(), data); err != nil {
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "unable to display help text: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type invokeConfig struct {
|
||||
Path string
|
||||
Target string
|
||||
Format string
|
||||
ID string
|
||||
Source string
|
||||
Type string
|
||||
Data string
|
||||
ContentType string
|
||||
File string
|
||||
Namespace string
|
||||
Confirm bool
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
func newInvokeConfig(clientFn invokeClientFn) (cfg invokeConfig, err error) {
|
||||
cfg = invokeConfig{
|
||||
Path: viper.GetString("path"),
|
||||
Target: viper.GetString("target"),
|
||||
ID: viper.GetString("id"),
|
||||
Source: viper.GetString("source"),
|
||||
Type: viper.GetString("type"),
|
||||
Data: viper.GetString("data"),
|
||||
ContentType: viper.GetString("content-type"),
|
||||
File: viper.GetString("file"),
|
||||
Confirm: viper.GetBool("confirm"),
|
||||
Verbose: viper.GetBool("verbose"),
|
||||
}
|
||||
|
||||
// If file was passed, read it in as data
|
||||
if cfg.File != "" {
|
||||
b, err := ioutil.ReadFile(cfg.File)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
cfg.Data = string(b)
|
||||
}
|
||||
|
||||
// if not in confirm/prompting mode, the cfg structure is complete.
|
||||
if !cfg.Confirm {
|
||||
return
|
||||
}
|
||||
|
||||
// Client instance for use during prompting.
|
||||
client, err := clientFn(cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If in interactive terminal mode, prompt to modify defaults.
|
||||
if interactiveTerminal() {
|
||||
return cfg.prompt(client)
|
||||
}
|
||||
|
||||
// Confirming, but noninteractive, is essentially a selective verbose mode
|
||||
// which prints out the effective values of config as a confirmation.
|
||||
fmt.Printf("Path: %v\n", cfg.Path)
|
||||
fmt.Printf("Target: %v\n", cfg.Target)
|
||||
fmt.Printf("ID: %v\n", cfg.ID)
|
||||
fmt.Printf("Source: %v\n", cfg.Source)
|
||||
fmt.Printf("Type: %v\n", cfg.Type)
|
||||
fmt.Printf("Data: %v\n", cfg.Data)
|
||||
fmt.Printf("Content Type: %v\n", cfg.ContentType)
|
||||
fmt.Printf("File: %v\n", cfg.File)
|
||||
return
|
||||
}
|
||||
|
||||
func (c invokeConfig) prompt(client *fn.Client) (invokeConfig, error) {
|
||||
var qs []*survey.Question
|
||||
|
||||
// First get path to effective Function
|
||||
qs = []*survey.Question{
|
||||
{
|
||||
Name: "Path",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Function Path:",
|
||||
Default: c.Path,
|
||||
},
|
||||
Validate: func(val interface{}) error {
|
||||
if val.(string) != "" {
|
||||
derivedName, _ := deriveNameAndAbsolutePathFromPath(val.(string))
|
||||
return utils.ValidateFunctionName(derivedName)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Transform: func(ans interface{}) interface{} {
|
||||
if ans.(string) != "" {
|
||||
_, absolutePath := deriveNameAndAbsolutePathFromPath(ans.(string))
|
||||
return absolutePath
|
||||
}
|
||||
return ""
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := survey.Ask(qs, &c); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
qs = []*survey.Question{
|
||||
{
|
||||
Name: "Target",
|
||||
Prompt: &survey.Input{
|
||||
Message: "(Optional) Target ('local', 'remote' or URL). If not provided, local will be preferred over remote.",
|
||||
Default: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Format",
|
||||
Prompt: &survey.Select{
|
||||
Message: "(Optional) Format Override",
|
||||
Options: []string{"", "http", "cloudevent"},
|
||||
Default: c.Format,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := survey.Ask(qs, &c); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
// Prompt for the next set of values, with defaults set first by the Function
|
||||
// as it exists on disk, followed by environment variables, and finally flags.
|
||||
// user interactive prompts therefore are the last applied, and thus highest
|
||||
// precidence values.
|
||||
qs = []*survey.Question{
|
||||
{
|
||||
Name: "ID",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Data ID",
|
||||
Default: c.ID,
|
||||
},
|
||||
}, {
|
||||
Name: "Source",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Data Source",
|
||||
Default: c.Source,
|
||||
},
|
||||
}, {
|
||||
Name: "Type",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Data Type",
|
||||
Default: c.Type,
|
||||
},
|
||||
}, {
|
||||
Name: "File",
|
||||
Prompt: &survey.Input{
|
||||
Message: "(Optional) Load Data Content from File",
|
||||
Default: c.File,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := survey.Ask(qs, &c); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
// If the user did not specify a file for data content, prompt for it
|
||||
if c.File == "" {
|
||||
qs = []*survey.Question{
|
||||
{
|
||||
Name: "Data",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Data Content",
|
||||
Default: c.Data,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := survey.Ask(qs, &c); err != nil {
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, allow mutation of the data content type.
|
||||
contentTypeMessage := "Content type of data"
|
||||
if c.File != "" {
|
||||
contentTypeMessage = "Content type of file"
|
||||
}
|
||||
qs = []*survey.Question{
|
||||
{
|
||||
Name: "ContentType",
|
||||
Prompt: &survey.Input{
|
||||
Message: contentTypeMessage,
|
||||
Default: c.ContentType,
|
||||
},
|
||||
}}
|
||||
if err := survey.Ask(qs, &c); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
|
@ -9,10 +9,10 @@ import (
|
|||
"knative.dev/kn-plugin-func/mock"
|
||||
)
|
||||
|
||||
// TestRepositoryList ensures that the 'list' subcommand shows the client's
|
||||
// TestRepository_List ensures that the 'list' subcommand shows the client's
|
||||
// set of repositories by name, respects the repositories flag (provides it to
|
||||
// the client), and prints the list as expected.
|
||||
func TestRepositoryList(t *testing.T) {
|
||||
func TestRepository_List(t *testing.T) {
|
||||
var (
|
||||
client = mock.NewClient()
|
||||
list = NewRepositoryListCmd(testRepositoryClientFn(client))
|
||||
|
@ -41,10 +41,10 @@ func TestRepositoryList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoryAdd ensures that the 'add' subcommand accepts its positional
|
||||
// TestRepository_Add ensures that the 'add' subcommand accepts its positional
|
||||
// arguments, respects the repositories path flag, and the expected name is echoed
|
||||
// upon subsequent 'list'.
|
||||
func TestRepositoryAdd(t *testing.T) {
|
||||
func TestRepository_Add(t *testing.T) {
|
||||
var (
|
||||
client = mock.NewClient()
|
||||
add = NewRepositoryAddCmd(testRepositoryClientFn(client))
|
||||
|
@ -81,10 +81,10 @@ func TestRepositoryAdd(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoryRename ensures that the 'rename' subcommand accepts its
|
||||
// TestRepository_Rename ensures that the 'rename' subcommand accepts its
|
||||
// positional arguments, respects the repositories path flag, and the name is
|
||||
// reflected as having been reanamed upon subsequent 'list'.
|
||||
func TestRepositoryRename(t *testing.T) {
|
||||
func TestRepository_Rename(t *testing.T) {
|
||||
var (
|
||||
client = mock.NewClient()
|
||||
add = NewRepositoryAddCmd(testRepositoryClientFn(client))
|
||||
|
@ -129,10 +129,10 @@ func TestRepositoryRename(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestReposotoryRemove ensures that the 'remove' subcommand accepts name as
|
||||
// TestReposotory_Remove ensures that the 'remove' subcommand accepts name as
|
||||
// its argument, respects the repositorieis flag, and the entry is removed upon
|
||||
// subsequent 'list'.
|
||||
func TestRepositoryRemove(t *testing.T) {
|
||||
func TestRepository_Remove(t *testing.T) {
|
||||
var (
|
||||
client = mock.NewClient()
|
||||
add = NewRepositoryAddCmd(testRepositoryClientFn(client))
|
||||
|
|
|
@ -34,12 +34,12 @@ curl $(kn service describe myfunc -o url)
|
|||
// resultant binary with no arguments prints the help/usage text.
|
||||
var root = &cobra.Command{
|
||||
Use: "func",
|
||||
Short: "Serverless functions",
|
||||
Short: "Serverless Functions",
|
||||
SilenceErrors: true, // we explicitly handle errors in Execute()
|
||||
SilenceUsage: true, // no usage dump on error
|
||||
Long: `Serverless functions
|
||||
Long: `Serverless Functions
|
||||
|
||||
Create, build and deploy functions in serverless containers for multiple runtimes on Knative`,
|
||||
Create, build and deploy Functions in serverless containers for multiple runtimes on Knative`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
fn "knative.dev/kn-plugin-func"
|
||||
)
|
||||
|
||||
func Test_mergeEnvMaps(t *testing.T) {
|
||||
func TestRoot_mergeEnvMaps(t *testing.T) {
|
||||
|
||||
a := "A"
|
||||
b := "B"
|
||||
|
@ -118,7 +118,7 @@ func Test_mergeEnvMaps(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_CMDParameterized(t *testing.T) {
|
||||
func TestRoot_CMDParameterized(t *testing.T) {
|
||||
|
||||
if root.Use != "func" {
|
||||
t.Fatalf("default command use should be \"func\".")
|
||||
|
|
24
cmd/run.go
24
cmd/run.go
|
@ -1,6 +1,8 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ory/viper"
|
||||
|
@ -98,14 +100,34 @@ func runRun(cmd *cobra.Command, args []string, clientFn runClientFn) (err error)
|
|||
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
|
||||
}
|
||||
|
||||
// Client for use running (and potentially building)
|
||||
client := clientFn(config)
|
||||
|
||||
// Build if not built and --build
|
||||
if config.Build && !function.Built() {
|
||||
if err = client.Build(cmd.Context(), config.Path); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return client.Run(cmd.Context(), config.Path)
|
||||
|
||||
// Run the Function at path
|
||||
job, err := client.Run(cmd.Context(), config.Path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer job.Stop()
|
||||
|
||||
fmt.Fprintf(cmd.OutOrStderr(), "Function started on port %v\n", job.Port)
|
||||
|
||||
select {
|
||||
case <-cmd.Context().Done():
|
||||
if !errors.Is(cmd.Context().Err(), context.Canceled) {
|
||||
err = cmd.Context().Err()
|
||||
}
|
||||
return
|
||||
case err = <-job.Errors:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type runConfig struct {
|
||||
|
|
178
cmd/run_test.go
178
cmd/run_test.go
|
@ -1,6 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
@ -10,104 +11,133 @@ import (
|
|||
"knative.dev/kn-plugin-func/mock"
|
||||
)
|
||||
|
||||
func TestRunRun(t *testing.T) {
|
||||
func TestRun_Run(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fileContents string
|
||||
buildErrors bool
|
||||
buildFlag bool
|
||||
shouldBuild bool
|
||||
shouldRun bool
|
||||
name string // name of the test
|
||||
desc string // description of the test
|
||||
funcState string // Function state, as described in func.yaml
|
||||
buildFlag bool // value to which the --build flag should be set
|
||||
buildError error // Set the builder to yield this error
|
||||
runError error // Set the runner to yield this error
|
||||
buildInvoked bool // should Builder.Build be invoked?
|
||||
runInvoked bool // should Runner.Run be invoked?
|
||||
}{
|
||||
{
|
||||
name: "Should attempt to run even if build is skipped",
|
||||
fileContents: `name: test-func
|
||||
name: "run when not building",
|
||||
desc: "Should run when build is not enabled",
|
||||
funcState: `name: test-func
|
||||
runtime: go
|
||||
created: 2009-11-10 23:00:00`,
|
||||
buildFlag: false,
|
||||
shouldBuild: false,
|
||||
shouldRun: true,
|
||||
buildFlag: false,
|
||||
buildInvoked: false,
|
||||
runInvoked: true,
|
||||
},
|
||||
{
|
||||
name: "Prebuilt image doesn't get built again",
|
||||
fileContents: `name: test-func
|
||||
name: "run and build",
|
||||
desc: "Should run and build when build is enabled and there is no image",
|
||||
funcState: `name: test-func
|
||||
runtime: go
|
||||
image: unexistant
|
||||
created: 2009-11-10 23:00:00`,
|
||||
buildFlag: true,
|
||||
shouldBuild: false,
|
||||
shouldRun: true,
|
||||
buildFlag: true,
|
||||
buildInvoked: true,
|
||||
runInvoked: true,
|
||||
},
|
||||
{
|
||||
name: "Build when image is empty and build flag is true",
|
||||
fileContents: `name: test-func
|
||||
name: "skip rebuild",
|
||||
desc: "Built image doesn't get built again",
|
||||
// TODO: this might be improved by checking if the user provided
|
||||
// the --build=true flag, allowing an override to force rebuild.
|
||||
// This could be accomplished by adding a 'provideBuildFlag' struct
|
||||
// member.
|
||||
funcState: `name: test-func
|
||||
runtime: go
|
||||
image: exampleimage
|
||||
created: 2009-11-10 23:00:00`,
|
||||
buildFlag: true,
|
||||
shouldBuild: true,
|
||||
shouldRun: true,
|
||||
buildFlag: true,
|
||||
buildInvoked: false,
|
||||
runInvoked: true,
|
||||
},
|
||||
{
|
||||
name: "Build error skips execution",
|
||||
fileContents: `name: test-func
|
||||
name: "Build errors return",
|
||||
desc: "Errors building cause an immediate return with error",
|
||||
funcState: `name: test-func
|
||||
runtime: go
|
||||
created: 2009-11-10 23:00:00`,
|
||||
buildFlag: true,
|
||||
shouldBuild: true,
|
||||
shouldRun: false,
|
||||
buildErrors: true,
|
||||
buildFlag: true,
|
||||
buildError: fmt.Errorf("generic build error"),
|
||||
buildInvoked: true,
|
||||
runInvoked: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
mockRunner := mock.NewRunner()
|
||||
mockBuilder := mock.NewBuilder()
|
||||
errorBuilder := mock.Builder{
|
||||
BuildFn: func(f fn.Function) error { return fmt.Errorf("build failed") },
|
||||
}
|
||||
cmd := NewRunCmd(func(rc runConfig) *fn.Client {
|
||||
buildOption := fn.WithBuilder(mockBuilder)
|
||||
if tt.buildErrors {
|
||||
buildOption = fn.WithBuilder(&errorBuilder)
|
||||
}
|
||||
return fn.New(
|
||||
fn.WithRunner(mockRunner),
|
||||
buildOption,
|
||||
fn.WithRegistry("ghcr.com/reg"),
|
||||
)
|
||||
})
|
||||
tempDir, err := os.MkdirTemp("", "func-tests")
|
||||
if err != nil {
|
||||
t.Fatalf("temp dir couldn't be created %v", err)
|
||||
}
|
||||
t.Log("tempDir created:", tempDir)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(tempDir)
|
||||
})
|
||||
|
||||
fullPath := tempDir + "/func.yaml"
|
||||
tempFile, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("temp file couldn't be created %v", err)
|
||||
}
|
||||
|
||||
cmd.SetArgs([]string{"--path=" + tempDir})
|
||||
viper.SetDefault("build", tt.buildFlag)
|
||||
|
||||
_, err = tempFile.WriteString(tt.fileContents)
|
||||
if err != nil {
|
||||
t.Fatalf("file content was not written %v", err)
|
||||
}
|
||||
// run as a sub-test
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := cmd.Execute()
|
||||
if err == nil && tt.buildErrors {
|
||||
t.Errorf("Expected error: %v but got %v", tt.buildErrors, err)
|
||||
defer fromTempDir(t)()
|
||||
|
||||
runner := mock.NewRunner()
|
||||
if tt.runError != nil {
|
||||
runner.RunFn = func(context.Context, fn.Function) (*fn.Job, error) { return nil, tt.runError }
|
||||
}
|
||||
if tt.shouldBuild && !(mockBuilder.BuildInvoked || errorBuilder.BuildInvoked) {
|
||||
t.Errorf("Function was expected to build is: %v but build execution was: %v", tt.shouldBuild, mockBuilder.BuildInvoked || errorBuilder.BuildInvoked)
|
||||
|
||||
builder := mock.NewBuilder()
|
||||
if tt.buildError != nil {
|
||||
builder.BuildFn = func(f fn.Function) error { return tt.buildError }
|
||||
}
|
||||
if mockRunner.RunInvoked != tt.shouldRun {
|
||||
t.Errorf("Function was expected to run is: %v but run execution was: %v", tt.shouldRun, mockRunner.RunInvoked)
|
||||
|
||||
// using a command whose client will be populated with mock
|
||||
// builder and mock runner, each of which may be set to error if the
|
||||
// test has an error defined.
|
||||
cmd := NewRunCmd(func(rc runConfig) *fn.Client {
|
||||
return fn.New(
|
||||
fn.WithRunner(runner),
|
||||
fn.WithBuilder(builder),
|
||||
fn.WithRegistry("ghcr.com/reg"),
|
||||
)
|
||||
})
|
||||
|
||||
// set test case's build
|
||||
viper.SetDefault("build", tt.buildFlag)
|
||||
|
||||
// set test case's func.yaml
|
||||
if err := os.WriteFile("func.yaml", []byte(tt.funcState), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
runErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
t0 := tt // capture tt into closure
|
||||
_, err := cmd.ExecuteContextC(ctx)
|
||||
if err != nil && t0.buildError != nil {
|
||||
// This is an expected error, so simply continue execution ignoring
|
||||
// the error (send nil on the channel to release the parent routine
|
||||
runErrCh <- nil
|
||||
return
|
||||
} else if err != nil {
|
||||
runErrCh <- err // error not expected
|
||||
return
|
||||
}
|
||||
|
||||
// No errors, but an error was expected:
|
||||
if t0.buildError != nil {
|
||||
runErrCh <- fmt.Errorf("Expected error: %v but got %v\n", t0.buildError, err)
|
||||
}
|
||||
|
||||
// Ensure invocations match expectations
|
||||
if builder.BuildInvoked != tt.buildInvoked {
|
||||
runErrCh <- fmt.Errorf("Function was expected to build is: %v but build execution was: %v", tt.buildInvoked, builder.BuildInvoked)
|
||||
}
|
||||
if runner.RunInvoked != tt.runInvoked {
|
||||
runErrCh <- fmt.Errorf("Function was expected to run is: %v but run execution was: %v", tt.runInvoked, runner.RunInvoked)
|
||||
}
|
||||
|
||||
close(runErrCh) // release the waiting parent process
|
||||
}()
|
||||
cancel() // trigger the return of cmd.ExecuteContextC in the routine
|
||||
<-ctx.Done()
|
||||
if err := <-runErrCh; err != nil { // wait for completion of assertions
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
// TestConfigPathDefault ensures that config defaults to XDG_CONFIG_HOME/func
|
||||
func TestConfigPathDefault(t *testing.T) {
|
||||
// TestConfig_PathDefault ensures that config defaults to XDG_CONFIG_HOME/func
|
||||
func TestConfig_PathDefault(t *testing.T) {
|
||||
// TODO
|
||||
// Set XDG_CONFIG_PATH to ./testdata/config
|
||||
// Confirm the config is populated from the test files.
|
||||
|
@ -20,7 +20,7 @@ func TestConfigPathDefault(t *testing.T) {
|
|||
|
||||
// TestConfigPath ensure that the config path provided via the WithConfig
|
||||
// option is respected.
|
||||
func TestConfigPath(t *testing.T) {
|
||||
func TestConfig_Path(t *testing.T) {
|
||||
// TODO
|
||||
// Create a client specifying ./testdata/config
|
||||
// Confirm the config is populated from the test files.
|
||||
|
@ -28,7 +28,7 @@ func TestConfigPath(t *testing.T) {
|
|||
|
||||
// TestConfigRepositoriesPath ensures that the repositories directory within
|
||||
// the effective config path is created if it does not already exist.
|
||||
func TestConfigRepositoriesPath(t *testing.T) {
|
||||
func TestConfig_RepositoriesPath(t *testing.T) {
|
||||
// TODO
|
||||
// Create a temporary directory
|
||||
// Specify this directory as the config path when instantiating a client.
|
||||
|
|
|
@ -155,6 +155,8 @@ func TestCheckAuth(t *testing.T) {
|
|||
}
|
||||
|
||||
func startServer(t *testing.T) (addr, addrTLS string, stopServer func()) {
|
||||
// TODO: this should be refactored to use OS-chosen ports so as not to
|
||||
// fail when a user is running a Function on the default port.)
|
||||
listener, err := net.Listen("tcp", "localhost:8080")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
316
docker/runner.go
316
docker/runner.go
|
@ -3,15 +3,15 @@ package docker
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -19,9 +19,24 @@ import (
|
|||
fn "knative.dev/kn-plugin-func"
|
||||
)
|
||||
|
||||
// Runner of functions using the docker command.
|
||||
const (
|
||||
// DefaultHost is the standard ipv4 looback
|
||||
DefaultHost = "127.0.0.1"
|
||||
|
||||
// DefaultPort is used as the preferred port, and in the unlikly event of an
|
||||
// error querying the OS for a free port during allocation.
|
||||
DefaultPort = "8080"
|
||||
|
||||
// DefaultDialTimeout when checking if a port is available.
|
||||
DefaultDialTimeout = 2 * time.Second
|
||||
|
||||
// DefaultStopTimeout when attempting to stop underlying containers.
|
||||
DefaultStopTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// Runner starts and stops Functions as local contaieners.
|
||||
type Runner struct {
|
||||
// Verbose logging flag.
|
||||
// Verbose logging
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
|
@ -30,122 +45,201 @@ func NewRunner() *Runner {
|
|||
return &Runner{}
|
||||
}
|
||||
|
||||
// Run the function at path
|
||||
func (n *Runner) Run(ctx context.Context, f fn.Function) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
// Run the Function.
|
||||
func (n *Runner) Run(ctx context.Context, f fn.Function) (job *fn.Job, err error) {
|
||||
|
||||
cli, _, err := NewClient(client.DefaultDockerHost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create docker api client")
|
||||
}
|
||||
defer cli.Close()
|
||||
var (
|
||||
port = choosePort(DefaultHost, DefaultPort, DefaultDialTimeout)
|
||||
c client.CommonAPIClient // Docker client
|
||||
id string // ID of running container
|
||||
conn net.Conn // Connection to container's stdio
|
||||
|
||||
// Channels for gathering runtime errors from the container instance
|
||||
copyErrCh = make(chan error, 10)
|
||||
contBodyCh <-chan container.ContainerWaitOKBody
|
||||
contErrCh <-chan error
|
||||
|
||||
// Combined runtime error channel for sending all errors to caller
|
||||
runtimeErrCh = make(chan error, 10)
|
||||
)
|
||||
|
||||
if f.Image == "" {
|
||||
return errors.New("Function has no associated Image. Has it been built? Using the --build flag will build the image if it hasn't been built yet")
|
||||
return job, errors.New("Function has no associated image. Has it been built?")
|
||||
}
|
||||
if c, _, err = NewClient(client.DefaultDockerHost); err != nil {
|
||||
return job, errors.Wrap(err, "failed to create Docker API client")
|
||||
}
|
||||
if id, err = newContainer(ctx, c, f, port, n.Verbose); err != nil {
|
||||
return job, errors.Wrap(err, "runner unable to create container")
|
||||
}
|
||||
if conn, err = copyStdio(ctx, c, id, copyErrCh); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for errors or premature exits
|
||||
contBodyCh, contErrCh = c.ContainerWait(ctx, id, container.WaitConditionNextExit)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case err = <-copyErrCh:
|
||||
runtimeErrCh <- err
|
||||
case body := <-contBodyCh:
|
||||
// NOTE: currently an exit is not expected and thus a return, for any
|
||||
// reason, is considered an error even when the exit code is 0.
|
||||
// Functions are expected to be long-running processes that do not exit
|
||||
// of their own accord when run locally. Should this expectation
|
||||
// change in the future, this channel-based wait may need to be
|
||||
// expanded to accept the case of a voluntary, successful exit.
|
||||
runtimeErrCh <- fmt.Errorf("exited code %v", body.StatusCode)
|
||||
case err = <-contErrCh:
|
||||
runtimeErrCh <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start
|
||||
if err = c.ContainerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
|
||||
return job, errors.Wrap(err, "runner unable to start container")
|
||||
}
|
||||
|
||||
// Stopper
|
||||
stop := func() {
|
||||
var (
|
||||
timeout = DefaultStopTimeout
|
||||
ctx = context.Background()
|
||||
)
|
||||
if err = c.ContainerStop(ctx, id, &timeout); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error stopping container %v: %v\n", id, err)
|
||||
}
|
||||
if err = c.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error removing container %v: %v\n", id, err)
|
||||
}
|
||||
if err = conn.Close(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error closing connection to container: %v\n", err)
|
||||
}
|
||||
if err = c.Close(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error closing daemon client: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Job reporting port, runtime errors and provides a mechanism for stopping.
|
||||
return fn.NewJob(f, port, runtimeErrCh, stop)
|
||||
}
|
||||
|
||||
// Dial the given (tcp) port on the given interface, returning an error if it is
|
||||
// unreachable.
|
||||
func dial(host, port string, dialTimeout time.Duration) (err error) {
|
||||
address := net.JoinHostPort(host, port)
|
||||
conn, err := net.DialTimeout("tcp", address, dialTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Note that TCP is presumed.
|
||||
func choosePort(host string, preferredPort string, dialTimeout time.Duration) string {
|
||||
// If we can not dial the preferredPort, it is assumed to be open.
|
||||
if err := dial(host, preferredPort, dialTimeout); err != nil {
|
||||
return preferredPort
|
||||
}
|
||||
|
||||
// Use an OS-chosen port
|
||||
lis, err := net.Listen("tcp", net.JoinHostPort(host, "")) // listen on any open port
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to check for open ports. using fallback %v. %v", DefaultPort, err)
|
||||
return DefaultPort
|
||||
}
|
||||
defer lis.Close()
|
||||
|
||||
_, port, err := net.SplitHostPort(lis.Addr().String())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to extract port from allocated listener address '%v'. %v", lis.Addr(), err)
|
||||
return DefaultPort
|
||||
}
|
||||
return port
|
||||
|
||||
}
|
||||
|
||||
func newContainer(ctx context.Context, c client.CommonAPIClient, f fn.Function, port string, verbose bool) (id string, err error) {
|
||||
var (
|
||||
containerCfg container.Config
|
||||
hostCfg container.HostConfig
|
||||
)
|
||||
if containerCfg, err = newContainerConfig(f, port, verbose); err != nil {
|
||||
return
|
||||
}
|
||||
if hostCfg, err = newHostConfig(port); err != nil {
|
||||
return
|
||||
}
|
||||
t, err := c.ContainerCreate(ctx, &containerCfg, &hostCfg, nil, nil, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return t.ID, nil
|
||||
}
|
||||
|
||||
func newContainerConfig(f fn.Function, _ string, verbose bool) (c container.Config, err error) {
|
||||
envs, err := newEnvironmentVariables(f, verbose)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// httpPort := nat.Port(fmt.Sprintf("%v/tcp", port))
|
||||
httpPort := nat.Port("8080/tcp")
|
||||
return container.Config{
|
||||
Image: f.Image,
|
||||
Env: envs,
|
||||
Tty: false,
|
||||
AttachStderr: true,
|
||||
AttachStdout: true,
|
||||
AttachStdin: false,
|
||||
ExposedPorts: map[nat.Port]struct{}{httpPort: {}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newHostConfig(port string) (c container.HostConfig, err error) {
|
||||
// httpPort := nat.Port(fmt.Sprintf("%v/tcp", port))
|
||||
httpPort := nat.Port("8080/tcp")
|
||||
ports := map[nat.Port][]nat.PortBinding{
|
||||
httpPort: {
|
||||
nat.PortBinding{
|
||||
HostPort: port,
|
||||
HostIP: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
return container.HostConfig{PortBindings: ports}, nil
|
||||
}
|
||||
|
||||
func newEnvironmentVariables(f fn.Function, verbose bool) ([]string, error) {
|
||||
// TODO: this has code-smell. It may not be ideal to have fn.Function
|
||||
// represent Envs as pointers, as this causes the clearly odd situation of
|
||||
// needing to check if an env defined in f is just nil pointers: an invalid
|
||||
// data structure.
|
||||
envs := []string{}
|
||||
for _, env := range f.Envs {
|
||||
if env.Name != nil && env.Value != nil {
|
||||
value, set, err := processEnvValue(*env.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
return envs, err
|
||||
}
|
||||
if set {
|
||||
envs = append(envs, *env.Name+"="+value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.Verbose {
|
||||
if verbose {
|
||||
envs = append(envs, "VERBOSE=true")
|
||||
}
|
||||
|
||||
httpPort := nat.Port("8080/tcp")
|
||||
ports := map[nat.Port][]nat.PortBinding{
|
||||
httpPort: {
|
||||
nat.PortBinding{
|
||||
HostPort: "8080",
|
||||
HostIP: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
conf := &container.Config{
|
||||
Env: envs,
|
||||
Tty: false,
|
||||
AttachStderr: true,
|
||||
AttachStdout: true,
|
||||
AttachStdin: false,
|
||||
Image: f.Image,
|
||||
ExposedPorts: map[nat.Port]struct{}{httpPort: {}},
|
||||
}
|
||||
|
||||
hostConf := &container.HostConfig{
|
||||
PortBindings: ports,
|
||||
}
|
||||
|
||||
cont, err := cli.ContainerCreate(ctx, conf, hostConf, nil, nil, "")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create container")
|
||||
}
|
||||
defer func() {
|
||||
err := cli.ContainerRemove(context.Background(), cont.ID, types.ContainerRemoveOptions{})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to remove container: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
attachOptions := types.ContainerAttachOptions{
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Stdin: false,
|
||||
Stream: true,
|
||||
}
|
||||
|
||||
resp, err := cli.ContainerAttach(ctx, cont.ID, attachOptions)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to attach container")
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
copyErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader)
|
||||
copyErrChan <- err
|
||||
}()
|
||||
|
||||
waitBodyChan, waitErrChan := cli.ContainerWait(ctx, cont.ID, container.WaitConditionNextExit)
|
||||
|
||||
err = cli.ContainerStart(ctx, cont.ID, types.ContainerStartOptions{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start container")
|
||||
}
|
||||
defer func() {
|
||||
t := time.Second * 10
|
||||
err := cli.ContainerStop(context.Background(), cont.ID, &t)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to stop container: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case body := <-waitBodyChan:
|
||||
if body.StatusCode != 0 {
|
||||
return fmt.Errorf("failed with status code: %d", body.StatusCode)
|
||||
}
|
||||
case err := <-waitErrChan:
|
||||
return err
|
||||
case err := <-copyErrChan:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
// run command supports only ENV values in from FOO=bar or FOO={{ env:LOCAL_VALUE }}
|
||||
// run command supports only ENV values in form:
|
||||
// FOO=bar or FOO={{ env:LOCAL_VALUE }}
|
||||
var evRegex = regexp.MustCompile(`^{{\s*(\w+)\s*:(\w+)\s*}}$`)
|
||||
|
||||
const (
|
||||
|
@ -172,3 +266,25 @@ func processEnvValue(val string) (string, bool, error) {
|
|||
}
|
||||
return val, true, nil
|
||||
}
|
||||
|
||||
// copy stdin and stdout from the container of the given ID. Errors encountered
|
||||
// during copy are communicated via a provided errs channel.
|
||||
func copyStdio(ctx context.Context, c client.CommonAPIClient, id string, errs chan error) (conn net.Conn, err error) {
|
||||
var (
|
||||
res types.HijackedResponse
|
||||
opt = types.ContainerAttachOptions{
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Stdin: false,
|
||||
Stream: true,
|
||||
}
|
||||
)
|
||||
if res, err = c.ContainerAttach(ctx, id, opt); err != nil {
|
||||
return conn, errors.Wrap(err, "runner unable to attach to container's stdio")
|
||||
}
|
||||
go func() {
|
||||
_, err := stdcopy.StdCopy(os.Stdout, os.Stderr, res.Reader)
|
||||
errs <- err
|
||||
}()
|
||||
return res.Conn, nil
|
||||
}
|
||||
|
|
|
@ -32,12 +32,12 @@ func TestDockerRun(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// TODO: This test is too tricky, as it requires the related image be
|
||||
// already built. Build the function prior to running?
|
||||
|
||||
// NOTE: test requires that the image be built already.
|
||||
|
||||
runner := docker.NewRunner()
|
||||
runner.Verbose = true
|
||||
if err = runner.Run(context.Background(), f); err != nil {
|
||||
if _, err = runner.Run(context.Background(), f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
/* TODO
|
||||
|
@ -50,9 +50,11 @@ func TestDockerRun(t *testing.T) {
|
|||
func TestDockerRunImagelessError(t *testing.T) {
|
||||
runner := docker.NewRunner()
|
||||
f := fn.NewFunctionWith(fn.Function{})
|
||||
err := runner.Run(context.Background(), f)
|
||||
expectedErrorMessage := "Function has no associated Image. Has it been built? Using the --build flag will build the image if it hasn't been built yet"
|
||||
|
||||
_, err := runner.Run(context.Background(), f)
|
||||
// TODO: switch to typed error:
|
||||
expectedErrorMessage := "Function has no associated image. Has it been built?"
|
||||
if err == nil || err.Error() != expectedErrorMessage {
|
||||
t.Fatalf("The expected error message is \"%v\" but got instead %v", expectedErrorMessage, err)
|
||||
t.Fatalf("Expected error '%v', got '%v'", expectedErrorMessage, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,40 +127,43 @@ When run as a `kn` plugin.
|
|||
kn func delete <name> [-n namespace, -p path]
|
||||
```
|
||||
|
||||
## `emit`
|
||||
## `invoke`
|
||||
|
||||
Emits a CloudEvent, sending it to the deployed function. The user may specify the event type, source and ID,
|
||||
and may provide event data on the command line or in a file on disk. By default, `event` works on the local
|
||||
directory, assuming that it is a function project. Alternatively the user may provide a path to a project
|
||||
directory using the `--path` flag, or send an event to an arbitrary endpoint using the `--sink` flag. The
|
||||
`--sink` flag also accepts the special value `local` to send an event to the function running locally, for
|
||||
example, when run via `func run`.
|
||||
Invokes a running function. By default, a locally running instance will be preferred
|
||||
over a remote if both are running. The user may specify the event type, source,
|
||||
ID, and may provide event data on the command line or in a file on disk.
|
||||
`invoke` works on the local directory, assuming that it is a function project.
|
||||
Alternatively the user may provide a path to a project directory using the
|
||||
`--path` flag, or send an event to an arbitrary endpoint using the `--target`
|
||||
flag. The `--target` flag also accepts the special values `local` and `remote`
|
||||
to send an event to a locally running function instance or a function running
|
||||
on the remote cluster in the active deployed context.
|
||||
|
||||
Similar `kn` command when using the [kn-plugin-event](https://github.com/knative-sandbox/kn-plugin-event): `kn event send [FLAGS]`
|
||||
|
||||
Examples:
|
||||
|
||||
```console
|
||||
# Send a CloudEvent to the deployed function with no data and default values
|
||||
# Send a request to the local function with no data and default values
|
||||
# for source, type and ID
|
||||
kn func emit
|
||||
kn func invoke
|
||||
|
||||
# Send a CloudEvent to the deployed function with the data found in ./test.json
|
||||
kn func emit --file ./test.json
|
||||
# Send a message to the local function with the data found in ./test.json
|
||||
kn func invoke --file ./test.json
|
||||
|
||||
# Send a CloudEvent to the function running locally with a CloudEvent containing
|
||||
# Send a message to the deployed function containing
|
||||
# "Hello World!" as the data field, with a content type of "text/plain"
|
||||
kn func emit --data "Hello World!" --content-type "text/plain" -s local
|
||||
kn func invoke --data "Hello World!" --content-type "text/plain" --target remote
|
||||
|
||||
# Send a CloudEvent to the function running locally with an event type of "my.event"
|
||||
kn func emit --type my.event --sink local
|
||||
# Send a message to the deployed function with an event type of "my.event"
|
||||
kn func invoke --type my.event --target remote
|
||||
|
||||
# Send a CloudEvent to the deployed function found at /path/to/fn with an id of "fn.test"
|
||||
kn func emit --path /path/to/fn -i fn.test
|
||||
# Send a message to the local function found at /path/to/fn with an id of "fn.test"
|
||||
kn func invoke --path /path/to/fn --id fn.test
|
||||
|
||||
# Send a CloudEvent to an arbitrary endpoint
|
||||
kn func emit --sink "http://my.event.broker.com"
|
||||
```
|
||||
kn func invoke --target "http://my.event.broker.com" --format=cloudevent
|
||||
|
||||
|
||||
## `config`
|
||||
|
||||
|
|
17
function.go
17
function.go
|
@ -100,6 +100,22 @@ type Function struct {
|
|||
// according to the client which is in charge of what constitutes being
|
||||
// fully "Created" (aka initialized)
|
||||
Created time.Time
|
||||
|
||||
// Invocation defines hints for use when invoking this function.
|
||||
// See Client.Invoke for usage.
|
||||
Invocation Invocation `yaml:"invocation,omitempty"`
|
||||
}
|
||||
|
||||
// Invocation defines hints on how to accomplish a Function invocation.
|
||||
type Invocation struct {
|
||||
// Format indicates the expected format of the invocation. Either 'http'
|
||||
// (a basic HTTP POST of standard form fields) or 'cloudevent'
|
||||
// (a CloudEvents v2 formatted http request).
|
||||
Format string `yaml:"format,omitempty"`
|
||||
|
||||
// Protocol Note:
|
||||
// Protocol is currently always HTTP. Method etc. determined by the single,
|
||||
// simple switch of the Format field.
|
||||
}
|
||||
|
||||
// NewFunctionWith defaults as provided.
|
||||
|
@ -354,6 +370,7 @@ func assertEmptyRoot(path string) (err error) {
|
|||
// Function rooted in the given directory.
|
||||
var contentiousFiles = []string{
|
||||
FunctionFile,
|
||||
".gitignore",
|
||||
}
|
||||
|
||||
// contentiousFilesIn the given directory
|
||||
|
|
|
@ -12,9 +12,9 @@ import (
|
|||
. "knative.dev/kn-plugin-func/testing"
|
||||
)
|
||||
|
||||
// TestWriteIdempotency ensures that a Function can be written repeatedly
|
||||
// TestFunction_WriteIdempotency ensures that a Function can be written repeatedly
|
||||
// without change.
|
||||
func TestWriteIdempotency(t *testing.T) {
|
||||
func TestFunction_WriteIdempotency(t *testing.T) {
|
||||
root, rm := Mktemp(t)
|
||||
defer rm()
|
||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||
|
@ -47,11 +47,11 @@ func TestWriteIdempotency(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestFunctionNameDefault ensures that a Function's name is defaulted to that
|
||||
// TestFunction_NameDefault ensures that a Function's name is defaulted to that
|
||||
// which can be derived from the last part of its path.
|
||||
// Creating a new Function from a path will error if there is no Function at
|
||||
// that path. Creating using the client initializes the default.
|
||||
func TestFunctionNameDefault(t *testing.T) {
|
||||
func TestFunction_NameDefault(t *testing.T) {
|
||||
// A path at which there is no Function currently
|
||||
root := "testdata/testFunctionNameDefault"
|
||||
defer Using(t, root)()
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package function
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvironmentLocal = "local"
|
||||
EnvironmentRemote = "remote"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotInitialized = errors.New("function is not initialized")
|
||||
ErrNotRunning = errors.New("function not running")
|
||||
ErrRootRequired = errors.New("function root path is required")
|
||||
ErrEnvironmentNotFound = errors.New("environment not found")
|
||||
)
|
||||
|
||||
// Instances manager
|
||||
//
|
||||
// Instances are point-in-time snapshots of a Function's runtime state in
|
||||
// a given environment. By default 'local' and 'remote' environmnts are
|
||||
// available when a Function is run locally and deployed (respectively).
|
||||
type Instances struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// newInstances creates a new manager of instances.
|
||||
func newInstances(client *Client) *Instances {
|
||||
return &Instances{client: client}
|
||||
}
|
||||
|
||||
// Get the instance data for a Function in the named environment.
|
||||
// For convenient access to the default 'local' and 'remote' environment
|
||||
// see the Local and Remote methods, respectively.
|
||||
// Instance returned is populated with a point-in-time snapshot of the
|
||||
// Function state in the named environment.
|
||||
func (s *Instances) Get(ctx context.Context, f Function, environment string) (Instance, error) {
|
||||
switch environment {
|
||||
case EnvironmentLocal:
|
||||
return s.Local(ctx, f)
|
||||
case EnvironmentRemote:
|
||||
return s.Remote(ctx, f.Name, f.Root)
|
||||
default:
|
||||
// Future versions will support additional ad-hoc named environments, such
|
||||
// as for testing. Local and remote remaining the base cases.
|
||||
return Instance{}, ErrEnvironmentNotFound
|
||||
}
|
||||
}
|
||||
|
||||
// Local instance details for the Function
|
||||
// If the Function is not running locally the error returned is ErrNotRunning
|
||||
func (s *Instances) Local(ctx context.Context, f Function) (Instance, error) {
|
||||
var i Instance
|
||||
// To create a local instance the Function must have a root path defined
|
||||
// which contains an initialized function and be running.
|
||||
if f.Root == "" {
|
||||
return i, ErrRootRequired
|
||||
}
|
||||
if !f.Initialized() {
|
||||
return i, ErrNotInitialized
|
||||
}
|
||||
ports := jobPorts(f)
|
||||
if len(ports) == 0 {
|
||||
return i, ErrNotRunning
|
||||
}
|
||||
|
||||
route := fmt.Sprintf("http://localhost:%s/", ports[0])
|
||||
|
||||
return Instance{
|
||||
Route: route,
|
||||
Routes: []string{route},
|
||||
Name: f.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Remote instance details for the Function
|
||||
//
|
||||
// Since this is specific to the implicitly available 'remote' environment, the
|
||||
// request can be completed with either a name or the local source. Therefore
|
||||
// either name or root path can be passed. If name is not passed, the Function
|
||||
// at root is loaded and its name used for describing the remote instance.
|
||||
// Name takes precedence.
|
||||
func (s *Instances) Remote(ctx context.Context, name, root string) (Instance, error) {
|
||||
var (
|
||||
f Function
|
||||
err error
|
||||
)
|
||||
|
||||
// Error if name and root disagree
|
||||
// If both a name and root were passed but the Function at the root either
|
||||
// does not exist or does not match the name, fail fast.
|
||||
// The purpose of this method's signature is to allow passing either name or
|
||||
// root, but doing so requires that we manually validate.
|
||||
if name != "" && root != "" {
|
||||
f, err = NewFunction(root)
|
||||
if err != nil {
|
||||
return Instance{}, err
|
||||
}
|
||||
if name != f.Name {
|
||||
return Instance{}, errors.New(
|
||||
"name passed does not match name of the Function at root. " +
|
||||
"Try passing either name or root rather than both.")
|
||||
}
|
||||
}
|
||||
|
||||
// Name takes precedence if provided
|
||||
if name != "" {
|
||||
f = Function{Name: name}
|
||||
} else {
|
||||
if f, err = NewFunction(root); err != nil {
|
||||
return Instance{}, err
|
||||
}
|
||||
}
|
||||
return s.client.describer.Describe(ctx, f.Name)
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package function
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
cloudevents "github.com/cloudevents/sdk-go/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultInvokeSource = "/boson/fn"
|
||||
DefaultInvokeType = "boson.fn"
|
||||
DefaultInvokeContentType = "text/plain"
|
||||
DefaultInvokeData = "Hello World"
|
||||
DefaultInvokeFormat = "http"
|
||||
)
|
||||
|
||||
// InvokeMesage is the message used by the convenience method Invoke to provide
|
||||
// a simple way to trigger the execution of a Function during development.
|
||||
type InvokeMessage struct {
|
||||
ID string
|
||||
Source string
|
||||
Type string
|
||||
ContentType string
|
||||
Data string
|
||||
Format string //optional override for Function-defined message format
|
||||
}
|
||||
|
||||
// NewInvokeMessage creates a new InvokeMessage with fields populated
|
||||
func NewInvokeMessage() InvokeMessage {
|
||||
return InvokeMessage{
|
||||
ID: uuid.NewString(),
|
||||
Source: DefaultInvokeSource,
|
||||
Type: DefaultInvokeType,
|
||||
ContentType: DefaultInvokeContentType,
|
||||
Data: DefaultInvokeData,
|
||||
// Format override not set by default: value from Function being preferred.
|
||||
}
|
||||
}
|
||||
|
||||
// invoke the Function instance in the target environment with the
|
||||
// invocation message.
|
||||
func invoke(ctx context.Context, c *Client, f Function, target string, m InvokeMessage) error {
|
||||
|
||||
// Get the first available route from 'local', 'remote', a named environment
|
||||
// or treat target
|
||||
route, err := invocationRoute(ctx, c, f, target) // choose instance to invoke
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Format" either 'http' or 'cloudevent'
|
||||
// TODO: discuss if providing a Format on Message should a) update the
|
||||
// Function to use the new format if none is defined already (backwards
|
||||
// compatibility fix) or b) always update the Function, even if it was already
|
||||
// set. Once decided, codify in a test.
|
||||
format := DefaultInvokeFormat
|
||||
if f.Invocation.Format != "" {
|
||||
// Prefer the format set during Function creation if defined.
|
||||
format = f.Invocation.Format
|
||||
}
|
||||
if m.Format != "" {
|
||||
// Use the override specified on the message if provided
|
||||
format = m.Format
|
||||
}
|
||||
|
||||
switch format {
|
||||
case "http":
|
||||
return sendPost(ctx, route, m, c.transport)
|
||||
case "cloudevent":
|
||||
return sendEvent(ctx, route, m, c.transport)
|
||||
default:
|
||||
return fmt.Errorf("format '%v' not supported.", format)
|
||||
}
|
||||
}
|
||||
|
||||
// invocationRoute returns a route to the named target instance of a Func:
|
||||
// 'local': local environment; locally running Function (error if not running)
|
||||
// 'remote': remote environment; first available instance (error if none)
|
||||
// '<environment>': A valid alternate target which contains instances.
|
||||
// '<url>': An explicit URL
|
||||
// '': Default if no target is passed is to first use local, then remote.
|
||||
// errors if neither are available.
|
||||
func invocationRoute(ctx context.Context, c *Client, f Function, target string) (string, error) {
|
||||
// TODO: this function has code-smell; will de-smellify it in next pass.
|
||||
if target == EnvironmentLocal {
|
||||
instance, err := c.Instances().Get(ctx, f, target)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrEnvironmentNotFound) {
|
||||
return "", errors.New("not running locally")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return instance.Route, nil
|
||||
|
||||
} else if target == EnvironmentRemote {
|
||||
instance, err := c.Instances().Get(ctx, f, target)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrEnvironmentNotFound) {
|
||||
return "", errors.New("not running in remote")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return instance.Route, nil
|
||||
|
||||
} else if target == "" { // target blank, check local first then remote.
|
||||
instance, err := c.Instances().Get(ctx, f, EnvironmentLocal)
|
||||
if err != nil && !errors.Is(err, ErrNotRunning) {
|
||||
return "", err // unexpected errors are anything other than ErrNotRunning
|
||||
}
|
||||
if err == nil {
|
||||
return instance.Route, nil // found instance in local environment
|
||||
}
|
||||
instance, err = c.Instances().Get(ctx, f, EnvironmentRemote)
|
||||
if errors.Is(err, ErrNotRunning) {
|
||||
return "", errors.New("not running locally or in the remote")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err // unexpected error
|
||||
}
|
||||
return instance.Route, nil
|
||||
} else { // treat an unrecognized target as an ad-hoc verbatim endpoint
|
||||
return target, nil
|
||||
}
|
||||
}
|
||||
|
||||
// sendEvent to the route populated with data in the invoke message.
|
||||
func sendEvent(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper) (err error) {
|
||||
event := cloudevents.NewEvent()
|
||||
event.SetID(m.ID)
|
||||
event.SetSource(m.Source)
|
||||
event.SetType(m.Type)
|
||||
if err = event.SetData(m.ContentType, m.Data); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c, err := cloudevents.NewClientHTTP(
|
||||
cloudevents.WithTarget(route),
|
||||
cloudevents.WithRoundTripper(t))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result := c.Send(cloudevents.ContextWithTarget(ctx, route), event)
|
||||
if cloudevents.IsUndelivered(result) {
|
||||
err = fmt.Errorf("unable to invoke: %v", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// sendPost to the route populated with data in the invoke message.
|
||||
func sendPost(ctx context.Context, route string, m InvokeMessage, t http.RoundTripper) error {
|
||||
client := http.Client{
|
||||
Transport: t,
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
resp, err := client.PostForm(route, url.Values{
|
||||
"ID": {m.ID},
|
||||
"Source": {m.Source},
|
||||
"Type": {m.Type},
|
||||
"ContentType": {m.ContentType},
|
||||
"Data": {m.Data},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("failure invoking '%v' (HTTP %v)", route, resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package function
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
// RunDataDir holds transient runtime metadata
|
||||
// By default it is excluded from source control.
|
||||
RunDataDir = ".func"
|
||||
)
|
||||
|
||||
// Job represents a running Function job (presumably started by this process'
|
||||
// Runner instance.
|
||||
type Job struct {
|
||||
Function Function
|
||||
Port string
|
||||
Errors chan error
|
||||
onStop func()
|
||||
}
|
||||
|
||||
// Create a new Job which represents a running Function task by providing
|
||||
// the port on which it was started, a channel on which runtime errors can
|
||||
// be received, and a stop function.
|
||||
func NewJob(f Function, port string, errs chan error, onStop func()) (*Job, error) {
|
||||
j := &Job{
|
||||
Function: f,
|
||||
Port: port,
|
||||
Errors: errs,
|
||||
onStop: onStop,
|
||||
}
|
||||
return j, j.save() // Everything is a file: save instance data to disk.
|
||||
}
|
||||
|
||||
// Stop the Job, running the provided stop delegate and removing runtime
|
||||
// metadata from disk.
|
||||
func (j *Job) Stop() {
|
||||
_ = j.remove() // Remove representation on disk
|
||||
j.onStop()
|
||||
}
|
||||
|
||||
func (j *Job) save() error {
|
||||
instancesDir := filepath.Join(j.Function.Root, RunDataDir, "instances")
|
||||
// job metadata is stored in <root>/.func/instances
|
||||
mkdir(instancesDir)
|
||||
|
||||
// create a file <root>/.func/instances/<port>
|
||||
file, err := os.Create(filepath.Join(instancesDir, j.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
|
||||
// Store the effective port for use by other client instances, possibly
|
||||
// in other processes, such as to run Invoke from other terminal in CLI apps.
|
||||
/*
|
||||
if err := writeFunc(f, "port", []byte(port)); err != nil {
|
||||
return j, err
|
||||
}
|
||||
return j, nil
|
||||
*/
|
||||
}
|
||||
|
||||
func (j *Job) remove() error {
|
||||
filename := filepath.Join(j.Function.Root, RunDataDir, "instances", j.Port)
|
||||
return os.Remove(filename)
|
||||
}
|
||||
|
||||
// jobPorts returns all the ports on which an instance of the given Function is
|
||||
// running. len is 0 when not running.
|
||||
// Improperly initialized or nonexistent (zero value) Functions are considered
|
||||
// to not be running.
|
||||
func jobPorts(f Function) []string {
|
||||
if f.Root == "" || !f.Initialized() {
|
||||
return []string{}
|
||||
}
|
||||
instancesDir := filepath.Join(f.Root, RunDataDir, "instances")
|
||||
mkdir(instancesDir)
|
||||
|
||||
files, err := os.ReadDir(instancesDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error reading %v", instancesDir)
|
||||
return []string{}
|
||||
}
|
||||
ports := []string{}
|
||||
for _, f := range files {
|
||||
ports = append(ports, f.Name())
|
||||
}
|
||||
return ports
|
||||
}
|
|
@ -31,7 +31,7 @@ func NewDescriber(namespaceOverride string) (describer *Describer, err error) {
|
|||
// restricts to label-syntax, which is thus escaped. Therefore as a knative (kube) implementation
|
||||
// detal proper full names have to be escaped on the way in and unescaped on the way out. ex:
|
||||
// www.example-site.com -> www-example--site-com
|
||||
func (d *Describer) Describe(ctx context.Context, name string) (description fn.Info, err error) {
|
||||
func (d *Describer) Describe(ctx context.Context, name string) (description fn.Instance, err error) {
|
||||
|
||||
servingClient, err := NewServingClient(d.namespace)
|
||||
if err != nil {
|
||||
|
@ -58,6 +58,11 @@ func (d *Describer) Describe(ctx context.Context, name string) (description fn.I
|
|||
routeURLs = append(routeURLs, route.Status.URL.String())
|
||||
}
|
||||
|
||||
primaryRouteURL := ""
|
||||
if len(routes.Items) > 0 {
|
||||
primaryRouteURL = routes.Items[0].Status.URL.String()
|
||||
}
|
||||
|
||||
triggers, err := eventingClient.ListTriggers(ctx)
|
||||
// IsNotFound -- Eventing is probably not installed on the cluster
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
|
@ -86,6 +91,7 @@ func (d *Describer) Describe(ctx context.Context, name string) (description fn.I
|
|||
|
||||
description.Name = name
|
||||
description.Namespace = d.namespace
|
||||
description.Route = primaryRouteURL
|
||||
description.Routes = routeURLs
|
||||
description.Subscriptions = subscriptions
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
)
|
||||
|
||||
type Describer struct {
|
||||
DescribeInvoked bool
|
||||
DescribeFn func(string) (fn.Instance, error)
|
||||
}
|
||||
|
||||
func NewDescriber() *Describer {
|
||||
return &Describer{
|
||||
DescribeFn: func(string) (fn.Instance, error) { return fn.Instance{}, nil },
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Describer) Describe(_ context.Context, name string) (fn.Instance, error) {
|
||||
l.DescribeInvoked = true
|
||||
return l.DescribeFn(name)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Emitter struct {
|
||||
EmitInvoked bool
|
||||
EmitFn func(string) error
|
||||
}
|
||||
|
||||
func NewEmitter() *Emitter {
|
||||
return &Emitter{
|
||||
EmitFn: func(string) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Emitter) Emit(ctx context.Context, s string) error {
|
||||
i.EmitInvoked = true
|
||||
return i.EmitFn(s)
|
||||
}
|
|
@ -2,21 +2,35 @@ package mock
|
|||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
)
|
||||
|
||||
// Runner runs a Function in a separate process, canceling it on context.Cancel.
|
||||
// Immediately returned is the port of the running Function.
|
||||
type Runner struct {
|
||||
RunInvoked bool
|
||||
RootRequested string
|
||||
RunFn func(context.Context, fn.Function) (*fn.Job, error)
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func NewRunner() *Runner {
|
||||
return &Runner{}
|
||||
return &Runner{
|
||||
RunFn: func(ctx context.Context, f fn.Function) (*fn.Job, error) {
|
||||
errs := make(chan error, 1)
|
||||
stop := func() {}
|
||||
return fn.NewJob(f, "8080", errs, stop)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) Run(ctx context.Context, f fn.Function) error {
|
||||
func (r *Runner) Run(ctx context.Context, f fn.Function) (*fn.Job, error) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.RunInvoked = true
|
||||
r.RootRequested = f.Root
|
||||
return nil
|
||||
|
||||
return r.RunFn(ctx, f)
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ import (
|
|||
// requisite test.
|
||||
const RepositoriesTestRepo = "repository"
|
||||
|
||||
// TestRepositoriesList ensures the base case of listing
|
||||
// TestRepositories_List ensures the base case of listing
|
||||
// repositories without error in the default scenario of builtin only.
|
||||
func TestRepositoriesList(t *testing.T) {
|
||||
func TestRepositories_List(t *testing.T) {
|
||||
root, rm := Mktemp(t)
|
||||
defer rm()
|
||||
|
||||
|
@ -38,9 +38,9 @@ func TestRepositoriesList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesGetInvalid ensures that attempting to get an invalid repo
|
||||
// TestRepositories_GetInvalid ensures that attempting to get an invalid repo
|
||||
// results in error.
|
||||
func TestRepositoriesGetInvalid(t *testing.T) {
|
||||
func TestRepositories_GetInvalid(t *testing.T) {
|
||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||
|
||||
// invalid should error
|
||||
|
@ -50,8 +50,8 @@ func TestRepositoriesGetInvalid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesGet ensures a repository can be accessed by name.
|
||||
func TestRepositoriesGet(t *testing.T) {
|
||||
// TestRepositories_Get ensures a repository can be accessed by name.
|
||||
func TestRepositories_Get(t *testing.T) {
|
||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||
|
||||
// valid should not error
|
||||
|
@ -66,9 +66,9 @@ func TestRepositoriesGet(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesAll ensures repos are returned from
|
||||
// TestRepositories_All ensures repos are returned from
|
||||
// .All accessor. Tests both builtin and buitlin+extensible cases.
|
||||
func TestRepositoriesAll(t *testing.T) {
|
||||
func TestRepositories_All(t *testing.T) {
|
||||
uri := TestRepoURI(RepositoriesTestRepo, t)
|
||||
root, rm := Mktemp(t)
|
||||
defer rm()
|
||||
|
@ -104,8 +104,8 @@ func TestRepositoriesAll(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesAdd checks basic adding of a repository by URI.
|
||||
func TestRepositoriesAdd(t *testing.T) {
|
||||
// TestRepositories_Add checks basic adding of a repository by URI.
|
||||
func TestRepositories_Add(t *testing.T) {
|
||||
uri := TestRepoURI(RepositoriesTestRepo, t) // ./testdata/$RepositoriesTestRepo.git
|
||||
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
||||
defer rm()
|
||||
|
@ -137,9 +137,9 @@ func TestRepositoriesAdd(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesAddDefaultName ensures that repository name is optional,
|
||||
// TestRepositories_AddDefaultName ensures that repository name is optional,
|
||||
// by default being set to the name of the repoisotory from the URI.
|
||||
func TestRepositoriesAddDeafultName(t *testing.T) {
|
||||
func TestRepositories_AddDeafultName(t *testing.T) {
|
||||
// The test repository is the "base case" repo, which is a manifestless
|
||||
// repo meant to exemplify the simplest use case: a repo with no metadata
|
||||
// that simply contains templates, grouped by runtime. It therefore does
|
||||
|
@ -171,10 +171,10 @@ func TestRepositoriesAddDeafultName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesAddWithManifest ensures that a repository with
|
||||
// TestRepositories_AddWithManifest ensures that a repository with
|
||||
// a manfest wherein a default name is specified, is used as the name for the
|
||||
// added repository when a name is not explicitly specified.
|
||||
func TestRepositoriesAddWithManifest(t *testing.T) {
|
||||
func TestRepositories_AddWithManifest(t *testing.T) {
|
||||
// repository-b is meant to exemplify the use case of a repository which
|
||||
// defines a custom language pack and makes full use of the manifest.yaml.
|
||||
// The manifest.yaml is included which specifies things like custom templates
|
||||
|
@ -207,9 +207,9 @@ func TestRepositoriesAddWithManifest(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesAddExistingErrors ensures that adding a repository that
|
||||
// TestRepositories_AddExistingErrors ensures that adding a repository that
|
||||
// already exists yields an error.
|
||||
func TestRepositoriesAddExistingErrors(t *testing.T) {
|
||||
func TestRepositories_AddExistingErrors(t *testing.T) {
|
||||
uri := TestRepoURI(RepositoriesTestRepo, t)
|
||||
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
||||
defer rm()
|
||||
|
@ -242,8 +242,8 @@ func TestRepositoriesAddExistingErrors(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesRename ensures renaming a repository succeeds.
|
||||
func TestRepositoriesRename(t *testing.T) {
|
||||
// TestRepositories_Rename ensures renaming a repository succeeds.
|
||||
func TestRepositories_Rename(t *testing.T) {
|
||||
uri := TestRepoURI(RepositoriesTestRepo, t)
|
||||
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
||||
defer rm()
|
||||
|
@ -275,9 +275,9 @@ func TestRepositoriesRename(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesRemove ensures that removing a repository by name
|
||||
// TestRepositories_Remove ensures that removing a repository by name
|
||||
// removes it from the list and FS.
|
||||
func TestRepositoriesRemove(t *testing.T) {
|
||||
func TestRepositories_Remove(t *testing.T) {
|
||||
uri := TestRepoURI(RepositoriesTestRepo, t) // ./testdata/repository.git
|
||||
root, rm := Mktemp(t) // create and cd to a temp dir
|
||||
defer rm()
|
||||
|
@ -310,9 +310,9 @@ func TestRepositoriesRemove(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesURL ensures that a repository populates its URL member
|
||||
// TestRepositories_URL ensures that a repository populates its URL member
|
||||
// from the git repository's origin url (if it is a git repo and exists)
|
||||
func TestRepositoriesURL(t *testing.T) {
|
||||
func TestRepositories_URL(t *testing.T) {
|
||||
// FIXME: This test is temporarily disabled. See not in Repository.Write
|
||||
// in short: as a side-effect of removing the double-clone, the in-memory
|
||||
// repo is insufficient as it does not include a .git directory.
|
||||
|
@ -344,13 +344,13 @@ func TestRepositoriesURL(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoriesMissing ensures that a missing repositores directory
|
||||
// TestRepositories_Missing ensures that a missing repositores directory
|
||||
// does not cause an error unless it was explicitly set (zero value indicates
|
||||
// no repos should be loaded from os).
|
||||
// This may change in an upcoming release where the repositories directory
|
||||
// will be created at the config path if it does not exist, but this requires
|
||||
// first moving the defaulting path logic from CLI into the client lib.
|
||||
func TestRepositoriesMissing(t *testing.T) {
|
||||
func TestRepositories_Missing(t *testing.T) {
|
||||
// Client with no repositories path defined.
|
||||
client := fn.New()
|
||||
|
||||
|
|
|
@ -26,6 +26,10 @@ const (
|
|||
DefaultLivenessEndpoint = "/health/liveness"
|
||||
// DefaultTemplatesPath is the root of the defined repository
|
||||
DefaultTemplatesPath = "."
|
||||
// DefaultInvocationFormat is a named invocation hint for the convenience
|
||||
// helper .Invoke. It is usually set at the template level. The default
|
||||
// ('http') is a plain HTTP POST.
|
||||
DefaultInvocationFormat = "http"
|
||||
|
||||
// Defaults for Builder and Builders not expressly defined as a purposeful
|
||||
// delegation of choice.
|
||||
|
@ -66,6 +70,10 @@ type Repository struct {
|
|||
// TODO upgrade to fs.FS introduced in go1.16
|
||||
FS Filesystem
|
||||
|
||||
// Invocation hints for all templates in this repo
|
||||
// (it is more likely this will be set only at the template level)
|
||||
Invocation Invocation `yaml:"invocation,omitempty"`
|
||||
|
||||
uri string // URI which was used when initially creating
|
||||
|
||||
}
|
||||
|
@ -92,6 +100,10 @@ type Runtime struct {
|
|||
// of Runtime.
|
||||
BuildConfig `yaml:",inline"`
|
||||
|
||||
// Invocation hints for all templates in this runtime
|
||||
// (it is more likely this will be set only at the template level)
|
||||
Invocation Invocation `yaml:"invocation,omitempty"`
|
||||
|
||||
// Templates defined for the runtime
|
||||
Templates []Template
|
||||
}
|
||||
|
@ -121,6 +133,7 @@ func NewRepository(name, uri string) (r Repository, err error) {
|
|||
Liveness: DefaultLivenessEndpoint,
|
||||
Readiness: DefaultLivenessEndpoint,
|
||||
},
|
||||
Invocation: Invocation{Format: DefaultInvocationFormat},
|
||||
}
|
||||
r.FS, err = filesystemFromURI(uri) // Get a Filesystem from the URI
|
||||
if err != nil {
|
||||
|
@ -240,6 +253,7 @@ func repositoryRuntimes(r Repository) (runtimes []Runtime, err error) {
|
|||
BuildConfig: r.BuildConfig,
|
||||
HealthEndpoints: r.HealthEndpoints,
|
||||
BuildEnvs: r.BuildEnvs,
|
||||
Invocation: r.Invocation,
|
||||
}
|
||||
// Runtime Manifest
|
||||
// Load the file if it exists, which may override values inherited from the
|
||||
|
@ -293,6 +307,7 @@ func runtimeTemplates(r Repository, runtime Runtime) (templates []Template, err
|
|||
BuildConfig: runtime.BuildConfig,
|
||||
HealthEndpoints: runtime.HealthEndpoints,
|
||||
BuildEnvs: runtime.BuildEnvs,
|
||||
Invocation: runtime.Invocation,
|
||||
}
|
||||
|
||||
// Template Manifeset
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
fn "knative.dev/kn-plugin-func"
|
||||
)
|
||||
|
||||
// TestRepositoryTemplatesPath ensures that repositories can specify
|
||||
// TestRepository_TemplatesPath ensures that repositories can specify
|
||||
// an alternate location for templates using a manifest.
|
||||
func TestRepositoryTemplatesPath(t *testing.T) {
|
||||
func TestRepository_TemplatesPath(t *testing.T) {
|
||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||
|
||||
// The repo ./testdata/repositories/customLanguagePackRepo includes a
|
||||
|
@ -34,11 +34,11 @@ func TestRepositoryTemplatesPath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoryInheritance ensures that repositories which define a manifest
|
||||
// TestRepository_Inheritance ensures that repositories which define a manifest
|
||||
// properly inherit values defined at the repo level, runtime level
|
||||
// and template level. The tests check for both embedded structures:
|
||||
// HealthEndpoints BuildConfig.
|
||||
func TestRepositoryInheritance(t *testing.T) {
|
||||
func TestRepository_Inheritance(t *testing.T) {
|
||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||
|
||||
// The repo ./testdata/repositories/customLanguagePack includes a manifest
|
||||
|
|
|
@ -138,6 +138,10 @@
|
|||
"Created": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"invocation": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$ref": "#/definitions/Invocation"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
@ -170,6 +174,15 @@
|
|||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Invocation": {
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"Label": {
|
||||
"required": [
|
||||
"key"
|
||||
|
|
|
@ -5,22 +5,31 @@ type Template struct {
|
|||
// Name (short name) of this template within the repository.
|
||||
// See .Fullname for the calculated field which is the unique primary id.
|
||||
Name string `yaml:"-"` // use filesystem for name, not yaml
|
||||
|
||||
// Runtime for which this template applies.
|
||||
Runtime string
|
||||
|
||||
// Repository within which this template is contained. Value is set to the
|
||||
// currently effective name of the repository, which may vary. It is user-
|
||||
// defined when the repository is added, and can be set to "default" when
|
||||
// the client is loaded in single repo mode. I.e. not canonical.
|
||||
Repository string
|
||||
|
||||
// BuildConfig defines builders and buildpacks. the denormalized view of
|
||||
// members which can be defined per repo or per runtime first.
|
||||
BuildConfig `yaml:",inline"`
|
||||
|
||||
// HealthEndpoints. The denormalized view of members which can be defined
|
||||
// first per repo or per runtime.
|
||||
HealthEndpoints `yaml:"healthEndpoints,omitempty"`
|
||||
|
||||
// BuildEnvs defines environment variables related to the builders,
|
||||
// this can be used to parameterize the builders
|
||||
BuildEnvs []Env `yaml:"buildEnvs,omitempty"`
|
||||
|
||||
// Invocation defines invocation hints for a Functions which is created
|
||||
// from this template prior to being materially modified.
|
||||
Invocation Invocation `yaml:"invocation,omitempty"`
|
||||
}
|
||||
|
||||
// Fullname is a calculated field of [repo]/[name] used
|
||||
|
|
|
@ -160,6 +160,9 @@ func (t *Templates) Write(f Function) (Function, error) {
|
|||
if f.HealthEndpoints.Readiness == "" {
|
||||
f.HealthEndpoints.Readiness = template.HealthEndpoints.Readiness
|
||||
}
|
||||
if f.Invocation.Format == "" {
|
||||
f.Invocation.Format = template.Invocation.Format
|
||||
}
|
||||
|
||||
// Copy the template files from the repo filesystem to the new Function's root
|
||||
// removing the manifest (if it exists; errors ignored)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# optional. Invocation defines hints for how Functions created using this
|
||||
# template can be invoked. These settings can be updated on the resultant
|
||||
# Function as development progresses to ensure 'invoke' can always trigger the
|
||||
# execution of a running Function instance for testing and development.
|
||||
invocation:
|
||||
# Invocations of Functions from this template is via basic HTTP CloudEvent
|
||||
format: "cloudevent"
|
|
@ -0,0 +1,8 @@
|
|||
# optional. Invocation defines hints for how Functions created using this
|
||||
# template can be invoked. These settings can be updated on the resultant
|
||||
# Function as development progresses to ensure 'invoke' can always trigger the
|
||||
# execution of a running Function instance for testing and development.
|
||||
invocation:
|
||||
# The invocation format for this template is a standard HTTP request, which
|
||||
# by default is an HTTP POST of form fields of the invocation message.
|
||||
format: "http"
|
|
@ -1,2 +1,4 @@
|
|||
builders:
|
||||
default: quay.io/boson/faas-python-builder:v0.8.4
|
||||
invocation:
|
||||
format: "cloudevent"
|
||||
|
|
|
@ -2,3 +2,6 @@ builders:
|
|||
default: quay.io/boson/faas-jvm-builder:v0.8.4
|
||||
jvm: quay.io/boson/faas-jvm-builder:v0.8.4
|
||||
native: quay.io/boson/faas-quarkus-native-builder:v0.8.4
|
||||
|
||||
invocation:
|
||||
format: "cloudevent"
|
||||
|
|
|
@ -6,3 +6,6 @@ builders:
|
|||
buildpacks:
|
||||
- paketo-buildpacks/nodejs
|
||||
- ghcr.io/boson-project/typescript-function-buildpack:tip
|
||||
|
||||
invocation:
|
||||
format: "cloudevent"
|
||||
|
|
|
@ -17,9 +17,9 @@ import (
|
|||
. "knative.dev/kn-plugin-func/testing"
|
||||
)
|
||||
|
||||
// TestTemplatesList ensures that all templates are listed taking into account
|
||||
// TestTemplates_List ensures that all templates are listed taking into account
|
||||
// both internal and extensible (prefixed) repositories.
|
||||
func TestTemplatesList(t *testing.T) {
|
||||
func TestTemplates_List(t *testing.T) {
|
||||
// A client which specifies a location of exensible repositoreis on disk
|
||||
// will list all builtin plus exensible
|
||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||
|
@ -45,13 +45,14 @@ func TestTemplatesList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplatesListExtendedNotFound ensures that an error is not returned
|
||||
// TestTemplates_List_ExtendedNotFound ensures that an error is not returned
|
||||
// when retrieving the list of templates for a runtime that does not exist
|
||||
// in an extended repository, but does in the default.
|
||||
func TestTemplatesListExtendedNotFound(t *testing.T) {
|
||||
func TestTemplates_List_ExtendedNotFound(t *testing.T) {
|
||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||
|
||||
// list templates for the "python" runtime - not supplied by the extended repos
|
||||
// list templates for the "python" runtime -
|
||||
// not supplied by the extended repos
|
||||
templates, err := client.Templates().List("python")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -67,9 +68,9 @@ func TestTemplatesListExtendedNotFound(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplatesGet ensures that a template's metadata object can
|
||||
// TestTemplates_Get ensures that a template's metadata object can
|
||||
// be retrieved by full name (full name prefix optional for embedded).
|
||||
func TestTemplatesGet(t *testing.T) {
|
||||
func TestTemplates_Get(t *testing.T) {
|
||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||
|
||||
// Check embedded
|
||||
|
@ -95,10 +96,10 @@ func TestTemplatesGet(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateEmbedded ensures that embedded templates are copied on write.
|
||||
func TestTemplateEmbedded(t *testing.T) {
|
||||
// TestTemplates_Embedded ensures that embedded templates are copied on write.
|
||||
func TestTemplates_Embedded(t *testing.T) {
|
||||
// create test directory
|
||||
root := "testdata/testTemplateEmbedded"
|
||||
root := "testdata/testTemplatesEmbedded"
|
||||
defer Using(t, root)()
|
||||
|
||||
// Client whose internal (builtin default) templates will be used.
|
||||
|
@ -121,12 +122,12 @@ func TestTemplateEmbedded(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateCustom ensures that a template from a filesystem source
|
||||
// TestTemplates_Custom ensures that a template from a filesystem source
|
||||
// (ie. custom provider on disk) can be specified as the source for a
|
||||
// template.
|
||||
func TestTemplateCustom(t *testing.T) {
|
||||
func TestTemplates_Custom(t *testing.T) {
|
||||
// Create test directory
|
||||
root := "testdata/testTemplateCustom"
|
||||
root := "testdata/testTemplatesCustom"
|
||||
defer Using(t, root)()
|
||||
|
||||
// CLient which uses custom repositories
|
||||
|
@ -154,11 +155,11 @@ func TestTemplateCustom(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateRemote ensures that a Git template repository provided via URI
|
||||
// TestTemplates_Remote ensures that a Git template repository provided via URI
|
||||
// can be specificed on creation of client, with subsequent calls to Create
|
||||
// using this remote by default.
|
||||
func TestTemplateRemote(t *testing.T) {
|
||||
root := "testdata/testTemplateRemote"
|
||||
func TestTemplates_Remote(t *testing.T) {
|
||||
root := "testdata/testTemplatesRemote"
|
||||
defer Using(t, root)()
|
||||
|
||||
// The difference between HTTP vs File protocol is internal to the
|
||||
|
@ -197,11 +198,11 @@ func TestTemplateRemote(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateDefault ensures that the expected default template
|
||||
// TestTemplates_Default ensures that the expected default template
|
||||
// is used when none specified.
|
||||
func TestTemplateDefault(t *testing.T) {
|
||||
func TestTemplates_Default(t *testing.T) {
|
||||
// create test directory
|
||||
root := "testdata/testTemplateDefault"
|
||||
root := "testdata/testTemplates_Default"
|
||||
defer Using(t, root)()
|
||||
|
||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||
|
@ -220,11 +221,11 @@ func TestTemplateDefault(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateInvalidErrors ensures that specifying unrecgognized
|
||||
// TestTemplates_InvalidErrors ensures that specifying unrecgognized
|
||||
// runtime/template errors
|
||||
func TestTemplateInvalidErrors(t *testing.T) {
|
||||
func TestTemplates_InvalidErrors(t *testing.T) {
|
||||
// create test directory
|
||||
root := "testdata/testTemplateInvalidErrors"
|
||||
root := "testdata/testTemplates_InvalidErrors"
|
||||
defer Using(t, root)()
|
||||
|
||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||
|
@ -240,6 +241,7 @@ func TestTemplateInvalidErrors(t *testing.T) {
|
|||
if !errors.Is(err, fn.ErrRuntimeNotFound) {
|
||||
t.Fatalf("Expected ErrRuntimeNotFound, got %v", err)
|
||||
}
|
||||
os.Remove(filepath.Join(root, ".gitignore"))
|
||||
|
||||
// Test for error writing an invalid template
|
||||
err = client.Create(fn.Function{
|
||||
|
@ -252,16 +254,16 @@ func TestTemplateInvalidErrors(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateModeEmbedded ensures that templates written from the embedded
|
||||
// TestTemplates_ModeEmbedded ensures that templates written from the embedded
|
||||
// templates retain their mode.
|
||||
func TestTemplateModeEmbedded(t *testing.T) {
|
||||
func TestTemplates_ModeEmbedded(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return
|
||||
// not applicable
|
||||
}
|
||||
|
||||
// set up test directory
|
||||
root := "testdata/testTemplateModeEmbedded"
|
||||
root := "testdata/testTemplatesModeEmbedded"
|
||||
defer Using(t, root)()
|
||||
|
||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||
|
@ -287,15 +289,15 @@ func TestTemplateModeEmbedded(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateModeCustom ensures that templates written from custom templates
|
||||
// TestTemplates_ModeCustom ensures that templates written from custom templates
|
||||
// retain their mode.
|
||||
func TestTemplateModeCustom(t *testing.T) {
|
||||
func TestTemplates_ModeCustom(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return // not applicable
|
||||
}
|
||||
|
||||
// test directories
|
||||
root := "testdata/testTemplateModeCustom"
|
||||
root := "testdata/testTemplates_ModeCustom"
|
||||
defer Using(t, root)()
|
||||
|
||||
client := fn.New(
|
||||
|
@ -322,15 +324,15 @@ func TestTemplateModeCustom(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateModeRemote ensures that templates written from remote templates
|
||||
// TestTemplates_ModeRemote ensures that templates written from remote templates
|
||||
// retain their mode.
|
||||
func TestTemplateModeRemote(t *testing.T) {
|
||||
func TestTemplates_ModeRemote(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return // not applicable
|
||||
}
|
||||
|
||||
// test directories
|
||||
root := "testdata/testTemplateModeRemote"
|
||||
root := "testdata/testTemplates_ModeRemote"
|
||||
defer Using(t, root)()
|
||||
|
||||
// Clone a repository from a local file path
|
||||
|
@ -376,11 +378,11 @@ func TestTemplateModeRemote(t *testing.T) {
|
|||
|
||||
// TODO: test typed errors for custom and remote (embedded checked)
|
||||
|
||||
// TestRuntimeManifestBuildEnvs ensures that BuildEnvs specified in a
|
||||
// TestTemplates_RuntimeManifestBuildEnvs ensures that BuildEnvs specified in a
|
||||
// runtimes's manifest are included in the final Function.
|
||||
func TestRuntimeManifestBuildEnvs(t *testing.T) {
|
||||
func TestTemplates_RuntimeManifestBuildEnvs(t *testing.T) {
|
||||
// create test directory
|
||||
root := "testdata/testRuntimeManifestBuildEnvs"
|
||||
root := "testdata/testTemplatesRuntimeManifestBuildEnvs"
|
||||
defer Using(t, root)()
|
||||
|
||||
// Client whose internal templates will be used.
|
||||
|
@ -423,11 +425,11 @@ func TestRuntimeManifestBuildEnvs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateManifestBuildEnvs ensures that BuildEnvs specified in a
|
||||
// TestTemplates_ManifestBuildEnvs ensures that BuildEnvs specified in a
|
||||
// template's manifest are included in the final Function.
|
||||
func TestTemplateManifestBuildEnvs(t *testing.T) {
|
||||
func TestTemplates_ManifestBuildEnvs(t *testing.T) {
|
||||
// create test directory
|
||||
root := "testdata/testTemplateManifestBuildEnvs"
|
||||
root := "testdata/testTemplatesManifestBuildEnvs"
|
||||
defer Using(t, root)()
|
||||
|
||||
// Client whose internal templates will be used.
|
||||
|
@ -470,9 +472,9 @@ func TestTemplateManifestBuildEnvs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRepositoryManifestBuildEnvs ensures that BuildEnvs specified in a
|
||||
// TestTemplates_RepositoryManifestBuildEnvs ensures that BuildEnvs specified in a
|
||||
// repository's manifest are included in the final Function.
|
||||
func TestRepositoryManifestBuildEnvs(t *testing.T) {
|
||||
func TestTemplates_RepositoryManifestBuildEnvs(t *testing.T) {
|
||||
// create test directory
|
||||
root := "testdata/testRepositoryManifestBuildEnvs"
|
||||
defer Using(t, root)()
|
||||
|
@ -517,9 +519,38 @@ func TestRepositoryManifestBuildEnvs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTemplateManifestRemoved ensures that the manifest is not left in
|
||||
// TestTemplates_ManifestInvocationHints ensures that invocation hints
|
||||
// from a template's manifest are included in the final Function.
|
||||
func TestTemplates_ManifestInvocationHints(t *testing.T) {
|
||||
root := "testdata/testTemplatesManifestInvocationHints"
|
||||
defer Using(t, root)()
|
||||
|
||||
client := fn.New(
|
||||
fn.WithRegistry(TestRegistry),
|
||||
fn.WithRepositories("testdata/repositories"))
|
||||
|
||||
err := client.Create(fn.Function{
|
||||
Root: root,
|
||||
Runtime: "manifestedRuntime",
|
||||
Template: "customLanguagePackRepo/manifestedTemplate",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := fn.NewFunction(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if f.Invocation.Format != "format" {
|
||||
t.Fatalf("expected invocation format 'format', got '%v'", f.Invocation.Format)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplates_ManifestRemoved ensures that the manifest is not left in
|
||||
// the resultant Function after write.
|
||||
func TestTemplateManifestRemoved(t *testing.T) {
|
||||
func TestTemplates_ManifestRemoved(t *testing.T) {
|
||||
// create test directory
|
||||
root := "testdata/testTemplateManifestRemoved"
|
||||
defer Using(t, root)()
|
||||
|
@ -551,3 +582,35 @@ func TestTemplateManifestRemoved(t *testing.T) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// TestTemplates_InvocationDefault ensures that creating a Function which
|
||||
// does not define an invocation hint defaults to the DefaultInvocationFormat
|
||||
// (http post)
|
||||
func TestTemplates_InvocationDefault(t *testing.T) {
|
||||
root := "testdata/testTemplatesInvocationDefault"
|
||||
defer Using(t, root)()
|
||||
|
||||
client := fn.New(
|
||||
fn.WithRegistry(TestRegistry),
|
||||
fn.WithRepositories("testdata/repositories"))
|
||||
|
||||
// The customTemplateRepo explicitly does not
|
||||
// include manifests as it exemplifies an entirely default template repo.
|
||||
err := client.Create(fn.Function{
|
||||
Root: root,
|
||||
Runtime: "customRuntime",
|
||||
Template: "customTemplateRepo/customTemplate",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := fn.NewFunction(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if f.Invocation.Format != fn.DefaultInvocationFormat {
|
||||
t.Fatalf("expected '%v' invocation format. Got '%v'", fn.DefaultInvocationFormat, f.Invocation.Format)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
//go:build e2e
|
||||
// +build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEmitCommand validates func emit command
|
||||
// A custom node js Function used to test 'func emit' command (see update_templates/node/events/index.js)
|
||||
// An event is sent using emit with a special event source 'func:emit', expected by the custom function.
|
||||
// When this source is matched, the event will get stored globally and will be returned
|
||||
// as HTTP response next time it receives another event with source "e2e:check"
|
||||
// A better solution could be evaluated in future.
|
||||
func TestEmitCommand(t *testing.T) {
|
||||
|
||||
project := FunctionTestProject{
|
||||
FunctionName: "emit-test-node",
|
||||
ProjectPath: filepath.Join(os.TempDir(), "emit-test-node"),
|
||||
Runtime: "node",
|
||||
Template: "cloudevents",
|
||||
}
|
||||
knFunc := NewKnFuncShellCli(t)
|
||||
|
||||
// Create new project
|
||||
Create(t, knFunc, project)
|
||||
defer project.RemoveProjectFolder()
|
||||
|
||||
//knFunc.Exec("build", "-r", GetRegistry(), "-p", project.ProjectPath, "-b", "quay.io/boson/faas-nodejs-builder:v0.7.1")
|
||||
|
||||
// Update the project folder with the content of update_templates/node/events/// and deploy it
|
||||
Update(t, knFunc, &project)
|
||||
defer Delete(t, knFunc, &project)
|
||||
|
||||
// Issue Func Emit command
|
||||
emitMessage := "HELLO FROM EMIT"
|
||||
result := knFunc.Exec("emit", "--content-type", "text/plain", "--data", emitMessage, "--source", "func:emit", "--path", project.ProjectPath)
|
||||
if result.Error != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// Issue another event (in order to capture the event sent by emit)
|
||||
testEvent := SimpleTestEvent{
|
||||
Type: "e2e:check",
|
||||
Source: "e2e:check",
|
||||
ContentType: "text/plain",
|
||||
Data: "Emit Check",
|
||||
}
|
||||
responseBody, _, err := testEvent.pushTo(project.FunctionURL, t)
|
||||
if err != nil {
|
||||
t.Fatal("error occurred while sending event", err.Error())
|
||||
}
|
||||
if responseBody == "" || !strings.Contains(responseBody, emitMessage) {
|
||||
t.Fatalf("fail to validate emit command. Expected [%v], returned [%v]", emitMessage, responseBody)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
//go:build e2e
|
||||
// +build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
cloudevents "github.com/cloudevents/sdk-go/v2"
|
||||
. "knative.dev/kn-plugin-func/testing"
|
||||
)
|
||||
|
||||
// TestInvokeFunction is used when testing the 'func invoke' subcommand.
|
||||
// It responds with a CloudEvent containing an echo of the data received
|
||||
// If the CloudEvent received has source "func:set" it will update the
|
||||
// current value of data.
|
||||
var TestInvokeFunctionImpl = `
|
||||
package function
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ce "github.com/cloudevents/sdk-go/v2/event"
|
||||
)
|
||||
|
||||
var _data = []byte{}
|
||||
var _type = "text/plain"
|
||||
|
||||
func Handle(ctx context.Context, event ce.Event) (*ce.Event, error) {
|
||||
if event.Source() == "func:set" {
|
||||
_data = event.Data()
|
||||
_type = event.DataContentType()
|
||||
}
|
||||
res := ce.New()
|
||||
res.SetSource("func:testInvokeHandler")
|
||||
res.SetData(_type, _data)
|
||||
return &res, nil
|
||||
}
|
||||
`
|
||||
|
||||
// TestInvoke ensures that invoking a CloudEvent Function succeeds, including
|
||||
// preserving custom values through the full round-trip.
|
||||
func TestInvoke(t *testing.T) {
|
||||
var (
|
||||
root = "testdata/e2e/testinvoke" // root path for the test Function
|
||||
bin, prefix = bin() // path to test binary and prefix args
|
||||
cleanup = Within(t, root) // Create and CD to root.
|
||||
cwd, _ = os.Getwd() // the current working directory (absolute)
|
||||
)
|
||||
defer cleanup()
|
||||
|
||||
run(t, bin, prefix, "create", "--verbose=true", "--language=go", "--template=cloudevents", cwd)
|
||||
set(t, "handle.go", TestInvokeFunctionImpl)
|
||||
run(t, bin, prefix, "deploy", "--verbose=true", "--registry", GetRegistry())
|
||||
run(t, bin, prefix, "invoke", "--verbose=true", "--content-type=text/plain", "--source=func:set", "--data=TEST")
|
||||
|
||||
// Validate by fetching the contents of the Function's data global
|
||||
fmt.Println("Validate:")
|
||||
req := cloudevents.NewEvent()
|
||||
req.SetID("1")
|
||||
req.SetSource("func:get")
|
||||
req.SetType("func.test")
|
||||
c, err := cloudevents.NewClientHTTP(cloudevents.WithTarget("http://testinvoke.default.127.0.0.1.sslip.io"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res, err := c.Request(context.Background(), req)
|
||||
if cloudevents.IsUndelivered(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(res.Data()) != "TEST" {
|
||||
t.Fatalf("expected data 'TEST' got '%v'", string(res.Data()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// bin returns the path to use for the binary plus any leading args that
|
||||
// should be prepended.
|
||||
// For example, this will usually either return `/path/to/func` or `kn func`.
|
||||
// See NewKnFuncShellCli for original source of this logic.
|
||||
func bin() (path string, args []string) {
|
||||
if IsUseKnFunc() {
|
||||
return "kn", []string{"func"}
|
||||
}
|
||||
if path = GetFuncBinaryPath(); path == "" {
|
||||
fmt.Fprintf(os.Stderr, "'E2E_FUNC_BIN_PATH' or 'E2E_USE_KN_FUNC' can be used to specify test binary path.")
|
||||
return "func", []string{} //default
|
||||
}
|
||||
return path, []string{}
|
||||
}
|
||||
|
||||
// run the given binary with the given two sets of arguments
|
||||
// This allows for swappable running between the two forms:
|
||||
// func [subcommand] [flags]
|
||||
// and
|
||||
// kn func [subcommand] [flags]
|
||||
func run(t *testing.T, bin string, prefix []string, suffix ...string) {
|
||||
t.Helper()
|
||||
args := append(prefix, suffix...)
|
||||
fmt.Printf("%v %v\n", bin, strings.Join(args, " "))
|
||||
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// set the contents of the given file
|
||||
func set(t *testing.T, path, data string) {
|
||||
if err := os.WriteFile(path, []byte(data), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -1,25 +1,25 @@
|
|||
'use strict';
|
||||
const { CloudEvent, HTTP } = require('cloudevents');
|
||||
|
||||
let lastEmitEventData = ""
|
||||
let lastInvokeEventData = ""
|
||||
|
||||
/**
|
||||
* Function used to test 'func emit' command
|
||||
* The trick here is sending the event using emit with a given event source 'func:emit'.
|
||||
* Function used to test 'func invoke' command
|
||||
* The trick here is sending the event using emit with a given event source 'func:invoke'.
|
||||
* For this source the consumed event will be stored and returned as http response when it received
|
||||
* another event with source 'e2e:check'.
|
||||
*
|
||||
* 1) function will consume and store the data "hello emit"
|
||||
* kn func emit -c "text/plain" -d "hello emit" -s "func:emit"
|
||||
* 1) function will consume and store the data "hello invoke"
|
||||
* kn func invoke -c "text/plain" -d "hello invoke" -s "func:invoke"
|
||||
*
|
||||
* 2) the below should return "hello emit" from previous command
|
||||
* 2) the below should return "hello invoke" from previous command
|
||||
* curl $node_func_url -X POST \
|
||||
* -H "Ce-Id: some-message-id" \
|
||||
* -H "Ce-Specversion: 1.0" \
|
||||
* -H "Ce-Type: e2e:check" \
|
||||
* -H "Ce-Source: e2e:check" \
|
||||
* -H "Content-Type: text/plain" \
|
||||
* -d 'Emit Check'
|
||||
* -d 'Invoke Check'
|
||||
*
|
||||
*
|
||||
* @param context
|
||||
|
@ -33,16 +33,16 @@ function handle(context, cloudevent) {
|
|||
};
|
||||
}
|
||||
|
||||
if (cloudevent.source == "func:emit") {
|
||||
if (cloudevent.source == "func:invoke") {
|
||||
context.log.info(`CloudEvent received : ${cloudevent.toString()}`);
|
||||
lastEmitEventData = cloudevent.data
|
||||
lastInvokeEventData = cloudevent.data
|
||||
}
|
||||
|
||||
if (cloudevent.source == "e2e:check") {
|
||||
return HTTP.binary(new CloudEvent({
|
||||
source: 'test:handle',
|
||||
type: 'test:emit',
|
||||
data: lastEmitEventData
|
||||
type: 'test:invoke',
|
||||
data: lastInvokeEventData
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -9,3 +9,13 @@ buildpacks:
|
|||
buildEnvs:
|
||||
- name: "TEST_TEMPLATE_VARIABLE"
|
||||
value: "test-template"
|
||||
|
||||
# optional. Invocation defines hints for how Functions created using this
|
||||
# template can be invoked. These settings can be updated on the resultant
|
||||
# Function as development progresses to ensure 'invoke' can always trigger the
|
||||
# execution of a running Function instance for testing and development.
|
||||
invocation:
|
||||
# The default invocation format is 'http': a basic HTTP POST of form values.
|
||||
# Formats not understood by the system fall back to this such that there
|
||||
# is graceful degredation of service when new formats are added.
|
||||
format: "format"
|
||||
|
|
Loading…
Reference in New Issue