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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -52,10 +53,11 @@ type Client struct {
|
||||||
dnsProvider DNSProvider // Provider of DNS services
|
dnsProvider DNSProvider // Provider of DNS services
|
||||||
registry string // default registry for OCI image tags
|
registry string // default registry for OCI image tags
|
||||||
progressListener ProgressListener // progress listener
|
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
|
repositories *Repositories // Repositories management
|
||||||
templates *Templates // Templates 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.
|
// ErrNotBuilt indicates the Function has not yet been built.
|
||||||
|
@ -96,8 +98,10 @@ const (
|
||||||
|
|
||||||
// Runner runs the Function locally.
|
// Runner runs the Function locally.
|
||||||
type Runner interface {
|
type Runner interface {
|
||||||
// Run the Function locally.
|
// Run the Function, returning a Job with metadata, error channels, and
|
||||||
Run(context.Context, Function) error
|
// 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.
|
// Remover of deployed services.
|
||||||
|
@ -138,18 +142,30 @@ type ProgressListener interface {
|
||||||
Done()
|
Done()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Describer of Functions' remote deployed aspect.
|
// Describer of Function instances
|
||||||
type Describer interface {
|
type Describer interface {
|
||||||
// Describe the running state of the service as reported by the underlyng platform.
|
// Describe the named Function in the remote environment.
|
||||||
Describe(ctx context.Context, name string) (description Info, err error)
|
Describe(ctx context.Context, name string) (Instance, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info about a given Function
|
// Instance data about the runtime state of a Function in a given environment.
|
||||||
type Info struct {
|
//
|
||||||
|
// 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"`
|
Name string `json:"name" yaml:"name"`
|
||||||
Image string `json:"image" yaml:"image"`
|
Image string `json:"image" yaml:"image"`
|
||||||
Namespace string `json:"namespace" yaml:"namespace"`
|
Namespace string `json:"namespace" yaml:"namespace"`
|
||||||
Routes []string `json:"routes" yaml:"routes"`
|
|
||||||
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
|
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,11 +182,6 @@ type DNSProvider interface {
|
||||||
Provide(Function) error
|
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
|
// PipelinesProvider manages lifecyle of CI/CD pipelines used by a Function
|
||||||
type PipelinesProvider interface {
|
type PipelinesProvider interface {
|
||||||
Run(context.Context, Function) error
|
Run(context.Context, Function) error
|
||||||
|
@ -186,18 +197,21 @@ func New(options ...Option) *Client {
|
||||||
runner: &noopRunner{output: os.Stdout},
|
runner: &noopRunner{output: os.Stdout},
|
||||||
remover: &noopRemover{output: os.Stdout},
|
remover: &noopRemover{output: os.Stdout},
|
||||||
lister: &noopLister{output: os.Stdout},
|
lister: &noopLister{output: os.Stdout},
|
||||||
|
describer: &noopDescriber{output: os.Stdout},
|
||||||
dnsProvider: &noopDNSProvider{output: os.Stdout},
|
dnsProvider: &noopDNSProvider{output: os.Stdout},
|
||||||
progressListener: &NoopProgressListener{},
|
progressListener: &NoopProgressListener{},
|
||||||
emitter: &noopEmitter{},
|
|
||||||
pipelinesProvider: &noopPipelinesProvider{},
|
pipelinesProvider: &noopPipelinesProvider{},
|
||||||
repositoriesPath: filepath.Join(ConfigPath(), "repositories"),
|
repositoriesPath: filepath.Join(ConfigPath(), "repositories"),
|
||||||
|
transport: http.DefaultTransport,
|
||||||
}
|
}
|
||||||
for _, o := range options {
|
for _, o := range options {
|
||||||
o(c)
|
o(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize sub-managers using now-fully-initialized client.
|
// Initialize sub-managers using now-fully-initialized client.
|
||||||
c.repositories = newRepositories(c)
|
c.repositories = newRepositories(c)
|
||||||
c.templates = newTemplates(c)
|
c.templates = newTemplates(c)
|
||||||
|
c.instances = newInstances(c)
|
||||||
|
|
||||||
// Trigger the creation of the config and repository paths
|
// Trigger the creation of the config and repository paths
|
||||||
_ = ConfigPath() // Config is package-global scoped
|
_ = 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
|
// WithTransport sets a custom transport to use internally.
|
||||||
// a CloudEvent to an arbitrary function endpoint
|
func WithTransport(t http.RoundTripper) Option {
|
||||||
func WithEmitter(e Emitter) Option {
|
|
||||||
return func(c *Client) {
|
return func(c *Client) {
|
||||||
c.emitter = e
|
c.transport = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,6 +396,11 @@ func (c *Client) Templates() *Templates {
|
||||||
return c.templates
|
return c.templates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Instances accessor
|
||||||
|
func (c *Client) Instances() *Instances {
|
||||||
|
return c.instances
|
||||||
|
}
|
||||||
|
|
||||||
// Runtimes available in totality.
|
// Runtimes available in totality.
|
||||||
// Not all repository/template combinations necessarily exist,
|
// Not all repository/template combinations necessarily exist,
|
||||||
// and further validation is performed when a template+runtime is chosen.
|
// 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
|
return runtimes.Items(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// METHODS
|
// LIFECYCLE METHODS
|
||||||
// ---------
|
// -----------------
|
||||||
|
|
||||||
// New Function.
|
// New Function.
|
||||||
// Use Create, Build and Deploy independently for lower level control.
|
// 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)
|
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
|
// Path is defaulted to the current working directory
|
||||||
if cfg.Root == "" {
|
if cfg.Root == "" {
|
||||||
if cfg.Root, err = os.Getwd(); err != nil {
|
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)
|
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)
|
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.
|
// Write out the new Function's Template files.
|
||||||
// Templates contain values which may result in the Function being mutated
|
// Templates contain values which may result in the Function being mutated
|
||||||
// (default builders, etc), so a new (potentially mutated) Function is
|
// (default builders, etc), so a new (potentially mutated) Function is
|
||||||
|
@ -548,33 +571,36 @@ func (c *Client) Create(cfg Function) (err error) {
|
||||||
return
|
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
|
// Build the Function at path. Errors if the Function is either unloadable or does
|
||||||
// not contain a populated Image.
|
// not contain a populated Image.
|
||||||
func (c *Client) Build(ctx context.Context, path string) (err error) {
|
func (c *Client) Build(ctx context.Context, path string) (err error) {
|
||||||
c.progressListener.Increment("Building function image")
|
c.progressListener.Increment("Building function image")
|
||||||
|
|
||||||
m := []string{
|
// If not logging verbosely, the ongoing progress of the build will not
|
||||||
"Still building",
|
// be streaming to stdout, and the lack of activity has been seen to cause
|
||||||
"Don't give up on me",
|
// users to prematurely exit due to the sluggishness of pulling large images
|
||||||
"This is taking a while",
|
if !c.verbose {
|
||||||
"Still building"}
|
c.printBuildActivity(ctx) // print friendly messages until context is canceled
|
||||||
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()
|
|
||||||
}()
|
|
||||||
|
|
||||||
f, err := NewFunction(path)
|
f, err := NewFunction(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -606,6 +632,33 @@ func (c *Client) Build(ctx context.Context, path string) (err error) {
|
||||||
return
|
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
|
// Deploy the Function at path. Errors if the Function has not been
|
||||||
// initialized with an image tag.
|
// initialized with an image tag.
|
||||||
func (c *Client) Deploy(ctx context.Context, path string) (err error) {
|
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.
|
// 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() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
c.progressListener.Stopping()
|
c.progressListener.Stopping()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Create an instance of a Function representation at the given root.
|
// Load the Function
|
||||||
f, err := NewFunction(root)
|
f, err := NewFunction(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !f.Initialized() {
|
if !f.Initialized() {
|
||||||
// TODO: this needs a test.
|
// 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.
|
// Run the Function, which returns a Job for use interacting (at arms length)
|
||||||
return c.runner.Run(ctx, f)
|
// 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.
|
// Return to the caller the effective port, a function to call to trigger
|
||||||
func (c *Client) List(ctx context.Context) ([]ListItem, error) {
|
// stop, and a channel on which can be received runtime errors.
|
||||||
// delegate to concrete implementation of lister entirely.
|
return job, nil
|
||||||
return c.lister.List(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info for a Function. Name takes precidence. If no name is provided,
|
// Info for a Function. Name takes precidence. If no name is provided,
|
||||||
// the Function defined at root is used.
|
// 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() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
c.progressListener.Stopping()
|
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)
|
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,
|
// Remove a Function. Name takes precidence. If no name is provided,
|
||||||
// the Function defined at root is used if it exists.
|
// the Function defined at root is used if it exists.
|
||||||
func (c *Client) Remove(ctx context.Context, cfg Function) error {
|
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)
|
return c.remover.Remove(ctx, f.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit a CloudEvent to a function endpoint
|
// Invoke is a convenience method for triggering the execution of a Function
|
||||||
func (c *Client) Emit(ctx context.Context, endpoint string) error {
|
// 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() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
c.progressListener.Stopping()
|
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
|
// 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
|
// Runner
|
||||||
type noopRunner struct{ output io.Writer }
|
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
|
// Remover
|
||||||
type noopRemover struct{ output io.Writer }
|
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 }
|
func (n *noopLister) List(context.Context) ([]ListItem, error) { return []ListItem{}, nil }
|
||||||
|
|
||||||
// Emitter
|
// Describer
|
||||||
type noopEmitter struct{}
|
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
|
// PipelinesProvider
|
||||||
type noopPipelinesProvider struct{}
|
type noopPipelinesProvider struct{}
|
||||||
|
|
443
client_test.go
443
client_test.go
|
@ -4,16 +4,20 @@
|
||||||
package function_test
|
package function_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
cloudevents "github.com/cloudevents/sdk-go/v2"
|
||||||
fn "knative.dev/kn-plugin-func"
|
fn "knative.dev/kn-plugin-func"
|
||||||
"knative.dev/kn-plugin-func/mock"
|
"knative.dev/kn-plugin-func/mock"
|
||||||
. "knative.dev/kn-plugin-func/testing"
|
. "knative.dev/kn-plugin-func/testing"
|
||||||
|
@ -30,11 +34,11 @@ const (
|
||||||
TestRuntime = "go"
|
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
|
// New is the superset of creating a new fully deployed Function, and
|
||||||
// thus implicitly tests Create, Build and Deploy, which are exposed
|
// thus implicitly tests Create, Build and Deploy, which are exposed
|
||||||
// by the client API for those who prefer manual transmissions.
|
// 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"
|
root := "testdata/example.com/testNew"
|
||||||
defer Using(t, root)()
|
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
|
// 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
|
// on-disk, and also confirms that the XDG_CONFIG_HOME environment variable is
|
||||||
// respected when calculating this home path.
|
// respected when calculating this home path.
|
||||||
func TestInstantiationCreatesRepositoriesPath(t *testing.T) {
|
func TestClient_InstantiationCreatesRepositoriesPath(t *testing.T) {
|
||||||
root := "testdata/example.com/testNewCreatesRepositoriesPath"
|
root := "testdata/example.com/testNewCreatesRepositoriesPath"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
|
@ -74,8 +78,8 @@ func TestInstantiationCreatesRepositoriesPath(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRuntimeRequired ensures that the the runtime is an expected value.
|
// TestClient_New_RuntimeRequired ensures that the the runtime is an expected value.
|
||||||
func TestRuntimeRequired(t *testing.T) {
|
func TestClient_New_RuntimeRequired(t *testing.T) {
|
||||||
// Create a root for the new Function
|
// Create a root for the new Function
|
||||||
root := "testdata/example.com/testRuntimeRequired"
|
root := "testdata/example.com/testRuntimeRequired"
|
||||||
defer Using(t, root)()
|
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.
|
// 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"
|
root := "testdata/example.com/testNameDefaults"
|
||||||
defer Using(t, root)()
|
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.
|
// are written on new.
|
||||||
func TestWritesTemplate(t *testing.T) {
|
func TestClient_New_WritesTemplate(t *testing.T) {
|
||||||
root := "testdata/example.com/testWritesTemplate"
|
root := "testdata/example.com/testWritesTemplate"
|
||||||
defer Using(t, root)()
|
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.
|
// Function does not reinitialize.
|
||||||
func TestExtantAborts(t *testing.T) {
|
func TestClient_New_ExtantAborts(t *testing.T) {
|
||||||
root := "testdata/example.com/testExtantAborts"
|
root := "testdata/example.com/testExtantAborts"
|
||||||
defer Using(t, root)()
|
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.
|
// (visible) files aborts.
|
||||||
func TestNonemptyAborts(t *testing.T) {
|
func TestClient_New_NonemptyAborts(t *testing.T) {
|
||||||
root := "testdata/example.com/testNonemptyAborts"
|
root := "testdata/example.com/testNonemptyAborts"
|
||||||
defer Using(t, root)()
|
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
|
// only contains hidden files does not error, protecting against the naieve
|
||||||
// implementation of aborting initialization if any files exist, which would
|
// implementation of aborting initialization if any files exist, which would
|
||||||
// break functions tracked in source control (.git), or when used in
|
// break functions tracked in source control (.git), or when used in
|
||||||
// conjunction with other tools (.envrc, etc)
|
// conjunction with other tools (.envrc, etc)
|
||||||
func TestHiddenFilesIgnored(t *testing.T) {
|
func TestClient_New_HiddenFilesIgnored(t *testing.T) {
|
||||||
// Create a directory for the Function
|
// Create a directory for the Function
|
||||||
root := "testdata/example.com/testHiddenFilesIgnored"
|
root := "testdata/example.com/testHiddenFilesIgnored"
|
||||||
defer Using(t, root)()
|
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
|
// using a custom path to template repositories on disk. The custom repositories
|
||||||
// location is not defined herein but expected to be provided because, for
|
// 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
|
// 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
|
// $FUNC_REPOSITORIES/boson/go/json
|
||||||
// See the CLI for full details, but a standard default location is
|
// See the CLI for full details, but a standard default location is
|
||||||
// $HOME/.config/func/repositories/boson/go/json
|
// $HOME/.config/func/repositories/boson/go/json
|
||||||
func TestRepositoriesExtensible(t *testing.T) {
|
func TestClient_New_RepositoriesExtensible(t *testing.T) {
|
||||||
root := "testdata/example.com/testRepositoriesExtensible"
|
root := "testdata/example.com/testRepositoriesExtensible"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
|
@ -234,8 +238,9 @@ func TestRepositoriesExtensible(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRuntimeNotFound generates an error (embedded default repository).
|
// TestRuntime_New_RuntimeNotFoundError generates an error when the provided
|
||||||
func TestRuntimeNotFound(t *testing.T) {
|
// runtime is not fo0und (embedded default repository).
|
||||||
|
func TestClient_New_RuntimeNotFoundError(t *testing.T) {
|
||||||
root := "testdata/example.com/testRuntimeNotFound"
|
root := "testdata/example.com/testRuntimeNotFound"
|
||||||
defer Using(t, root)()
|
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
|
// 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"
|
root := "testdata/example.com/testRuntimeNotFoundCustom"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
|
@ -271,8 +276,8 @@ func TestRuntimeNotFoundCustom(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTemplateNotFound generates an error (embedded default repository).
|
// TestClient_New_TemplateNotFoundError generates an error (embedded default repository).
|
||||||
func TestTemplateNotFound(t *testing.T) {
|
func TestClient_New_TemplateNotFoundError(t *testing.T) {
|
||||||
root := "testdata/example.com/testTemplateNotFound"
|
root := "testdata/example.com/testTemplateNotFound"
|
||||||
defer Using(t, root)()
|
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.
|
// 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"
|
root := "testdata/example.com/testTemplateNotFoundCustom"
|
||||||
defer Using(t, root)()
|
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.
|
// 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
|
// Explicit name to use
|
||||||
name := "service.example.com"
|
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.
|
// prepended with the DefaultRegistry if a single token.
|
||||||
// Registry is the namespace at the container image registry.
|
// Registry is the namespace at the container image registry.
|
||||||
// If not prepended with the registry, it will be defaulted:
|
// 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
|
// 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,
|
// registry in all cases. When we support in-cluster container registries,
|
||||||
// this configuration parameter will become optional.
|
// this configuration parameter will become optional.
|
||||||
func TestRegistryRequired(t *testing.T) {
|
func TestClient_New_RegistryRequired(t *testing.T) {
|
||||||
// Create a root for the Function
|
// Create a root for the Function
|
||||||
root := "testdata/example.com/testRegistryRequired"
|
root := "testdata/example.com/testRegistryRequired"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
@ -358,10 +363,10 @@ func TestRegistryRequired(t *testing.T) {
|
||||||
fmt.Println(err)
|
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
|
// container is populated based of a derivation using configured registry
|
||||||
// plus the service name.
|
// plus the service name.
|
||||||
func TestDeriveImage(t *testing.T) {
|
func TestClient_New_ImageNameDerived(t *testing.T) {
|
||||||
// Create the root Function directory
|
// Create the root Function directory
|
||||||
root := "testdata/example.com/testDeriveImage"
|
root := "testdata/example.com/testDeriveImage"
|
||||||
defer Using(t, root)()
|
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.
|
// a registry prefix has the DefaultRegistry prepended.
|
||||||
// For example "alice" becomes "docker.io/alice"
|
// For example "alice" becomes "docker.io/alice"
|
||||||
func TestDeriveImageDefaultRegistry(t *testing.T) {
|
func TestClient_New_ImageRegistryDefaults(t *testing.T) {
|
||||||
// Create the root Function directory
|
// Create the root Function directory
|
||||||
root := "testdata/example.com/testDeriveImageDefaultRegistry"
|
root := "testdata/example.com/testDeriveImageDefaultRegistry"
|
||||||
defer Using(t, root)()
|
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
|
// subcomponents via delegation through Build, Push and
|
||||||
// Deploy (and confirms expected fields calculated).
|
// Deploy (and confirms expected fields calculated).
|
||||||
func TestNewDelegates(t *testing.T) {
|
func TestClient_New_Delegation(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
root = "testdata/example.com/testNewDelegates" // .. in which to initialize
|
root = "testdata/example.com/testNewDelegates" // .. in which to initialize
|
||||||
expectedName = "testNewDelegates" // expected to be derived
|
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.
|
// TestClient_Run ensures that the runner is invoked with the absolute path requested.
|
||||||
func TestRun(t *testing.T) {
|
// Implicitly checks that the stop fn returned also is respected.
|
||||||
|
func TestClient_Run(t *testing.T) {
|
||||||
// Create the root Function directory
|
// Create the root Function directory
|
||||||
root := "testdata/example.com/testRun"
|
root := "testdata/example.com/testRun"
|
||||||
defer Using(t, root)()
|
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()
|
runner := mock.NewRunner()
|
||||||
client := fn.New(fn.WithRegistry(TestRegistry), fn.WithRunner(runner))
|
client := fn.New(fn.WithRegistry(TestRegistry), fn.WithRunner(runner))
|
||||||
if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil {
|
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
|
// 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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer job.Stop()
|
||||||
|
|
||||||
// Assert the runner was invoked, and with the expected root.
|
// Assert the runner was invoked, and with the expected root.
|
||||||
if !runner.RunInvoked {
|
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.
|
// process, erroring if run on a directory uncreated.
|
||||||
func TestUpdate(t *testing.T) {
|
func TestClient_Update(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
root = "testdata/example.com/testUpdate"
|
root = "testdata/example.com/testUpdate"
|
||||||
expectedName = "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.
|
// 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 (
|
var (
|
||||||
root = "testdata/example.com/testRemoveByPath"
|
root = "testdata/example.com/testRemoveByPath"
|
||||||
expectedName = "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.
|
// of the name provided, with precidence over a provided root path.
|
||||||
func TestRemoveByName(t *testing.T) {
|
func TestClient_Remove_ByName(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
root = "testdata/example.com/testRemoveByName"
|
root = "testdata/example.com/testRemoveByName"
|
||||||
expectedName = "explicitName.example.com"
|
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.
|
// 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
|
// the name will not be derived from path and the Function removed by this
|
||||||
// derived name; which could be unexpected and destructive.
|
// derived name; which could be unexpected and destructive.
|
||||||
func TestRemoveUninitializedFails(t *testing.T) {
|
func TestClient_Remove_UninitializedFails(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
root = "testdata/example.com/testRemoveUninitializedFails"
|
root = "testdata/example.com/testRemoveUninitializedFails"
|
||||||
remover = mock.NewRemover()
|
remover = mock.NewRemover()
|
||||||
|
@ -695,8 +751,8 @@ func TestRemoveUninitializedFails(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestList merely ensures that the client invokes the configured lister.
|
// TestClient_List merely ensures that the client invokes the configured lister.
|
||||||
func TestList(t *testing.T) {
|
func TestClient_List(t *testing.T) {
|
||||||
lister := mock.NewLister()
|
lister := mock.NewLister()
|
||||||
|
|
||||||
client := fn.New(fn.WithLister(lister)) // lists deployed Functions.
|
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,
|
// that is not contextually dependent on being associated with a Function,
|
||||||
// can be run from anywhere, thus ensuring that the client itself makes
|
// can be run from anywhere, thus ensuring that the client itself makes
|
||||||
// a distinction between Function-scoped methods and not.
|
// a distinction between Function-scoped methods and not.
|
||||||
func TestListOutsideRoot(t *testing.T) {
|
func TestClient_List_OutsideRoot(t *testing.T) {
|
||||||
lister := mock.NewLister()
|
lister := mock.NewLister()
|
||||||
|
|
||||||
// Instantiate in the current working directory, with no name.
|
// 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)
|
// fully created (ie. was only initialized, not actually built and deploys)
|
||||||
// yields an expected, and informative, error.
|
// 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
|
root := "testdata/example.com/testDeployUnbuilt" // Root from which to run the test
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
|
@ -755,34 +811,10 @@ func TestDeployUnbuilt(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEmit ensures that the client properly invokes the emitter when provided
|
// TestClient_New_BuildersPersisted Asserts that the client preserves user-
|
||||||
func TestEmit(t *testing.T) {
|
// provided Builders on the Function configuration with the internal default
|
||||||
sink := "http://testy.mctestface.com"
|
// if not provided.
|
||||||
emitter := mock.NewEmitter()
|
func TestClient_New_BuildersPersisted(t *testing.T) {
|
||||||
|
|
||||||
// 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) {
|
|
||||||
root := "testdata/example.com/testConfiguredBuilders" // Root from which to run the test
|
root := "testdata/example.com/testConfiguredBuilders" // Root from which to run the test
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||||
|
@ -796,7 +828,7 @@ func TestWithConfiguredBuilders(t *testing.T) {
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Create the Function, which should preserve custom builders
|
// 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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -817,10 +849,10 @@ func TestWithConfiguredBuilders(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asserts that the client properly sets user provided Builders
|
// TestClient_New_BuilderDefault ensures that if a custom builder is
|
||||||
// in the Function configuration, and if one of the provided is
|
// provided of name "default", this is chosen as the default builder instead
|
||||||
// keyed as "default", this is set as the default Builder.
|
// of the inbuilt static default.
|
||||||
func TestWithConfiguredBuildersWithDefault(t *testing.T) {
|
func TestClient_New_BuilderDefault(t *testing.T) {
|
||||||
root := "testdata/example.com/testConfiguredBuildersWithDefault" // Root from which to run the test
|
root := "testdata/example.com/testConfiguredBuildersWithDefault" // Root from which to run the test
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
|
@ -829,7 +861,7 @@ func TestWithConfiguredBuildersWithDefault(t *testing.T) {
|
||||||
"default": "docker.io/example/default",
|
"default": "docker.io/example/default",
|
||||||
}
|
}
|
||||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||||
if err := client.Create(fn.Function{
|
if err := client.New(context.Background(), fn.Function{
|
||||||
Runtime: TestRuntime,
|
Runtime: TestRuntime,
|
||||||
Root: root,
|
Root: root,
|
||||||
Builders: builders,
|
Builders: builders,
|
||||||
|
@ -852,9 +884,9 @@ func TestWithConfiguredBuildersWithDefault(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asserts that the client properly sets the Buildpacks property
|
// TestClient_New_BuildpacksPersisted ensures that provided buildpacks are
|
||||||
// in the Function configuration when it is provided.
|
// persisted on new Functions.
|
||||||
func TestWithConfiguredBuildpacks(t *testing.T) {
|
func TestClient_New_BuildpacksPersisted(t *testing.T) {
|
||||||
root := "testdata/example.com/testConfiguredBuildpacks" // Root from which to run the test
|
root := "testdata/example.com/testConfiguredBuildpacks" // Root from which to run the test
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
|
@ -862,7 +894,7 @@ func TestWithConfiguredBuildpacks(t *testing.T) {
|
||||||
"docker.io/example/custom-buildpack",
|
"docker.io/example/custom-buildpack",
|
||||||
}
|
}
|
||||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||||
if err := client.Create(fn.Function{
|
if err := client.New(context.Background(), fn.Function{
|
||||||
Runtime: TestRuntime,
|
Runtime: TestRuntime,
|
||||||
Root: root,
|
Root: root,
|
||||||
Buildpacks: buildpacks,
|
Buildpacks: buildpacks,
|
||||||
|
@ -880,8 +912,8 @@ func TestWithConfiguredBuildpacks(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRuntimes ensures that the total set of runtimes are returned.
|
// TestClient_Runtimes ensures that the total set of runtimes are returned.
|
||||||
func TestRuntimes(t *testing.T) {
|
func TestClient_Runtimes(t *testing.T) {
|
||||||
// TODO: test when a specific repo override is indicated
|
// TODO: test when a specific repo override is indicated
|
||||||
// (remote repo which takes precidence over embedded and extended)
|
// (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.
|
// 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"
|
root := "testdata/example.com/testCreateStamp"
|
||||||
defer Using(t, root)()
|
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)
|
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"
|
"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.
|
// registry explicitly as an argument invokes the registry validation code.
|
||||||
func TestBuildInvalidRegistry(t *testing.T) {
|
func TestBuild_InvalidRegistry(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
args = []string{"--registry", "foo/bar/foobar/boofar"} // provide an invalid registry name
|
args = []string{"--registry", "foo/bar/foobar/boofar"} // provide an invalid registry name
|
||||||
builder = mock.NewBuilder() // with a mock builder
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
pushFlag bool
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cfg buildConfig
|
cfg buildConfig
|
||||||
|
|
|
@ -56,7 +56,7 @@ NAME
|
||||||
{{.Prefix}}func create - Create a Function project.
|
{{.Prefix}}func create - Create a Function project.
|
||||||
|
|
||||||
SYNOPSIS
|
SYNOPSIS
|
||||||
func create [-l|--language] [-t|--template] [-r|--repository]
|
{{.Prefix}}func create [-l|--language] [-t|--template] [-r|--repository]
|
||||||
[-c|--confirm] [-v|--verbose] [path]
|
[-c|--confirm] [-v|--verbose] [path]
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
|
@ -292,6 +292,53 @@ func newCreateConfig(args []string, clientFn createClientFn) (cfg createConfig,
|
||||||
return
|
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.
|
// isValidRuntime determines if the given language runtime is a valid choice.
|
||||||
func isValidRuntime(client *fn.Client, runtime string) bool {
|
func isValidRuntime(client *fn.Client, runtime string) bool {
|
||||||
runtimes, err := client.Runtimes()
|
runtimes, err := client.Runtimes()
|
||||||
|
@ -373,53 +420,6 @@ func newInvalidTemplateError(client *fn.Client, runtime, template string) error
|
||||||
return ErrInvalidTemplate(errors.New(b.String()))
|
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
|
// prompt the user with value of config members, allowing for interactively
|
||||||
// mutating the values. The provided clientFn is used to construct a transient
|
// mutating the values. The provided clientFn is used to construct a transient
|
||||||
// client for use during prompt autocompletion/suggestions (such as suggesting
|
// client for use during prompt autocompletion/suggestions (such as suggesting
|
||||||
|
|
|
@ -12,9 +12,9 @@ import (
|
||||||
"knative.dev/kn-plugin-func/utils"
|
"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.
|
// and valid input completes without error; degenerate case.
|
||||||
func TestCreate(t *testing.T) {
|
func TestCreate_Execute(t *testing.T) {
|
||||||
defer fromTempDir(t)()
|
defer fromTempDir(t)()
|
||||||
|
|
||||||
// command with a client factory which yields a fully default client.
|
// 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.
|
// done with a runtime.
|
||||||
func TestCreateWithNoRuntime(t *testing.T) {
|
func TestCreate_NoRuntime(t *testing.T) {
|
||||||
defer fromTempDir(t)()
|
defer fromTempDir(t)()
|
||||||
|
|
||||||
// command with a client factory which yields a fully default client.
|
// 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.
|
// done with one of the valid runtimes only.
|
||||||
func TestCreateWithInvalidRuntime(t *testing.T) {
|
func TestCreate_WithInvalidRuntime(t *testing.T) {
|
||||||
defer fromTempDir(t)()
|
defer fromTempDir(t)()
|
||||||
|
|
||||||
// command with a client factory which yields a fully default client.
|
// 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.
|
// done with one of the valid templates only.
|
||||||
func TestCreateWithInvalidTemplate(t *testing.T) {
|
func TestCreate_InvalidTemplate(t *testing.T) {
|
||||||
defer fromTempDir(t)()
|
defer fromTempDir(t)()
|
||||||
|
|
||||||
// command with a client factory which yields a fully default client.
|
// 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.
|
// DNS-1123 labels for Function name.
|
||||||
func TestCreateValidatesName(t *testing.T) {
|
func TestCreate_ValidatesName(t *testing.T) {
|
||||||
defer fromTempDir(t)()
|
defer fromTempDir(t)()
|
||||||
|
|
||||||
// Create a new Create command with a fn.Client construtor
|
// 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
|
// expected repositories path, respecting the setting for XDG_CONFIG_PATH
|
||||||
// when deriving the default
|
// when deriving the default
|
||||||
func TestCreateRepositoriesPath(t *testing.T) {
|
func TestCreate_RepositoriesPath(t *testing.T) {
|
||||||
defer fromTempDir(t)()
|
defer fromTempDir(t)()
|
||||||
|
|
||||||
// Update XDG_CONFIG_HOME to point to some arbitrary location.
|
// Update XDG_CONFIG_HOME to point to some arbitrary location.
|
||||||
|
|
|
@ -8,9 +8,9 @@ import (
|
||||||
"knative.dev/kn-plugin-func/mock"
|
"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.
|
// Function explicitly as an argument invokes the remover appropriately.
|
||||||
func TestDeleteByName(t *testing.T) {
|
func TestDelete_ByName(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
testname = "testname" // explicit name for the Function
|
testname = "testname" // explicit name for the Function
|
||||||
args = []string{testname} // passed as the lone argument
|
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)
|
// 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
|
// from within a new temporary directory
|
||||||
defer fromTempDir(t)()
|
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.
|
// path generates an error.
|
||||||
// Providing the --path (-p) flag indicates the name of the function to delete
|
// 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
|
// 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.
|
// 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.
|
// A mock remover which will be sampled to ensure it is not invoked.
|
||||||
remover := mock.NewRemover()
|
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)
|
// Output Formatting (serializers)
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
|
|
||||||
type info fn.Info
|
type info fn.Instance
|
||||||
|
|
||||||
func (i info) Human(w io.Writer) error {
|
func (i info) Human(w io.Writer) error {
|
||||||
fmt.Fprintln(w, "Function name:")
|
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"
|
"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
|
// set of repositories by name, respects the repositories flag (provides it to
|
||||||
// the client), and prints the list as expected.
|
// the client), and prints the list as expected.
|
||||||
func TestRepositoryList(t *testing.T) {
|
func TestRepository_List(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
client = mock.NewClient()
|
client = mock.NewClient()
|
||||||
list = NewRepositoryListCmd(testRepositoryClientFn(client))
|
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
|
// arguments, respects the repositories path flag, and the expected name is echoed
|
||||||
// upon subsequent 'list'.
|
// upon subsequent 'list'.
|
||||||
func TestRepositoryAdd(t *testing.T) {
|
func TestRepository_Add(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
client = mock.NewClient()
|
client = mock.NewClient()
|
||||||
add = NewRepositoryAddCmd(testRepositoryClientFn(client))
|
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
|
// positional arguments, respects the repositories path flag, and the name is
|
||||||
// reflected as having been reanamed upon subsequent 'list'.
|
// reflected as having been reanamed upon subsequent 'list'.
|
||||||
func TestRepositoryRename(t *testing.T) {
|
func TestRepository_Rename(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
client = mock.NewClient()
|
client = mock.NewClient()
|
||||||
add = NewRepositoryAddCmd(testRepositoryClientFn(client))
|
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
|
// its argument, respects the repositorieis flag, and the entry is removed upon
|
||||||
// subsequent 'list'.
|
// subsequent 'list'.
|
||||||
func TestRepositoryRemove(t *testing.T) {
|
func TestRepository_Remove(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
client = mock.NewClient()
|
client = mock.NewClient()
|
||||||
add = NewRepositoryAddCmd(testRepositoryClientFn(client))
|
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.
|
// resultant binary with no arguments prints the help/usage text.
|
||||||
var root = &cobra.Command{
|
var root = &cobra.Command{
|
||||||
Use: "func",
|
Use: "func",
|
||||||
Short: "Serverless functions",
|
Short: "Serverless Functions",
|
||||||
SilenceErrors: true, // we explicitly handle errors in Execute()
|
SilenceErrors: true, // we explicitly handle errors in Execute()
|
||||||
SilenceUsage: true, // no usage dump on error
|
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() {
|
func init() {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
fn "knative.dev/kn-plugin-func"
|
fn "knative.dev/kn-plugin-func"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_mergeEnvMaps(t *testing.T) {
|
func TestRoot_mergeEnvMaps(t *testing.T) {
|
||||||
|
|
||||||
a := "A"
|
a := "A"
|
||||||
b := "B"
|
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" {
|
if root.Use != "func" {
|
||||||
t.Fatalf("default command use should be \"func\".")
|
t.Fatalf("default command use should be \"func\".")
|
||||||
|
|
24
cmd/run.go
24
cmd/run.go
|
@ -1,6 +1,8 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ory/viper"
|
"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)
|
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)
|
client := clientFn(config)
|
||||||
|
|
||||||
|
// Build if not built and --build
|
||||||
if config.Build && !function.Built() {
|
if config.Build && !function.Built() {
|
||||||
if err = client.Build(cmd.Context(), config.Path); err != nil {
|
if err = client.Build(cmd.Context(), config.Path); err != nil {
|
||||||
return
|
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 {
|
type runConfig struct {
|
||||||
|
|
178
cmd/run_test.go
178
cmd/run_test.go
|
@ -1,6 +1,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -10,104 +11,133 @@ import (
|
||||||
"knative.dev/kn-plugin-func/mock"
|
"knative.dev/kn-plugin-func/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRunRun(t *testing.T) {
|
func TestRun_Run(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string // name of the test
|
||||||
fileContents string
|
desc string // description of the test
|
||||||
buildErrors bool
|
funcState string // Function state, as described in func.yaml
|
||||||
buildFlag bool
|
buildFlag bool // value to which the --build flag should be set
|
||||||
shouldBuild bool
|
buildError error // Set the builder to yield this error
|
||||||
shouldRun bool
|
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",
|
name: "run when not building",
|
||||||
fileContents: `name: test-func
|
desc: "Should run when build is not enabled",
|
||||||
|
funcState: `name: test-func
|
||||||
runtime: go
|
runtime: go
|
||||||
created: 2009-11-10 23:00:00`,
|
created: 2009-11-10 23:00:00`,
|
||||||
buildFlag: false,
|
buildFlag: false,
|
||||||
shouldBuild: false,
|
buildInvoked: false,
|
||||||
shouldRun: true,
|
runInvoked: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Prebuilt image doesn't get built again",
|
name: "run and build",
|
||||||
fileContents: `name: test-func
|
desc: "Should run and build when build is enabled and there is no image",
|
||||||
|
funcState: `name: test-func
|
||||||
runtime: go
|
runtime: go
|
||||||
image: unexistant
|
|
||||||
created: 2009-11-10 23:00:00`,
|
created: 2009-11-10 23:00:00`,
|
||||||
buildFlag: true,
|
buildFlag: true,
|
||||||
shouldBuild: false,
|
buildInvoked: true,
|
||||||
shouldRun: true,
|
runInvoked: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Build when image is empty and build flag is true",
|
name: "skip rebuild",
|
||||||
fileContents: `name: test-func
|
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
|
runtime: go
|
||||||
|
image: exampleimage
|
||||||
created: 2009-11-10 23:00:00`,
|
created: 2009-11-10 23:00:00`,
|
||||||
buildFlag: true,
|
buildFlag: true,
|
||||||
shouldBuild: true,
|
buildInvoked: false,
|
||||||
shouldRun: true,
|
runInvoked: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Build error skips execution",
|
name: "Build errors return",
|
||||||
fileContents: `name: test-func
|
desc: "Errors building cause an immediate return with error",
|
||||||
|
funcState: `name: test-func
|
||||||
runtime: go
|
runtime: go
|
||||||
created: 2009-11-10 23:00:00`,
|
created: 2009-11-10 23:00:00`,
|
||||||
buildFlag: true,
|
buildFlag: true,
|
||||||
shouldBuild: true,
|
buildError: fmt.Errorf("generic build error"),
|
||||||
shouldRun: false,
|
buildInvoked: true,
|
||||||
buildErrors: true,
|
runInvoked: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
mockRunner := mock.NewRunner()
|
// run as a sub-test
|
||||||
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)
|
|
||||||
}
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err := cmd.Execute()
|
defer fromTempDir(t)()
|
||||||
if err == nil && tt.buildErrors {
|
|
||||||
t.Errorf("Expected error: %v but got %v", tt.buildErrors, err)
|
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"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestConfigPathDefault ensures that config defaults to XDG_CONFIG_HOME/func
|
// TestConfig_PathDefault ensures that config defaults to XDG_CONFIG_HOME/func
|
||||||
func TestConfigPathDefault(t *testing.T) {
|
func TestConfig_PathDefault(t *testing.T) {
|
||||||
// TODO
|
// TODO
|
||||||
// Set XDG_CONFIG_PATH to ./testdata/config
|
// Set XDG_CONFIG_PATH to ./testdata/config
|
||||||
// Confirm the config is populated from the test files.
|
// 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
|
// TestConfigPath ensure that the config path provided via the WithConfig
|
||||||
// option is respected.
|
// option is respected.
|
||||||
func TestConfigPath(t *testing.T) {
|
func TestConfig_Path(t *testing.T) {
|
||||||
// TODO
|
// TODO
|
||||||
// Create a client specifying ./testdata/config
|
// Create a client specifying ./testdata/config
|
||||||
// Confirm the config is populated from the test files.
|
// 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
|
// TestConfigRepositoriesPath ensures that the repositories directory within
|
||||||
// the effective config path is created if it does not already exist.
|
// the effective config path is created if it does not already exist.
|
||||||
func TestConfigRepositoriesPath(t *testing.T) {
|
func TestConfig_RepositoriesPath(t *testing.T) {
|
||||||
// TODO
|
// TODO
|
||||||
// Create a temporary directory
|
// Create a temporary directory
|
||||||
// Specify this directory as the config path when instantiating a client.
|
// 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()) {
|
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")
|
listener, err := net.Listen("tcp", "localhost:8080")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
316
docker/runner.go
316
docker/runner.go
|
@ -3,15 +3,15 @@ package docker
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -19,9 +19,24 @@ import (
|
||||||
fn "knative.dev/kn-plugin-func"
|
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 {
|
type Runner struct {
|
||||||
// Verbose logging flag.
|
// Verbose logging
|
||||||
Verbose bool
|
Verbose bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,122 +45,201 @@ func NewRunner() *Runner {
|
||||||
return &Runner{}
|
return &Runner{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the function at path
|
// Run the Function.
|
||||||
func (n *Runner) Run(ctx context.Context, f fn.Function) error {
|
func (n *Runner) Run(ctx context.Context, f fn.Function) (job *fn.Job, err error) {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
cli, _, err := NewClient(client.DefaultDockerHost)
|
var (
|
||||||
if err != nil {
|
port = choosePort(DefaultHost, DefaultPort, DefaultDialTimeout)
|
||||||
return errors.Wrap(err, "failed to create docker api client")
|
c client.CommonAPIClient // Docker client
|
||||||
}
|
id string // ID of running container
|
||||||
defer cli.Close()
|
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 == "" {
|
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{}
|
envs := []string{}
|
||||||
for _, env := range f.Envs {
|
for _, env := range f.Envs {
|
||||||
if env.Name != nil && env.Value != nil {
|
if env.Name != nil && env.Value != nil {
|
||||||
value, set, err := processEnvValue(*env.Value)
|
value, set, err := processEnvValue(*env.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return envs, err
|
||||||
}
|
}
|
||||||
if set {
|
if set {
|
||||||
envs = append(envs, *env.Name+"="+value)
|
envs = append(envs, *env.Name+"="+value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if n.Verbose {
|
if verbose {
|
||||||
envs = append(envs, "VERBOSE=true")
|
envs = append(envs, "VERBOSE=true")
|
||||||
}
|
}
|
||||||
|
return envs, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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*}}$`)
|
var evRegex = regexp.MustCompile(`^{{\s*(\w+)\s*:(\w+)\s*}}$`)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -172,3 +266,25 @@ func processEnvValue(val string) (string, bool, error) {
|
||||||
}
|
}
|
||||||
return val, true, nil
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 := docker.NewRunner()
|
||||||
runner.Verbose = true
|
runner.Verbose = true
|
||||||
if err = runner.Run(context.Background(), f); err != nil {
|
if _, err = runner.Run(context.Background(), f); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
/* TODO
|
/* TODO
|
||||||
|
@ -50,9 +50,11 @@ func TestDockerRun(t *testing.T) {
|
||||||
func TestDockerRunImagelessError(t *testing.T) {
|
func TestDockerRunImagelessError(t *testing.T) {
|
||||||
runner := docker.NewRunner()
|
runner := docker.NewRunner()
|
||||||
f := fn.NewFunctionWith(fn.Function{})
|
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 {
|
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]
|
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,
|
Invokes a running function. By default, a locally running instance will be preferred
|
||||||
and may provide event data on the command line or in a file on disk. By default, `event` works on the local
|
over a remote if both are running. The user may specify the event type, source,
|
||||||
directory, assuming that it is a function project. Alternatively the user may provide a path to a project
|
ID, and may provide event data on the command line or in a file on disk.
|
||||||
directory using the `--path` flag, or send an event to an arbitrary endpoint using the `--sink` flag. The
|
`invoke` works on the local directory, assuming that it is a function project.
|
||||||
`--sink` flag also accepts the special value `local` to send an event to the function running locally, for
|
Alternatively the user may provide a path to a project directory using the
|
||||||
example, when run via `func run`.
|
`--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]`
|
Similar `kn` command when using the [kn-plugin-event](https://github.com/knative-sandbox/kn-plugin-event): `kn event send [FLAGS]`
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
```console
|
```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
|
# 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
|
# Send a message to the local function with the data found in ./test.json
|
||||||
kn func emit --file ./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"
|
# "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"
|
# Send a message to the deployed function with an event type of "my.event"
|
||||||
kn func emit --type my.event --sink local
|
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"
|
# Send a message to the local function found at /path/to/fn with an id of "fn.test"
|
||||||
kn func emit --path /path/to/fn -i fn.test
|
kn func invoke --path /path/to/fn --id fn.test
|
||||||
|
|
||||||
# Send a CloudEvent to an arbitrary endpoint
|
# 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`
|
## `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
|
// according to the client which is in charge of what constitutes being
|
||||||
// fully "Created" (aka initialized)
|
// fully "Created" (aka initialized)
|
||||||
Created time.Time
|
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.
|
// NewFunctionWith defaults as provided.
|
||||||
|
@ -354,6 +370,7 @@ func assertEmptyRoot(path string) (err error) {
|
||||||
// Function rooted in the given directory.
|
// Function rooted in the given directory.
|
||||||
var contentiousFiles = []string{
|
var contentiousFiles = []string{
|
||||||
FunctionFile,
|
FunctionFile,
|
||||||
|
".gitignore",
|
||||||
}
|
}
|
||||||
|
|
||||||
// contentiousFilesIn the given directory
|
// contentiousFilesIn the given directory
|
||||||
|
|
|
@ -12,9 +12,9 @@ import (
|
||||||
. "knative.dev/kn-plugin-func/testing"
|
. "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.
|
// without change.
|
||||||
func TestWriteIdempotency(t *testing.T) {
|
func TestFunction_WriteIdempotency(t *testing.T) {
|
||||||
root, rm := Mktemp(t)
|
root, rm := Mktemp(t)
|
||||||
defer rm()
|
defer rm()
|
||||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
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.
|
// 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
|
// Creating a new Function from a path will error if there is no Function at
|
||||||
// that path. Creating using the client initializes the default.
|
// 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
|
// A path at which there is no Function currently
|
||||||
root := "testdata/testFunctionNameDefault"
|
root := "testdata/testFunctionNameDefault"
|
||||||
defer Using(t, root)()
|
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
|
// 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:
|
// 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
|
// 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)
|
servingClient, err := NewServingClient(d.namespace)
|
||||||
if err != nil {
|
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())
|
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)
|
triggers, err := eventingClient.ListTriggers(ctx)
|
||||||
// IsNotFound -- Eventing is probably not installed on the cluster
|
// IsNotFound -- Eventing is probably not installed on the cluster
|
||||||
if err != nil && !errors.IsNotFound(err) {
|
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.Name = name
|
||||||
description.Namespace = d.namespace
|
description.Namespace = d.namespace
|
||||||
|
description.Route = primaryRouteURL
|
||||||
description.Routes = routeURLs
|
description.Routes = routeURLs
|
||||||
description.Subscriptions = subscriptions
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
fn "knative.dev/kn-plugin-func"
|
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 {
|
type Runner struct {
|
||||||
RunInvoked bool
|
RunInvoked bool
|
||||||
RootRequested string
|
RootRequested string
|
||||||
|
RunFn func(context.Context, fn.Function) (*fn.Job, error)
|
||||||
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRunner() *Runner {
|
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.RunInvoked = true
|
||||||
r.RootRequested = f.Root
|
r.RootRequested = f.Root
|
||||||
return nil
|
|
||||||
|
return r.RunFn(ctx, f)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import (
|
||||||
// requisite test.
|
// requisite test.
|
||||||
const RepositoriesTestRepo = "repository"
|
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.
|
// 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)
|
root, rm := Mktemp(t)
|
||||||
defer rm()
|
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.
|
// results in error.
|
||||||
func TestRepositoriesGetInvalid(t *testing.T) {
|
func TestRepositories_GetInvalid(t *testing.T) {
|
||||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||||
|
|
||||||
// invalid should error
|
// invalid should error
|
||||||
|
@ -50,8 +50,8 @@ func TestRepositoriesGetInvalid(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRepositoriesGet ensures a repository can be accessed by name.
|
// TestRepositories_Get ensures a repository can be accessed by name.
|
||||||
func TestRepositoriesGet(t *testing.T) {
|
func TestRepositories_Get(t *testing.T) {
|
||||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||||
|
|
||||||
// valid should not error
|
// 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.
|
// .All accessor. Tests both builtin and buitlin+extensible cases.
|
||||||
func TestRepositoriesAll(t *testing.T) {
|
func TestRepositories_All(t *testing.T) {
|
||||||
uri := TestRepoURI(RepositoriesTestRepo, t)
|
uri := TestRepoURI(RepositoriesTestRepo, t)
|
||||||
root, rm := Mktemp(t)
|
root, rm := Mktemp(t)
|
||||||
defer rm()
|
defer rm()
|
||||||
|
@ -104,8 +104,8 @@ func TestRepositoriesAll(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRepositoriesAdd checks basic adding of a repository by URI.
|
// TestRepositories_Add checks basic adding of a repository by URI.
|
||||||
func TestRepositoriesAdd(t *testing.T) {
|
func TestRepositories_Add(t *testing.T) {
|
||||||
uri := TestRepoURI(RepositoriesTestRepo, t) // ./testdata/$RepositoriesTestRepo.git
|
uri := TestRepoURI(RepositoriesTestRepo, t) // ./testdata/$RepositoriesTestRepo.git
|
||||||
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
||||||
defer rm()
|
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.
|
// 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
|
// 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
|
// repo meant to exemplify the simplest use case: a repo with no metadata
|
||||||
// that simply contains templates, grouped by runtime. It therefore does
|
// 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
|
// a manfest wherein a default name is specified, is used as the name for the
|
||||||
// added repository when a name is not explicitly specified.
|
// 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
|
// 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.
|
// defines a custom language pack and makes full use of the manifest.yaml.
|
||||||
// The manifest.yaml is included which specifies things like custom templates
|
// 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.
|
// already exists yields an error.
|
||||||
func TestRepositoriesAddExistingErrors(t *testing.T) {
|
func TestRepositories_AddExistingErrors(t *testing.T) {
|
||||||
uri := TestRepoURI(RepositoriesTestRepo, t)
|
uri := TestRepoURI(RepositoriesTestRepo, t)
|
||||||
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
||||||
defer rm()
|
defer rm()
|
||||||
|
@ -242,8 +242,8 @@ func TestRepositoriesAddExistingErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRepositoriesRename ensures renaming a repository succeeds.
|
// TestRepositories_Rename ensures renaming a repository succeeds.
|
||||||
func TestRepositoriesRename(t *testing.T) {
|
func TestRepositories_Rename(t *testing.T) {
|
||||||
uri := TestRepoURI(RepositoriesTestRepo, t)
|
uri := TestRepoURI(RepositoriesTestRepo, t)
|
||||||
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
root, rm := Mktemp(t) // create and cd to a temp dir, returning path.
|
||||||
defer rm()
|
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.
|
// 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
|
uri := TestRepoURI(RepositoriesTestRepo, t) // ./testdata/repository.git
|
||||||
root, rm := Mktemp(t) // create and cd to a temp dir
|
root, rm := Mktemp(t) // create and cd to a temp dir
|
||||||
defer rm()
|
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)
|
// 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
|
// 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
|
// 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.
|
// 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
|
// does not cause an error unless it was explicitly set (zero value indicates
|
||||||
// no repos should be loaded from os).
|
// no repos should be loaded from os).
|
||||||
// This may change in an upcoming release where the repositories directory
|
// 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
|
// 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.
|
// 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 with no repositories path defined.
|
||||||
client := fn.New()
|
client := fn.New()
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,10 @@ const (
|
||||||
DefaultLivenessEndpoint = "/health/liveness"
|
DefaultLivenessEndpoint = "/health/liveness"
|
||||||
// DefaultTemplatesPath is the root of the defined repository
|
// DefaultTemplatesPath is the root of the defined repository
|
||||||
DefaultTemplatesPath = "."
|
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
|
// Defaults for Builder and Builders not expressly defined as a purposeful
|
||||||
// delegation of choice.
|
// delegation of choice.
|
||||||
|
@ -66,6 +70,10 @@ type Repository struct {
|
||||||
// TODO upgrade to fs.FS introduced in go1.16
|
// TODO upgrade to fs.FS introduced in go1.16
|
||||||
FS Filesystem
|
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
|
uri string // URI which was used when initially creating
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -92,6 +100,10 @@ type Runtime struct {
|
||||||
// of Runtime.
|
// of Runtime.
|
||||||
BuildConfig `yaml:",inline"`
|
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 defined for the runtime
|
||||||
Templates []Template
|
Templates []Template
|
||||||
}
|
}
|
||||||
|
@ -121,6 +133,7 @@ func NewRepository(name, uri string) (r Repository, err error) {
|
||||||
Liveness: DefaultLivenessEndpoint,
|
Liveness: DefaultLivenessEndpoint,
|
||||||
Readiness: DefaultLivenessEndpoint,
|
Readiness: DefaultLivenessEndpoint,
|
||||||
},
|
},
|
||||||
|
Invocation: Invocation{Format: DefaultInvocationFormat},
|
||||||
}
|
}
|
||||||
r.FS, err = filesystemFromURI(uri) // Get a Filesystem from the URI
|
r.FS, err = filesystemFromURI(uri) // Get a Filesystem from the URI
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -240,6 +253,7 @@ func repositoryRuntimes(r Repository) (runtimes []Runtime, err error) {
|
||||||
BuildConfig: r.BuildConfig,
|
BuildConfig: r.BuildConfig,
|
||||||
HealthEndpoints: r.HealthEndpoints,
|
HealthEndpoints: r.HealthEndpoints,
|
||||||
BuildEnvs: r.BuildEnvs,
|
BuildEnvs: r.BuildEnvs,
|
||||||
|
Invocation: r.Invocation,
|
||||||
}
|
}
|
||||||
// Runtime Manifest
|
// Runtime Manifest
|
||||||
// Load the file if it exists, which may override values inherited from the
|
// 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,
|
BuildConfig: runtime.BuildConfig,
|
||||||
HealthEndpoints: runtime.HealthEndpoints,
|
HealthEndpoints: runtime.HealthEndpoints,
|
||||||
BuildEnvs: runtime.BuildEnvs,
|
BuildEnvs: runtime.BuildEnvs,
|
||||||
|
Invocation: runtime.Invocation,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Template Manifeset
|
// Template Manifeset
|
||||||
|
|
|
@ -11,9 +11,9 @@ import (
|
||||||
fn "knative.dev/kn-plugin-func"
|
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.
|
// 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"))
|
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||||
|
|
||||||
// The repo ./testdata/repositories/customLanguagePackRepo includes a
|
// 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
|
// properly inherit values defined at the repo level, runtime level
|
||||||
// and template level. The tests check for both embedded structures:
|
// and template level. The tests check for both embedded structures:
|
||||||
// HealthEndpoints BuildConfig.
|
// HealthEndpoints BuildConfig.
|
||||||
func TestRepositoryInheritance(t *testing.T) {
|
func TestRepository_Inheritance(t *testing.T) {
|
||||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||||
|
|
||||||
// The repo ./testdata/repositories/customLanguagePack includes a manifest
|
// The repo ./testdata/repositories/customLanguagePack includes a manifest
|
||||||
|
|
|
@ -138,6 +138,10 @@
|
||||||
"Created": {
|
"Created": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"invocation": {
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"$ref": "#/definitions/Invocation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
@ -170,6 +174,15 @@
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"Invocation": {
|
||||||
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"Label": {
|
"Label": {
|
||||||
"required": [
|
"required": [
|
||||||
"key"
|
"key"
|
||||||
|
|
|
@ -5,22 +5,31 @@ type Template struct {
|
||||||
// Name (short name) of this template within the repository.
|
// Name (short name) of this template within the repository.
|
||||||
// See .Fullname for the calculated field which is the unique primary id.
|
// See .Fullname for the calculated field which is the unique primary id.
|
||||||
Name string `yaml:"-"` // use filesystem for name, not yaml
|
Name string `yaml:"-"` // use filesystem for name, not yaml
|
||||||
|
|
||||||
// Runtime for which this template applies.
|
// Runtime for which this template applies.
|
||||||
Runtime string
|
Runtime string
|
||||||
|
|
||||||
// Repository within which this template is contained. Value is set to the
|
// Repository within which this template is contained. Value is set to the
|
||||||
// currently effective name of the repository, which may vary. It is user-
|
// 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
|
// 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.
|
// the client is loaded in single repo mode. I.e. not canonical.
|
||||||
Repository string
|
Repository string
|
||||||
|
|
||||||
// BuildConfig defines builders and buildpacks. the denormalized view of
|
// BuildConfig defines builders and buildpacks. the denormalized view of
|
||||||
// members which can be defined per repo or per runtime first.
|
// members which can be defined per repo or per runtime first.
|
||||||
BuildConfig `yaml:",inline"`
|
BuildConfig `yaml:",inline"`
|
||||||
|
|
||||||
// HealthEndpoints. The denormalized view of members which can be defined
|
// HealthEndpoints. The denormalized view of members which can be defined
|
||||||
// first per repo or per runtime.
|
// first per repo or per runtime.
|
||||||
HealthEndpoints `yaml:"healthEndpoints,omitempty"`
|
HealthEndpoints `yaml:"healthEndpoints,omitempty"`
|
||||||
|
|
||||||
// BuildEnvs defines environment variables related to the builders,
|
// BuildEnvs defines environment variables related to the builders,
|
||||||
// this can be used to parameterize the builders
|
// this can be used to parameterize the builders
|
||||||
BuildEnvs []Env `yaml:"buildEnvs,omitempty"`
|
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
|
// 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 == "" {
|
if f.HealthEndpoints.Readiness == "" {
|
||||||
f.HealthEndpoints.Readiness = template.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
|
// Copy the template files from the repo filesystem to the new Function's root
|
||||||
// removing the manifest (if it exists; errors ignored)
|
// 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:
|
builders:
|
||||||
default: quay.io/boson/faas-python-builder:v0.8.4
|
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
|
default: quay.io/boson/faas-jvm-builder:v0.8.4
|
||||||
jvm: 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
|
native: quay.io/boson/faas-quarkus-native-builder:v0.8.4
|
||||||
|
|
||||||
|
invocation:
|
||||||
|
format: "cloudevent"
|
||||||
|
|
|
@ -6,3 +6,6 @@ builders:
|
||||||
buildpacks:
|
buildpacks:
|
||||||
- paketo-buildpacks/nodejs
|
- paketo-buildpacks/nodejs
|
||||||
- ghcr.io/boson-project/typescript-function-buildpack:tip
|
- ghcr.io/boson-project/typescript-function-buildpack:tip
|
||||||
|
|
||||||
|
invocation:
|
||||||
|
format: "cloudevent"
|
||||||
|
|
|
@ -17,9 +17,9 @@ import (
|
||||||
. "knative.dev/kn-plugin-func/testing"
|
. "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.
|
// 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
|
// A client which specifies a location of exensible repositoreis on disk
|
||||||
// will list all builtin plus exensible
|
// will list all builtin plus exensible
|
||||||
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
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
|
// when retrieving the list of templates for a runtime that does not exist
|
||||||
// in an extended repository, but does in the default.
|
// 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"))
|
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")
|
templates, err := client.Templates().List("python")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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).
|
// 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"))
|
client := fn.New(fn.WithRepositories("testdata/repositories"))
|
||||||
|
|
||||||
// Check embedded
|
// Check embedded
|
||||||
|
@ -95,10 +96,10 @@ func TestTemplatesGet(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTemplateEmbedded ensures that embedded templates are copied on write.
|
// TestTemplates_Embedded ensures that embedded templates are copied on write.
|
||||||
func TestTemplateEmbedded(t *testing.T) {
|
func TestTemplates_Embedded(t *testing.T) {
|
||||||
// create test directory
|
// create test directory
|
||||||
root := "testdata/testTemplateEmbedded"
|
root := "testdata/testTemplatesEmbedded"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
// Client whose internal (builtin default) templates will be used.
|
// 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
|
// (ie. custom provider on disk) can be specified as the source for a
|
||||||
// template.
|
// template.
|
||||||
func TestTemplateCustom(t *testing.T) {
|
func TestTemplates_Custom(t *testing.T) {
|
||||||
// Create test directory
|
// Create test directory
|
||||||
root := "testdata/testTemplateCustom"
|
root := "testdata/testTemplatesCustom"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
// CLient which uses custom repositories
|
// 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
|
// can be specificed on creation of client, with subsequent calls to Create
|
||||||
// using this remote by default.
|
// using this remote by default.
|
||||||
func TestTemplateRemote(t *testing.T) {
|
func TestTemplates_Remote(t *testing.T) {
|
||||||
root := "testdata/testTemplateRemote"
|
root := "testdata/testTemplatesRemote"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
// The difference between HTTP vs File protocol is internal to the
|
// 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.
|
// is used when none specified.
|
||||||
func TestTemplateDefault(t *testing.T) {
|
func TestTemplates_Default(t *testing.T) {
|
||||||
// create test directory
|
// create test directory
|
||||||
root := "testdata/testTemplateDefault"
|
root := "testdata/testTemplates_Default"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
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
|
// runtime/template errors
|
||||||
func TestTemplateInvalidErrors(t *testing.T) {
|
func TestTemplates_InvalidErrors(t *testing.T) {
|
||||||
// create test directory
|
// create test directory
|
||||||
root := "testdata/testTemplateInvalidErrors"
|
root := "testdata/testTemplates_InvalidErrors"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
client := fn.New(fn.WithRegistry(TestRegistry))
|
||||||
|
@ -240,6 +241,7 @@ func TestTemplateInvalidErrors(t *testing.T) {
|
||||||
if !errors.Is(err, fn.ErrRuntimeNotFound) {
|
if !errors.Is(err, fn.ErrRuntimeNotFound) {
|
||||||
t.Fatalf("Expected ErrRuntimeNotFound, got %v", err)
|
t.Fatalf("Expected ErrRuntimeNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
|
os.Remove(filepath.Join(root, ".gitignore"))
|
||||||
|
|
||||||
// Test for error writing an invalid template
|
// Test for error writing an invalid template
|
||||||
err = client.Create(fn.Function{
|
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.
|
// templates retain their mode.
|
||||||
func TestTemplateModeEmbedded(t *testing.T) {
|
func TestTemplates_ModeEmbedded(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return
|
return
|
||||||
// not applicable
|
// not applicable
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up test directory
|
// set up test directory
|
||||||
root := "testdata/testTemplateModeEmbedded"
|
root := "testdata/testTemplatesModeEmbedded"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
client := fn.New(fn.WithRegistry(TestRegistry))
|
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.
|
// retain their mode.
|
||||||
func TestTemplateModeCustom(t *testing.T) {
|
func TestTemplates_ModeCustom(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return // not applicable
|
return // not applicable
|
||||||
}
|
}
|
||||||
|
|
||||||
// test directories
|
// test directories
|
||||||
root := "testdata/testTemplateModeCustom"
|
root := "testdata/testTemplates_ModeCustom"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
client := fn.New(
|
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.
|
// retain their mode.
|
||||||
func TestTemplateModeRemote(t *testing.T) {
|
func TestTemplates_ModeRemote(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return // not applicable
|
return // not applicable
|
||||||
}
|
}
|
||||||
|
|
||||||
// test directories
|
// test directories
|
||||||
root := "testdata/testTemplateModeRemote"
|
root := "testdata/testTemplates_ModeRemote"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
// Clone a repository from a local file path
|
// 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)
|
// 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.
|
// runtimes's manifest are included in the final Function.
|
||||||
func TestRuntimeManifestBuildEnvs(t *testing.T) {
|
func TestTemplates_RuntimeManifestBuildEnvs(t *testing.T) {
|
||||||
// create test directory
|
// create test directory
|
||||||
root := "testdata/testRuntimeManifestBuildEnvs"
|
root := "testdata/testTemplatesRuntimeManifestBuildEnvs"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
// Client whose internal templates will be used.
|
// 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.
|
// template's manifest are included in the final Function.
|
||||||
func TestTemplateManifestBuildEnvs(t *testing.T) {
|
func TestTemplates_ManifestBuildEnvs(t *testing.T) {
|
||||||
// create test directory
|
// create test directory
|
||||||
root := "testdata/testTemplateManifestBuildEnvs"
|
root := "testdata/testTemplatesManifestBuildEnvs"
|
||||||
defer Using(t, root)()
|
defer Using(t, root)()
|
||||||
|
|
||||||
// Client whose internal templates will be used.
|
// 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.
|
// repository's manifest are included in the final Function.
|
||||||
func TestRepositoryManifestBuildEnvs(t *testing.T) {
|
func TestTemplates_RepositoryManifestBuildEnvs(t *testing.T) {
|
||||||
// create test directory
|
// create test directory
|
||||||
root := "testdata/testRepositoryManifestBuildEnvs"
|
root := "testdata/testRepositoryManifestBuildEnvs"
|
||||||
defer Using(t, root)()
|
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.
|
// the resultant Function after write.
|
||||||
func TestTemplateManifestRemoved(t *testing.T) {
|
func TestTemplates_ManifestRemoved(t *testing.T) {
|
||||||
// create test directory
|
// create test directory
|
||||||
root := "testdata/testTemplateManifestRemoved"
|
root := "testdata/testTemplateManifestRemoved"
|
||||||
defer Using(t, root)()
|
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';
|
'use strict';
|
||||||
const { CloudEvent, HTTP } = require('cloudevents');
|
const { CloudEvent, HTTP } = require('cloudevents');
|
||||||
|
|
||||||
let lastEmitEventData = ""
|
let lastInvokeEventData = ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function used to test 'func emit' command
|
* Function used to test 'func invoke' command
|
||||||
* The trick here is sending the event using emit with a given event source 'func:emit'.
|
* 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
|
* For this source the consumed event will be stored and returned as http response when it received
|
||||||
* another event with source 'e2e:check'.
|
* another event with source 'e2e:check'.
|
||||||
*
|
*
|
||||||
* 1) function will consume and store the data "hello emit"
|
* 1) function will consume and store the data "hello invoke"
|
||||||
* kn func emit -c "text/plain" -d "hello emit" -s "func:emit"
|
* 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 \
|
* curl $node_func_url -X POST \
|
||||||
* -H "Ce-Id: some-message-id" \
|
* -H "Ce-Id: some-message-id" \
|
||||||
* -H "Ce-Specversion: 1.0" \
|
* -H "Ce-Specversion: 1.0" \
|
||||||
* -H "Ce-Type: e2e:check" \
|
* -H "Ce-Type: e2e:check" \
|
||||||
* -H "Ce-Source: e2e:check" \
|
* -H "Ce-Source: e2e:check" \
|
||||||
* -H "Content-Type: text/plain" \
|
* -H "Content-Type: text/plain" \
|
||||||
* -d 'Emit Check'
|
* -d 'Invoke Check'
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @param context
|
* @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()}`);
|
context.log.info(`CloudEvent received : ${cloudevent.toString()}`);
|
||||||
lastEmitEventData = cloudevent.data
|
lastInvokeEventData = cloudevent.data
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cloudevent.source == "e2e:check") {
|
if (cloudevent.source == "e2e:check") {
|
||||||
return HTTP.binary(new CloudEvent({
|
return HTTP.binary(new CloudEvent({
|
||||||
source: 'test:handle',
|
source: 'test:handle',
|
||||||
type: 'test:emit',
|
type: 'test:invoke',
|
||||||
data: lastEmitEventData
|
data: lastInvokeEventData
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,3 +9,13 @@ buildpacks:
|
||||||
buildEnvs:
|
buildEnvs:
|
||||||
- name: "TEST_TEMPLATE_VARIABLE"
|
- name: "TEST_TEMPLATE_VARIABLE"
|
||||||
value: "test-template"
|
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