mirror of https://github.com/knative/func.git
feat: test suite
- updated tests to new api throughout - expanded tests where appropriate - lint issues - minor code review comments addressed
This commit is contained in:
parent
6e0f4caa93
commit
d33fb2d694
|
@ -7,3 +7,5 @@
|
|||
|
||||
target
|
||||
node_modules
|
||||
/coverage.out
|
||||
/bin
|
||||
|
|
192
client.go
192
client.go
|
@ -18,8 +18,6 @@ const (
|
|||
// Client for managing Function instances.
|
||||
type Client struct {
|
||||
verbose bool // print verbose logs
|
||||
local bool // Run in local-only mode
|
||||
internal bool // Deploy without publicly accessible route
|
||||
builder Builder // Builds a runnable image from Function source
|
||||
pusher Pusher // Pushes the image assocaited with a Function.
|
||||
deployer Deployer // Deploys a Function
|
||||
|
@ -151,20 +149,6 @@ func WithVerbose(v bool) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithLocal sets the local mode
|
||||
func WithLocal(l bool) Option {
|
||||
return func(c *Client) {
|
||||
c.local = l
|
||||
}
|
||||
}
|
||||
|
||||
// WithInternal sets the internal (no public route) mode for deployed Function.
|
||||
func WithInternal(i bool) Option {
|
||||
return func(c *Client) {
|
||||
c.internal = i
|
||||
}
|
||||
}
|
||||
|
||||
// WithBuilder provides the concrete implementation of a builder.
|
||||
func WithBuilder(d Builder) Option {
|
||||
return func(c *Client) {
|
||||
|
@ -266,6 +250,60 @@ func WithRepository(repository string) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// Create a Function.
|
||||
// Includes Initialization, Building, and Deploying.
|
||||
func (c *Client) Create(cfg Function) (err error) {
|
||||
c.progressListener.SetTotal(4)
|
||||
defer c.progressListener.Done()
|
||||
|
||||
// Initialize, writing out a template implementation and a config file.
|
||||
// TODO: the Function's Initialize parameters are slightly different than
|
||||
// the Initializer interface, and can thus cause confusion (one passes an
|
||||
// optional name the other passes root path). This could easily cause
|
||||
// confusion and thus we may want to rename Initalizer to the more specific
|
||||
// task it performs: ContextTemplateWriter or similar.
|
||||
c.progressListener.Increment("Initializing new Function project")
|
||||
err = c.Initialize(cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Load the now-initialized Function.
|
||||
f, err := NewFunction(cfg.Root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build the now-initialized Function
|
||||
c.progressListener.Increment("Building container image")
|
||||
if err = c.Build(f.Root); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Deploy the initialized Function, returning its publicly
|
||||
// addressible name for possible registration.
|
||||
c.progressListener.Increment("Deploying Function to cluster")
|
||||
if err = c.Deploy(f.Root); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create an external route to the Function
|
||||
c.progressListener.Increment("Creating route to Function")
|
||||
if err = c.Route(f.Root); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.progressListener.Complete("Create complete")
|
||||
|
||||
// TODO: use the knative client during deployment such that the actual final
|
||||
// route can be returned from the deployment step, passed to the DNS Router
|
||||
// for routing actual traffic, and returned here.
|
||||
if c.verbose {
|
||||
fmt.Printf("https://%v/\n", f.Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize creates a new Function project locally using the settings
|
||||
// provided on a Function object.
|
||||
func (c *Client) Initialize(cfg Function) (err error) {
|
||||
|
@ -275,17 +313,8 @@ func (c *Client) Initialize(cfg Function) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Do not initialize if already initialized.
|
||||
if f.Initialized() {
|
||||
err = fmt.Errorf("Function at '%v' already initialized.", cfg.Root)
|
||||
return
|
||||
}
|
||||
|
||||
// Do not re-initialize unless the directory is empty This is to protect the
|
||||
// user from inadvertently overwriting local files which share the same name
|
||||
// as those in the applied template if, for instance, an incorrect path is
|
||||
// supplied for the new Function. This assertion is that the target path is
|
||||
// empty of all but unrelated hidden files.
|
||||
// Assert the specified root is free of visible files and contentious
|
||||
// hidden files (the ConfigFile, which indicates it is already initialized)
|
||||
if err = assertEmptyRoot(f.Root); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -304,8 +333,6 @@ func (c *Client) Initialize(cfg Function) (err error) {
|
|||
f.Runtime = cfg.Runtime
|
||||
if f.Runtime == "" {
|
||||
f.Runtime = DefaultRuntime
|
||||
} else {
|
||||
f.Runtime = cfg.Runtime
|
||||
}
|
||||
|
||||
// Assert trigger was provided, or default.
|
||||
|
@ -404,61 +431,6 @@ func (c *Client) Route(path string) (err error) {
|
|||
return c.dnsProvider.Provide(f)
|
||||
}
|
||||
|
||||
// Create a Function of the given runtime.
|
||||
// Name and Root are optional:
|
||||
// Name is derived from root if possible.
|
||||
// Root is defaulted to the current working directory.
|
||||
func (c *Client) Create(cfg Function) (err error) {
|
||||
c.progressListener.SetTotal(4)
|
||||
defer c.progressListener.Done()
|
||||
|
||||
// Initialize, writing out a template implementation and a config file.
|
||||
// TODO: the Function's Initialize parameters are slightly different than
|
||||
// the Initializer interface, and can thus cause confusion (one passes an
|
||||
// optional name the other passes root path). This could easily cause
|
||||
// confusion and thus we may want to rename Initalizer to the more specific
|
||||
// task it performs: ContextTemplateWriter or similar.
|
||||
c.progressListener.Increment("Initializing new Function project")
|
||||
err = c.Initialize(cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Load the now-initialized Function.
|
||||
f, err := NewFunction(cfg.Root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build the now-initialized Function
|
||||
c.progressListener.Increment("Building container image")
|
||||
err = c.Build(f.Root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Deploy the initialized Function, returning its publicly
|
||||
// addressible name for possible registration.
|
||||
c.progressListener.Increment("Deploying Function to cluster")
|
||||
if err = c.Deploy(f.Root); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create an external route to the Function
|
||||
c.progressListener.Increment("Creating route to Function")
|
||||
if err = c.Route(f.Root); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.progressListener.Complete("Create complete")
|
||||
|
||||
// TODO: use the knative client during deployment such that the actual final
|
||||
// route can be returned from the deployment step, passed to the DNS Router
|
||||
// for routing actual traffic, and returned here.
|
||||
fmt.Printf("https://%v/\n", f.Name)
|
||||
return
|
||||
}
|
||||
|
||||
// Update a previously created Function.
|
||||
func (c *Client) Update(root string) (err error) {
|
||||
|
||||
|
@ -539,20 +511,20 @@ func (c *Client) Describe(name, root string) (fd FunctionDescription, err error)
|
|||
}
|
||||
|
||||
// Remove a Function. Name takes precidence. If no name is provided,
|
||||
// the Function defined at root is used.
|
||||
func (c *Client) Remove(name, root string) error {
|
||||
// the Function defined at root is used if it exists.
|
||||
func (c *Client) Remove(cfg Function) error {
|
||||
// If name is provided, it takes precidence.
|
||||
// Otherwise load the Function deined at root.
|
||||
if name != "" {
|
||||
return c.remover.Remove(name)
|
||||
if cfg.Name != "" {
|
||||
return c.remover.Remove(cfg.Name)
|
||||
}
|
||||
|
||||
f, err := NewFunction(root)
|
||||
f, err := NewFunction(cfg.Root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !f.Initialized() {
|
||||
return fmt.Errorf("%v is not initialized", f.Name)
|
||||
return fmt.Errorf("Function at %v can not be removed unless initialized. Try removing by name.", f.Root)
|
||||
}
|
||||
return c.remover.Remove(f.Name)
|
||||
}
|
||||
|
@ -567,59 +539,35 @@ func (c *Client) Remove(name, root string) error {
|
|||
|
||||
type noopBuilder struct{ output io.Writer }
|
||||
|
||||
func (n *noopBuilder) Build(_ Function) error {
|
||||
fmt.Fprintln(n.output, "skipping build: client not initialized WithBuilder")
|
||||
return nil
|
||||
}
|
||||
func (n *noopBuilder) Build(_ Function) error { return nil }
|
||||
|
||||
type noopPusher struct{ output io.Writer }
|
||||
|
||||
func (n *noopPusher) Push(_ Function) error {
|
||||
fmt.Fprintln(n.output, "skipping push: client not initialized WithPusher")
|
||||
return nil
|
||||
}
|
||||
func (n *noopPusher) Push(_ Function) error { return nil }
|
||||
|
||||
type noopDeployer struct{ output io.Writer }
|
||||
|
||||
func (n *noopDeployer) Deploy(_ Function) error {
|
||||
fmt.Fprintln(n.output, "skipping deploy: client not initialized WithDeployer")
|
||||
return nil
|
||||
}
|
||||
func (n *noopDeployer) Deploy(_ Function) error { return nil }
|
||||
|
||||
type noopUpdater struct{ output io.Writer }
|
||||
|
||||
func (n *noopUpdater) Update(_ Function) error {
|
||||
fmt.Fprintln(n.output, "skipping deploy: client not initialized WithDeployer")
|
||||
return nil
|
||||
}
|
||||
func (n *noopUpdater) Update(_ Function) error { return nil }
|
||||
|
||||
type noopRunner struct{ output io.Writer }
|
||||
|
||||
func (n *noopRunner) Run(_ Function) error {
|
||||
fmt.Fprintln(n.output, "skipping run: client not initialized WithRunner")
|
||||
return nil
|
||||
}
|
||||
func (n *noopRunner) Run(_ Function) error { return nil }
|
||||
|
||||
type noopRemover struct{ output io.Writer }
|
||||
|
||||
func (n *noopRemover) Remove(string) error {
|
||||
fmt.Fprintln(n.output, "skipping remove: client not initialized WithRemover")
|
||||
return nil
|
||||
}
|
||||
func (n *noopRemover) Remove(string) error { return nil }
|
||||
|
||||
type noopLister struct{ output io.Writer }
|
||||
|
||||
func (n *noopLister) List() ([]string, error) {
|
||||
fmt.Fprintln(n.output, "skipping list: client not initialized WithLister")
|
||||
return []string{}, nil
|
||||
}
|
||||
func (n *noopLister) List() ([]string, error) { return []string{}, nil }
|
||||
|
||||
type noopDNSProvider struct{ output io.Writer }
|
||||
|
||||
func (n *noopDNSProvider) Provide(_ Function) error {
|
||||
// Note: at this time manual DNS provisioning required for name -> knative serving netowrk load-balancer
|
||||
return nil
|
||||
}
|
||||
func (n *noopDNSProvider) Provide(_ Function) error { return nil }
|
||||
|
||||
type noopProgressListener struct{}
|
||||
|
||||
|
|
969
client_test.go
969
client_test.go
File diff suppressed because it is too large
Load Diff
|
@ -1 +0,0 @@
|
|||
package faas
|
|
@ -31,11 +31,13 @@ func runDelete(cmd *cobra.Command, args []string) (err error) {
|
|||
remover := knative.NewRemover()
|
||||
remover.Verbose = config.Verbose
|
||||
|
||||
function := faas.Function{Root: config.Path, Name: config.Name}
|
||||
|
||||
client := faas.New(
|
||||
faas.WithVerbose(verbose),
|
||||
faas.WithRemover(remover))
|
||||
|
||||
return client.Remove(config.Name, config.Path)
|
||||
return client.Remove(function)
|
||||
}
|
||||
|
||||
type deleteConfig struct {
|
||||
|
|
|
@ -19,9 +19,7 @@ func init() {
|
|||
initCmd.Flags().StringP("trigger", "t", faas.DefaultTrigger, "Function trigger (ex: 'http','events') - $FAAS_TRIGGER")
|
||||
initCmd.Flags().BoolP("yes", "y", false, "When in interactive mode (attached to a TTY), skip prompts. - $FAAS_YES")
|
||||
|
||||
var err error
|
||||
err = initCmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList)
|
||||
if err != nil {
|
||||
if err := initCmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList); err != nil {
|
||||
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/ory/viper"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -99,6 +98,8 @@ func newListConfig() listConfig {
|
|||
}
|
||||
|
||||
// DEPRECATED BELOW (?):
|
||||
// TODO: regenerate completions, which may necessitate the below change:
|
||||
/*
|
||||
|
||||
var validFormats []string
|
||||
|
||||
|
@ -129,3 +130,4 @@ func fmtYAML(writer io.Writer, names []string) error {
|
|||
encoder := yaml.NewEncoder(writer)
|
||||
return encoder.Encode(names)
|
||||
}
|
||||
*/
|
||||
|
|
20
config.go
20
config.go
|
@ -8,12 +8,12 @@ import (
|
|||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// ConfigFileName is the name of the config's serialized form.
|
||||
const ConfigFileName = ".faas.config"
|
||||
// ConfigFile is the name of the config's serialized form.
|
||||
const ConfigFile = ".faas.yaml"
|
||||
|
||||
// Config represents the serialized state of a Function's metadata.
|
||||
// See the Function struct for attribute documentation.
|
||||
type Config struct {
|
||||
type config struct {
|
||||
Name string `yaml:"name"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
Runtime string `yaml:"runtime"`
|
||||
|
@ -26,8 +26,8 @@ type Config struct {
|
|||
// errors accessing an extant config file, or the contents of the file do not
|
||||
// unmarshall. A missing file at a valid path does not error but returns the
|
||||
// empty value of Config.
|
||||
func newConfig(root string) (c Config, err error) {
|
||||
filename := filepath.Join(root, ConfigFileName)
|
||||
func newConfig(root string) (c config, err error) {
|
||||
filename := filepath.Join(root, ConfigFile)
|
||||
if _, err = os.Stat(filename); os.IsNotExist(err) {
|
||||
err = nil // do not consider a missing config file an error
|
||||
return // return the zero value of the config
|
||||
|
@ -42,7 +42,7 @@ func newConfig(root string) (c Config, err error) {
|
|||
|
||||
// fromConfig returns a Function populated from config.
|
||||
// Note that config does not include ancillary fields not serialized, such as Root.
|
||||
func fromConfig(c Config) (f Function) {
|
||||
func fromConfig(c config) (f Function) {
|
||||
return Function{
|
||||
Name: c.Name,
|
||||
Namespace: c.Namespace,
|
||||
|
@ -52,8 +52,8 @@ func fromConfig(c Config) (f Function) {
|
|||
}
|
||||
|
||||
// toConfig serializes a Function to a config object.
|
||||
func toConfig(f Function) Config {
|
||||
return Config{
|
||||
func toConfig(f Function) config {
|
||||
return config{
|
||||
Name: f.Name,
|
||||
Namespace: f.Namespace,
|
||||
Runtime: f.Runtime,
|
||||
|
@ -63,9 +63,9 @@ func toConfig(f Function) Config {
|
|||
|
||||
// writeConfig for the given Function out to disk at root.
|
||||
func writeConfig(f Function) (err error) {
|
||||
path := filepath.Join(f.Root, ConfigFileName)
|
||||
path := filepath.Join(f.Root, ConfigFile)
|
||||
c := toConfig(f)
|
||||
bb := []byte{}
|
||||
var bb []byte
|
||||
if bb, err = yaml.Marshal(&c); err != nil {
|
||||
return
|
||||
}
|
||||
|
|
23
function.go
23
function.go
|
@ -97,6 +97,13 @@ func (f Function) Initialized() bool {
|
|||
// Default if not provided is --repository (a required global setting)
|
||||
// followed by the provided (or derived) image name.
|
||||
func DerivedImage(root, repository string) (image string, err error) {
|
||||
// Repository is currently required until such time as we support
|
||||
// pushing to an implicitly-available in-cluster registry by default.
|
||||
if repository == "" {
|
||||
err = errors.New("Repository name is required.")
|
||||
return
|
||||
}
|
||||
|
||||
f, err := NewFunction(root)
|
||||
if err != nil {
|
||||
// an inability to load the funciton means it is not yet initialized
|
||||
|
@ -127,6 +134,12 @@ func DerivedImage(root, repository string) (image string, err error) {
|
|||
} else {
|
||||
err = fmt.Errorf("repository should be either 'namespace' or 'registry/namespace'")
|
||||
}
|
||||
|
||||
// Explicitly append :latest. We currently expect source control to drive
|
||||
// versioning, rather than rely on Docker Hub tags with explicit version
|
||||
// numbers, as is seen in many serverless solutions. This will be updated
|
||||
// to branch name when we add source-driven canary/ bluegreen deployments.
|
||||
image = image + ":latest"
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -165,7 +178,7 @@ func assertEmptyRoot(path string) (err error) {
|
|||
// contentiousFiles are files which, if extant, preclude the creation of a
|
||||
// Function rooted in the given directory.
|
||||
var contentiousFiles = []string{
|
||||
".faas.yaml",
|
||||
ConfigFile,
|
||||
".appsody-config.yaml",
|
||||
}
|
||||
|
||||
|
@ -182,14 +195,8 @@ func contentiousFilesIn(dir string) (contentious []string, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// effectivelyEmpty directories are those which have no visible files,
|
||||
// and none of the explicitly enumerated contentious files.
|
||||
// effectivelyEmpty directories are those which have no visible files
|
||||
func isEffectivelyEmpty(dir string) (bool, error) {
|
||||
// Check for contentious files
|
||||
if contentious, err := contentiousFilesIn(dir); len(contentious) > 0 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check for any non-hidden files
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
package faas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -77,66 +73,3 @@ func TestPathToDomain(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyConfig ensures that
|
||||
// - a directory with no config has nothing applied without error.
|
||||
// - a directory with an invalid config errors.
|
||||
// - a directory with a valid config has it applied.
|
||||
func TestApplyConfig(t *testing.T) {
|
||||
// Create a temporary directory
|
||||
root := "./testdata/example.com/cfgtest"
|
||||
cfgFile := filepath.Join(root, ConfigFileName)
|
||||
err := os.MkdirAll(root, 0700)
|
||||
if err != nil {
|
||||
fmt.Println("Error on TestApplyConfig: ", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
c := Function{Name: "staticDefault"}
|
||||
|
||||
// Assert config optional.
|
||||
// Ensure that applying a directory with no config does not error.
|
||||
if err := applyConfig(&c, root); err != nil {
|
||||
t.Fatalf("unexpected error applying a nonexistent config: %v", err)
|
||||
}
|
||||
|
||||
// Assert an extant, but empty config file causes no errors,
|
||||
// and leaves data intact on the client instance.
|
||||
if err := ioutil.WriteFile(cfgFile, []byte(""), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := applyConfig(&c, root); err != nil {
|
||||
t.Fatalf("unexpected error applying an empty config: %v", err)
|
||||
}
|
||||
|
||||
// Assert an unparseable config file errors
|
||||
if err := ioutil.WriteFile(cfgFile, []byte("=invalid="), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := applyConfig(&c, root); err == nil {
|
||||
t.Fatal("Did not receive expected error from invalid config.")
|
||||
}
|
||||
|
||||
// Assert a valid config with no value zeroes out the default
|
||||
if err := ioutil.WriteFile(cfgFile, []byte("name:"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := applyConfig(&c, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Name != "" {
|
||||
t.Fatalf("Expected name to be zeroed by config, but got '%v'", c.Name)
|
||||
}
|
||||
|
||||
// Assert a valid config with a value for name is applied.
|
||||
if err := ioutil.WriteFile(cfgFile, []byte("name: www.example.com"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := applyConfig(&c, root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Name != "www.example.com" {
|
||||
t.Fatalf("Expected name 'www.example.com', got '%v'", c.Name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
package mock
|
||||
|
||||
import "github.com/boson-project/faas"
|
||||
|
||||
type Builder struct {
|
||||
BuildInvoked bool
|
||||
BuildFn func(tag string) (image string, err error)
|
||||
BuildFn func(faas.Function) error
|
||||
}
|
||||
|
||||
func NewBuilder() *Builder {
|
||||
return &Builder{
|
||||
BuildFn: func(string) (string, error) { return "", nil },
|
||||
BuildFn: func(faas.Function) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Builder) Build(tag string) (string, error) {
|
||||
func (i *Builder) Build(f faas.Function) error {
|
||||
i.BuildInvoked = true
|
||||
return i.BuildFn(tag)
|
||||
return i.BuildFn(f)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
package mock
|
||||
|
||||
import "github.com/boson-project/faas"
|
||||
|
||||
type Deployer struct {
|
||||
DeployInvoked bool
|
||||
DeployFn func(name, image string) (address string, err error)
|
||||
DeployFn func(faas.Function) error
|
||||
}
|
||||
|
||||
func NewDeployer() *Deployer {
|
||||
return &Deployer{
|
||||
DeployFn: func(string, string) (string, error) { return "", nil },
|
||||
DeployFn: func(faas.Function) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Deployer) Deploy(name, image string) (address string, err error) {
|
||||
func (i *Deployer) Deploy(f faas.Function) error {
|
||||
i.DeployInvoked = true
|
||||
return i.DeployFn(name, image)
|
||||
return i.DeployFn(f)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
package mock
|
||||
|
||||
import "github.com/boson-project/faas"
|
||||
|
||||
type Pusher struct {
|
||||
PushInvoked bool
|
||||
PushFn func(tag string) error
|
||||
PushFn func(faas.Function) error
|
||||
}
|
||||
|
||||
func NewPusher() *Pusher {
|
||||
return &Pusher{
|
||||
PushFn: func(tag string) error { return nil },
|
||||
PushFn: func(faas.Function) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Pusher) Push(tag string) error {
|
||||
func (i *Pusher) Push(f faas.Function) error {
|
||||
i.PushInvoked = true
|
||||
return i.PushFn(tag)
|
||||
return i.PushFn(f)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package mock
|
||||
|
||||
import "github.com/boson-project/faas"
|
||||
|
||||
type Runner struct {
|
||||
RunInvoked bool
|
||||
RootRequested string
|
||||
|
@ -9,8 +11,8 @@ func NewRunner() *Runner {
|
|||
return &Runner{}
|
||||
}
|
||||
|
||||
func (r *Runner) Run(root string) error {
|
||||
func (r *Runner) Run(f faas.Function) error {
|
||||
r.RunInvoked = true
|
||||
r.RootRequested = root
|
||||
r.RootRequested = f.Root
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
package mock
|
||||
|
||||
import "github.com/boson-project/faas"
|
||||
|
||||
type Updater struct {
|
||||
UpdateInvoked bool
|
||||
UpdateFn func(name, image string) error
|
||||
UpdateFn func(faas.Function) error
|
||||
}
|
||||
|
||||
func NewUpdater() *Updater {
|
||||
return &Updater{
|
||||
UpdateFn: func(string, string) error { return nil },
|
||||
UpdateFn: func(faas.Function) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Updater) Update(name, image string) error {
|
||||
func (i *Updater) Update(f faas.Function) error {
|
||||
i.UpdateInvoked = true
|
||||
return i.UpdateFn(name, image)
|
||||
return i.UpdateFn(f)
|
||||
}
|
||||
|
|
16
templates.go
16
templates.go
|
@ -20,8 +20,8 @@ import (
|
|||
// an HTTP Handler ("http") and Cloud Events ("events")
|
||||
const DefaultTemplate = "http"
|
||||
|
||||
// FileAccessor encapsulates methods for accessing template files.
|
||||
type FileAccessor interface {
|
||||
// fileAccessor encapsulates methods for accessing template files.
|
||||
type fileAccessor interface {
|
||||
Stat(name string) (os.FileInfo, error)
|
||||
Open(p string) (file, error)
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ type file interface {
|
|||
// When pkger is run, code analysis detects this Include statement,
|
||||
// triggering the serializaation of the templates directory and all
|
||||
// its contents into pkged.go, which is then made available via
|
||||
// a pkger FileAccessor.
|
||||
// a pkger fileAccessor.
|
||||
// Path is relative to the go module root.
|
||||
func init() {
|
||||
_ = pkger.Include("/templates")
|
||||
|
@ -112,7 +112,7 @@ func (a filesystemAccessor) Open(path string) (file, error) {
|
|||
return os.Open(path)
|
||||
}
|
||||
|
||||
func copy(src, dest string, accessor FileAccessor) (err error) {
|
||||
func copy(src, dest string, accessor fileAccessor) (err error) {
|
||||
node, err := accessor.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -124,7 +124,7 @@ func copy(src, dest string, accessor FileAccessor) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func copyNode(src, dest string, accessor FileAccessor) (err error) {
|
||||
func copyNode(src, dest string, accessor fileAccessor) (err error) {
|
||||
node, err := accessor.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -135,7 +135,7 @@ func copyNode(src, dest string, accessor FileAccessor) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
children, err := ReadDir(src, accessor)
|
||||
children, err := readDir(src, accessor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ func copyNode(src, dest string, accessor FileAccessor) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func ReadDir(src string, accessor FileAccessor) ([]os.FileInfo, error) {
|
||||
func readDir(src string, accessor fileAccessor) ([]os.FileInfo, error) {
|
||||
f, err := accessor.Open(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -161,7 +161,7 @@ func ReadDir(src string, accessor FileAccessor) ([]os.FileInfo, error) {
|
|||
return list, nil
|
||||
}
|
||||
|
||||
func copyLeaf(src, dest string, accessor FileAccessor) (err error) {
|
||||
func copyLeaf(src, dest string, accessor fileAccessor) (err error) {
|
||||
srcFile, err := accessor.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
@ -6,108 +6,19 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
// TestInitialize ensures that on initialization of a the reference runtime
|
||||
// (Go), the template is written.
|
||||
func TestInitialize(t *testing.T) {
|
||||
var (
|
||||
path = "testdata/example.org/www"
|
||||
testFile = "handle.go"
|
||||
template = "http"
|
||||
)
|
||||
err := os.MkdirAll(path, 0744)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
err = NewInitializer("").Initialize("go", template, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test that the directory is not empty
|
||||
if _, err := os.Stat(filepath.Join(path, testFile)); os.IsNotExist(err) {
|
||||
t.Fatalf("Initialize did not result in '%v' being written to '%v'", testFile, path)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultTemplate ensures that if no template is provided, files are still written.
|
||||
func TestDefaultTemplate(t *testing.T) {
|
||||
var (
|
||||
path = "testdata/example.org/www"
|
||||
testFile = "handle.go"
|
||||
template = ""
|
||||
)
|
||||
err := os.MkdirAll(path, 0744)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
err = NewInitializer("").Initialize("go", template, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(path, testFile)); os.IsNotExist(err) {
|
||||
t.Fatalf("Initializing without providing a template did not result in '%v' being written to '%v'", testFile, path)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCustom ensures that a custom repository can be used as a template.
|
||||
// Custom repository 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 $FAAS_TEMPLATES, a Go template named 'json'
|
||||
// which is provided in the repository repository 'boson-experimental',
|
||||
// would be expected to be in the location:
|
||||
// $FAAS_TEMPLATES/boson-experimental/go/json
|
||||
// See the CLI for full details, but a standard default location is
|
||||
// $HOME/.config/templates/boson-experimental/go/json
|
||||
func TestCustom(t *testing.T) {
|
||||
var (
|
||||
path = "testdata/example.org/www"
|
||||
testFile = "handle.go"
|
||||
template = "boson-experimental/json"
|
||||
// repos = "testdata/templates"
|
||||
)
|
||||
err := os.MkdirAll(path, 0744)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Unrecognized runtime/template should error
|
||||
err = NewInitializer("").Initialize("go", template, path)
|
||||
if err == nil {
|
||||
t.Fatal("An unrecognized runtime/template should generate an error")
|
||||
}
|
||||
|
||||
// Recognized external (non-embedded) path should succeed
|
||||
err = NewInitializer("testdata/templates").Initialize("go", template, path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The template should have been written to the given path.
|
||||
if _, err := os.Stat(filepath.Join(path, testFile)); os.IsNotExist(err) {
|
||||
t.Fatalf("Initializing a custom did not result in the expected '%v' being written to '%v'", testFile, path)
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmbeddedFileMode ensures that files from the embedded templates are
|
||||
// TestTemplatesEmbeddedFileMode ensures that files from the embedded templates are
|
||||
// written with the same mode from whence they came
|
||||
func TestEmbeddedFileMode(t *testing.T) {
|
||||
var path = "testdata/example.org/www"
|
||||
func TestTemplatesEmbeddedFileMode(t *testing.T) {
|
||||
var path = "testdata/example.com/www"
|
||||
err := os.MkdirAll(path, 0744)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Initialize a quarkus app from the embedded templates.
|
||||
if err := NewInitializer("").Initialize("quarkus", "events", path); err != nil {
|
||||
client := New()
|
||||
function := Function{Root: path, Runtime: "quarkus", Trigger: "events"}
|
||||
if err := client.Initialize(function); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -124,12 +35,13 @@ func TestEmbeddedFileMode(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestCustomFileMode ensures that files from a file-system derived repository
|
||||
// of templates are written with the same mode from whence they came
|
||||
func TestFileMode(t *testing.T) {
|
||||
// TestTemplatesExtensibleFileMode ensures that files from a file-system
|
||||
// derived template is written with mode retained.
|
||||
func TestTemplatesExtensibleFileMode(t *testing.T) {
|
||||
var (
|
||||
path = "testdata/example.org/www"
|
||||
template = "boson-experimental/http"
|
||||
path = "testdata/example.com/www"
|
||||
template = "boson-experimental/http"
|
||||
templates = "testdata/templates"
|
||||
)
|
||||
err := os.MkdirAll(path, 0744)
|
||||
if err != nil {
|
||||
|
@ -137,8 +49,9 @@ func TestFileMode(t *testing.T) {
|
|||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Initialize a quarkus app from the custom repo in ./testdata
|
||||
if err = NewInitializer("testdata/templates").Initialize("quarkus", template, path); err != nil {
|
||||
client := New(WithTemplates(templates))
|
||||
function := Function{Root: path, Runtime: "quarkus", Trigger: template}
|
||||
if err := client.Initialize(function); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# testdata
|
||||
|
||||
Used by tests to hold files necessary for completing and as a place to create
|
||||
service functions of varying configurations.
|
||||
Contains test templates and directory targets for domain and subdomain-level tests.
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# region1
|
||||
|
||||
Used as a test target.
|
|
@ -1,3 +0,0 @@
|
|||
id: "20200507014534.69605855"
|
||||
project-name: www-example-com
|
||||
stack: quay.io/boson/go-ce-functions:0.0
|
|
@ -1,2 +0,0 @@
|
|||
name: www.example.com
|
||||
runtime: go
|
|
@ -1,5 +0,0 @@
|
|||
module function
|
||||
|
||||
go 1.13
|
||||
|
||||
require github.com/cloudevents/sdk-go v0.10.2
|
|
@ -1,32 +0,0 @@
|
|||
package function
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
cloudevents "github.com/cloudevents/sdk-go"
|
||||
)
|
||||
|
||||
// Handle a CloudEvent.
|
||||
// Supported function signatures:
|
||||
// func()
|
||||
// func() error
|
||||
// func(context.Context)
|
||||
// func(context.Context) error
|
||||
// func(cloudevents.Event)
|
||||
// func(cloudevents.Event) error
|
||||
// func(context.Context, cloudevents.Event)
|
||||
// func(context.Context, cloudevents.Event) error
|
||||
// func(cloudevents.Event, *cloudevents.EventResponse)
|
||||
// func(cloudevents.Event, *cloudevents.EventResponse) error
|
||||
// func(context.Context, cloudevents.Event, *cloudevents.EventResponse)
|
||||
// func(context.Context, cloudevents.Event, *cloudevents.EventResponse) error
|
||||
func Handle(ctx context.Context, event cloudevents.Event) error {
|
||||
if err := event.Validate(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid event received. %v", err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%v\n", event)
|
||||
return nil
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
package function
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
cloudevents "github.com/cloudevents/sdk-go"
|
||||
)
|
||||
|
||||
// TestHandle ensures that Handle accepts a valid CloudEvent without error.
|
||||
func TestHandle(t *testing.T) {
|
||||
// A minimal, but valid, event.
|
||||
event := cloudevents.NewEvent()
|
||||
event.SetID("TEST-EVENT-01")
|
||||
event.SetType("com.example.cloudevents.test")
|
||||
event.SetSource("http://localhost:8080/")
|
||||
|
||||
// Invoke the defined handler.
|
||||
if err := Handle(context.Background(), event); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleInvalid ensures that an invalid input event generates an error.
|
||||
func TestInvalidInput(t *testing.T) {
|
||||
invalidEvent := cloudevents.NewEvent() // missing required fields
|
||||
|
||||
// Attempt to handle the invalid event, ensuring that the handler validats events.
|
||||
if err := Handle(context.Background(), invalidEvent); err == nil {
|
||||
t.Fatalf("handler did not generate error on invalid event. Missing .Validate() check?")
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2E also tests the Handle function, but does so by creating an actual
|
||||
// CloudEvents HTTP sending and receiving clients. This is a bit redundant
|
||||
// with TestHandle, but illustrates how clients are configured and used.
|
||||
func TestE2E(t *testing.T) {
|
||||
var (
|
||||
receiver cloudevents.Client
|
||||
address string // at which the receiver beings listening (os-chosen port)
|
||||
sender cloudevents.Client // sends an event to the receiver via HTTP
|
||||
handler = Handle // test the user-defined Handler
|
||||
err error
|
||||
)
|
||||
|
||||
if receiver, address, err = newReceiver(t); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if sender, err = newSender(t, address); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := receiver.StartReceiver(context.Background(), handler); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, resp, err := sender.Send(context.Background(), newEvent(t, TestData{Sequence: 1, Message: "test message"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("OK:\n%v\n", resp)
|
||||
}
|
||||
|
||||
type TestData struct {
|
||||
Sequence int `json:"id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func newReceiver(t *testing.T) (c cloudevents.Client, address string, err error) {
|
||||
t.Helper()
|
||||
transport, err := cloudevents.NewHTTPTransport(
|
||||
cloudevents.WithPort(0), // use an OS-chosen unused port.
|
||||
cloudevents.WithPath("/"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
address = fmt.Sprintf("http://127.0.0.1:%v/", transport.GetPort())
|
||||
c, err = cloudevents.NewClient(transport)
|
||||
return
|
||||
}
|
||||
|
||||
func newSender(t *testing.T, address string) (c cloudevents.Client, err error) {
|
||||
t.Helper()
|
||||
transport, err := cloudevents.NewHTTPTransport(
|
||||
cloudevents.WithTarget(address),
|
||||
cloudevents.WithEncoding(cloudevents.HTTPStructuredV01))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return cloudevents.NewClient(transport, cloudevents.WithTimeNow())
|
||||
}
|
||||
|
||||
func newEvent(t *testing.T, data TestData) (event cloudevents.Event) {
|
||||
source, err := url.Parse("https://example.com/cloudfunction/cloudevent/cmd/runner")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contentType := "application/json"
|
||||
event = cloudevents.Event{
|
||||
Context: cloudevents.EventContextV01{
|
||||
EventID: "test-event-01",
|
||||
EventType: "com.cloudevents.sample.sent",
|
||||
Source: cloudevents.URLRef{URL: *source},
|
||||
ContentType: &contentType,
|
||||
}.AsV01(),
|
||||
Data: &data,
|
||||
}
|
||||
return
|
||||
}
|
Loading…
Reference in New Issue