refactor!: function signatures implied from trigger

Renames trigger to template, removing it as an unnecessary configuration.
This reiterates that a Function implementation can change function sig
implemented at any time, and it is not part of the configuration.  This
sets the stage for renaming 'templates', and the finalization of the
use cases enabling extensible templates.
This commit is contained in:
Luke Kingland 2021-06-09 01:07:08 +09:00
parent b3a6bdf398
commit b30e883e67
No known key found for this signature in database
GPG Key ID: 4896F75BAF2E1966
22 changed files with 88 additions and 113 deletions

View File

@ -15,7 +15,12 @@ import (
const (
DefaultRegistry = "docker.io"
DefaultRuntime = "node"
DefaultTrigger = "http"
// DefautlTemplate is the default Function signature / environmental context
// of the resultant function. All runtimes are expected to have at least
// one implementation of each supported funciton sinagure. Currently that
// includes an HTTP Handler ("http") and Cloud Events handler ("events")
DefaultTemplate = "http"
)
// Client for managing Function instances.
@ -348,15 +353,15 @@ func (c *Client) Create(cfg Function) (err error) {
f.Runtime = DefaultRuntime
}
// Assert trigger was provided, or default.
f.Trigger = cfg.Trigger
if f.Trigger == "" {
f.Trigger = DefaultTrigger
// Assert template was provided, or default.
f.Template = cfg.Template
if f.Template == "" {
f.Template = DefaultTemplate
}
// Write out a template.
w := templateWriter{templates: c.templates, verbose: c.verbose}
if err = w.Write(f.Runtime, f.Trigger, f.Root); err != nil {
if err = w.Write(f.Runtime, f.Template, f.Root); err != nil {
return
}

View File

@ -154,13 +154,6 @@ func TestDefaultRuntime(t *testing.T) {
}
}
// TestDefaultTemplate ensures that the default template is
// applied when not provided.
func TestDefaultTrigger(t *testing.T) {
// TODO: need to either expose accessor for introspection, or compare
// the files written to those in the embedded repisotory?
}
// TestExtensibleTemplates templates. Ensures that templates are extensible
// using a custom path to a template repository on disk. Custom repository
// location is not defined herein but expected to be provided because, for
@ -184,7 +177,7 @@ func TestExtensibleTemplates(t *testing.T) {
bosonFunc.WithRegistry(TestRegistry))
// Create a Function specifying a template, 'json' that only exists in the extensible set
if err := client.New(context.Background(), bosonFunc.Function{Root: root, Trigger: "boson-experimental/json"}); err != nil {
if err := client.New(context.Background(), bosonFunc.Function{Root: root, Template: "boson-experimental/json"}); err != nil {
t.Fatal(err)
}

View File

@ -17,7 +17,7 @@ func init() {
createCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
createCmd.Flags().StringP("runtime", "l", bosonFunc.DefaultRuntime, "Function runtime language/framework. Available runtimes: "+utils.RuntimeList()+" (Env: $FUNC_RUNTIME)")
createCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "templates"), "Path to additional templates (Env: $FUNC_TEMPLATES)")
createCmd.Flags().StringP("trigger", "t", bosonFunc.DefaultTrigger, "Function trigger. Available triggers: 'http' and 'events' (Env: $FUNC_TRIGGER)")
createCmd.Flags().StringP("template", "t", bosonFunc.DefaultTemplate, "Function template. For eample 'http' or 'events' (Env: $FUNC_TEMPLATE)")
if err := createCmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList); err != nil {
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
@ -43,12 +43,12 @@ kn func create
kn func create --runtime quarkus myfunc
# Create a function project that uses a CloudEvent based function signature
kn func create --trigger events myfunc
kn func create -t events myfunc
`,
SuggestFor: []string{"inti", "new"},
PreRunE: bindEnv("runtime", "templates", "trigger", "confirm"),
PreRunE: bindEnv("runtime", "templates", "template", "confirm"),
RunE: runCreate,
// TODO: autocomplate Functions for runtime and trigger.
// TODO: autocomplate or interactive prompt for runtime and template.
}
func runCreate(cmd *cobra.Command, args []string) error {
@ -61,10 +61,10 @@ func runCreate(cmd *cobra.Command, args []string) error {
config = config.Prompt()
function := bosonFunc.Function{
Name: config.Name,
Root: config.Path,
Runtime: config.Runtime,
Trigger: config.Trigger,
Name: config.Name,
Root: config.Path,
Runtime: config.Runtime,
Template: config.Template,
}
client := bosonFunc.New(
@ -90,11 +90,14 @@ type createConfig struct {
// location is $XDG_CONFIG_HOME/templates ($HOME/.config/func/templates)
Templates string
// Trigger is the form of the resultant Function, i.e. the Function signature
// and contextually avaialable resources. For example 'http' for a Function
// expected to be invoked via straight HTTP requests, or 'events' for a
// Function which will be invoked with CloudEvents.
Trigger string
// Template is the code written into the new Function project, including
// an implementation adhering to one of the supported function signatures.
// May also include additional configuration settings or examples.
// For example, embedded are 'http' for a Function whose funciton signature
// is invoked via straight HTTP requests, or 'events' for a Function which
// will be invoked with CloudEvents. These embedded templates contain a
// minimum implementation of the signature itself and example tests.
Template string
// Verbose output
Verbose bool
@ -118,7 +121,7 @@ func newCreateConfig(args []string) createConfig {
Path: derivedPath,
Runtime: viper.GetString("runtime"),
Templates: viper.GetString("templates"),
Trigger: viper.GetString("trigger"),
Template: viper.GetString("template"),
Confirm: viper.GetBool("confirm"),
Verbose: viper.GetBool("verbose"),
}
@ -133,7 +136,7 @@ func (c createConfig) Prompt() createConfig {
fmt.Printf("Project path: %v\n", c.Path)
fmt.Printf("Function name: %v\n", c.Name)
fmt.Printf("Runtime: %v\n", c.Runtime)
fmt.Printf("Trigger: %v\n", c.Trigger)
fmt.Printf("Template: %v\n", c.Template)
return c
}
@ -148,10 +151,9 @@ func (c createConfig) Prompt() createConfig {
}
return createConfig{
Name: derivedName,
Path: derivedPath,
Runtime: prompt.ForString("Runtime", c.Runtime),
Trigger: prompt.ForString("Trigger", c.Trigger),
// Templates intentionally omitted from prompt for being an edge case.
Name: derivedName,
Path: derivedPath,
Runtime: prompt.ForString("Runtime", c.Runtime),
Template: prompt.ForString("Template", c.Template),
}
}

View File

@ -47,7 +47,6 @@ namespace: ""
runtime: go
image: ""
imageDigest: ""
trigger: http
builder: quay.io/boson/faas-go-builder
builderMap:
default: quay.io/boson/faas-go-builder
@ -72,7 +71,6 @@ annotations: {}
}
f.Close()
oldWD, err := os.Getwd()
if err != nil {
t.Fatal(err)

View File

@ -36,7 +36,6 @@ type config struct {
Runtime string `yaml:"runtime"`
Image string `yaml:"image"`
ImageDigest string `yaml:"imageDigest"`
Trigger string `yaml:"trigger"`
Builder string `yaml:"builder"`
BuilderMap map[string]string `yaml:"builderMap"`
Volumes Volumes `yaml:"volumes"`
@ -128,7 +127,6 @@ func fromConfig(c config) (f Function) {
Runtime: c.Runtime,
Image: c.Image,
ImageDigest: c.ImageDigest,
Trigger: c.Trigger,
Builder: c.Builder,
BuilderMap: c.BuilderMap,
Volumes: c.Volumes,
@ -145,7 +143,6 @@ func toConfig(f Function) config {
Runtime: f.Runtime,
Image: f.Image,
ImageDigest: f.ImageDigest,
Trigger: f.Trigger,
Builder: f.Builder,
BuilderMap: f.BuilderMap,
Volumes: f.Volumes,
@ -227,7 +224,7 @@ func ValidateEnvs(envs Envs) (errors []string) {
// all key-pair values from secret are set as ENV; {{ secret.secretName }} or {{ configMap.configMapName }}
if !regWholeSecret.MatchString(*env.Value) && !regWholeConfigMap.MatchString(*env.Value) {
errors = append(errors, fmt.Sprintf("env entry #%d has invalid value field set, it has '%s', but allowed is only '{{ secret.secretName }}' or '{{ configMap.configMapName }}'",
i, *env.Value))
i, *env.Value))
}
} else {
if strings.HasPrefix(*env.Value, "{{") {

View File

@ -2,20 +2,20 @@
## `create`
Creates a new Function project at _`path`_. If _`path`_ is unspecified, assumes the current directory. If _`path`_ does not exist, it will be created. The function name is the name of the leaf directory at path. The user can specify the runtime and trigger with flags.
Creates a new Function project at _`path`_. If _`path`_ is unspecified, assumes the current directory. If _`path`_ does not exist, it will be created. The function name is the name of the leaf directory at path. The user can specify the runtime and template with flags.
Function name must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?').
Similar `kn` command: none.
```console
func create <path> [-l <runtime> -t <trigger>]
func create <path> [-l <runtime> -t <template>]
```
When run as a `kn` plugin.
```console
kn func create <path> [-l <runtime> -t <trigger>]
kn func create <path> [-l <runtime> -t <template>]
```
## `build`

View File

@ -87,10 +87,6 @@ The Kubernetes namespace where your function will be deployed.
The language runtime for your function. For example `python`.
### `trigger`
The invocation event that triggers your function. Possible values are `http`
for plain HTTP requests, and `events` for CloudEvent triggered functions.
## Local Environment Variables

View File

@ -9,7 +9,6 @@ template structure.
Project path: /home/developer/projects/fn
Function name: fn
Runtime: go
Trigger: http
tree
fn

View File

@ -39,7 +39,6 @@ func main() {
// Local implementation is written to the current working directory.
funcTest := bosonFunc.Function{
Runtime: "go",
Trigger: "events",
Name: "my-function",
Image: "quay.io/alice/my-function",
Root: "my-function",

View File

@ -9,7 +9,6 @@ template structure.
Project path: /home/developer/projects/fn
Function name: fn
Runtime: node
Trigger: http
tree fn
fn

View File

@ -9,7 +9,6 @@ template structure.
Project path: /home/developer/src/fn
Function name: fn
Runtime: python
Trigger: http
tree
fn

View File

@ -9,7 +9,6 @@ template structure.
Project path: /home/developer/projects/fn
Function name: fn
Runtime: quarkus
Trigger: http
tree
fn

View File

@ -9,7 +9,6 @@ template structure.
Project path: /home/developer/projects/fn
Function name: fn
Runtime: typescript
Trigger: http
tree fn
fn

View File

@ -23,8 +23,8 @@ type Function struct {
// Runtime is the language plus context. nodejs|go|quarkus|rust etc.
Runtime string
// Trigger of the Function. http|events etc.
Trigger string
// Template for the Function.
Template string
// Registry at which to store interstitial containers, in the form
// [registry]/[user]. If omitted, "Image" must be provided.
@ -55,7 +55,7 @@ type Function struct {
// List of volumes to be mounted to the function
Volumes Volumes
// Env variables to be set
// Env variables to be set
Envs Envs
// Map containing user-supplied annotations

View File

@ -17,11 +17,6 @@ import (
"github.com/markbates/pkger"
)
// DefautlTemplate is the default Function signature / environmental context
// of the resultant template. All runtimes are expected to have at least
// an HTTP Handler ("http") and Cloud Events ("events")
const DefaultTemplate = "http"
// fileAccessor encapsulates methods for accessing template files.
type fileAccessor interface {
Stat(name string) (os.FileInfo, error)

View File

@ -20,7 +20,7 @@ func TestTemplatesEmbeddedFileMode(t *testing.T) {
defer os.RemoveAll(path)
client := New()
function := Function{Root: path, Runtime: "quarkus", Trigger: "events"}
function := Function{Root: path, Runtime: "quarkus", Template: "events"}
if err := client.Create(function); err != nil {
t.Fatal(err)
}
@ -55,7 +55,7 @@ func TestTemplatesExtensibleFileMode(t *testing.T) {
defer os.RemoveAll(path)
client := New(WithTemplates(templates))
function := Function{Root: path, Runtime: "quarkus", Trigger: template}
function := Function{Root: path, Runtime: "quarkus", Template: template}
if err := client.Create(function); err != nil {
t.Fatal(err)
}

View File

@ -3,8 +3,8 @@ package e2e
import "testing"
// Create runs `func create' command for a given test project with basic validation
func Create(t *testing.T, knFunc *TestShellCmdRunner, project FunctionTestProject) {
result := knFunc.Exec("create", project.ProjectPath, "--runtime", project.Runtime, "--trigger", project.Trigger)
func Create(t *testing.T, knFunc *TestShellCmdRunner, project FunctionTestProject) {
result := knFunc.Exec("create", project.ProjectPath, "--runtime", project.Runtime, "--template", project.Template)
if result.Error != nil {
t.Fatal()
}

View File

@ -16,21 +16,21 @@ import (
// 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",
Trigger: "events",
FunctionName: "emit-test-node",
ProjectPath: filepath.Join(os.TempDir(), "emit-test-node"),
Runtime: "node",
Template: "events",
}
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)
@ -41,7 +41,7 @@ func TestEmitCommand(t *testing.T) {
if result.Error != nil {
t.Fatal()
}
// Issue another event (in order to capture the event sent by emit)
testEvent := SimpleTestEvent{
Type: "e2e:check",

View File

@ -18,24 +18,23 @@ type FunctionTestProject struct {
ProjectPath string
// Function Runtime. Example "node"
Runtime string
// Function Trigger. Example "http"
Trigger string
// Function Template. Example "http"
Template string
// Indicates function is already deployed
IsDeployed bool
// Indicates new revision deployed (custom template)
IsNewRevision bool
// Function URL
FunctionURL string
}
// NewFunctionTestProject initiates a project with derived function name an project path
func NewFunctionTestProject(runtime string, trigger string) FunctionTestProject {
func NewFunctionTestProject(runtime string, template string) FunctionTestProject {
project := FunctionTestProject{
Runtime: runtime,
Trigger: trigger,
Runtime: runtime,
Template: template,
}
project.FunctionName = "func-" + runtime + "-" + trigger
project.FunctionName = "func-" + runtime + "-" + template
project.ProjectPath = filepath.Join(os.TempDir(), project.FunctionName)
return project
}
@ -61,7 +60,7 @@ func (f FunctionTestProject) CreateProjectFolder() error {
func (f FunctionTestProject) RemoveProjectFolder() error {
if f.ProjectPath != "" {
err := os.RemoveAll(f.ProjectPath)
if err != nil && !os.IsNotExist(err) {
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to remove project folder: %s", err.Error())
}
}

View File

@ -9,15 +9,15 @@ import (
)
type SimpleTestEvent struct {
Type string
Source string
Type string
Source string
ContentType string
Data string
Data string
}
func (s SimpleTestEvent) pushTo(url string, t *testing.T) (body string, statusCode int, err error) {
client := &http.Client{}
req, err := http.NewRequest("POST", url, strings.NewReader(s.Data) )
req, err := http.NewRequest("POST", url, strings.NewReader(s.Data))
req.Header.Add("Ce-Id", "message-1")
req.Header.Add("Ce-Specversion", "1.0")
req.Header.Add("Ce-Type", s.Type)
@ -39,9 +39,9 @@ func (s SimpleTestEvent) pushTo(url string, t *testing.T) (body string, statusCo
}
type FunctionCloudEventsValidatorEntry struct {
targetUrl string
targetUrl string
contentType string
data string
data string
}
var defaultFunctionsCloudEventsValidators = map[string]FunctionCloudEventsValidatorEntry{
@ -57,11 +57,10 @@ var defaultFunctionsCloudEventsValidators = map[string]FunctionCloudEventsValida
},
}
// DefaultFunctionEventsTest executes a common test (applied for all runtimes) against a deployed
// functions that responds to CloudEvents
func DefaultFunctionEventsTest(t *testing.T, knFunc *TestShellCmdRunner, project FunctionTestProject) {
if project.Trigger == "events" && project.IsDeployed {
if project.Template == "events" && project.IsDeployed {
simpleEvent := SimpleTestEvent{
Type: "e2e.test",

View File

@ -10,9 +10,9 @@ import (
// HTTP Based Function Test Validator
type FunctionHttpResponsivenessValidator struct {
runtime string
runtime string
targetUrl string
expects string
expects string
}
func (f FunctionHttpResponsivenessValidator) Validate(t *testing.T, project FunctionTestProject) {
@ -31,32 +31,31 @@ func (f FunctionHttpResponsivenessValidator) Validate(t *testing.T, project Func
}
}
var defaultFunctionsHttpValidators = []FunctionHttpResponsivenessValidator{
{ runtime: "node",
{runtime: "node",
targetUrl: "%s?message=hello",
expects: `{"message":"hello"}`,
},
{ runtime: "go",
{runtime: "go",
targetUrl: "%s",
expects: `OK`,
},
{ runtime: "python",
{runtime: "python",
targetUrl: "%s",
expects: `Howdy!`,
},
{ runtime: "quarkus",
{runtime: "quarkus",
targetUrl: "%s?message=hello",
expects: `{"message":"hello"}`,
},
{ runtime: "springboot",
{runtime: "springboot",
targetUrl: "%s/health/readiness",
},
}
// DefaultFunctionHttpTest is meant to validate the deployed (default) function is actually responsive
func DefaultFunctionHttpTest(t *testing.T, knFunc *TestShellCmdRunner, project FunctionTestProject) {
if project.Trigger == "http" {
if project.Template == "http" {
for _, v := range defaultFunctionsHttpValidators {
v.Validate(t, project)
}
@ -64,19 +63,19 @@ func DefaultFunctionHttpTest(t *testing.T, knFunc *TestShellCmdRunner, project F
}
var newRevisionFunctionsHttpValidators = []FunctionHttpResponsivenessValidator{
{ runtime: "node",
{runtime: "node",
targetUrl: "%s",
expects: `HELLO NODE FUNCTION`,
},
{ runtime: "go",
{runtime: "go",
targetUrl: "%s",
expects: `HELLO GO FUNCTION`,
},
{ runtime: "python",
{runtime: "python",
targetUrl: "%s",
expects: `HELLO PYTHON FUNCTION`,
},
{ runtime: "quarkus",
{runtime: "quarkus",
targetUrl: "%s",
expects: `HELLO QUARKUS FUNCTION`,
},
@ -84,14 +83,13 @@ var newRevisionFunctionsHttpValidators = []FunctionHttpResponsivenessValidator{
// NewRevisionFunctionHttpTest is meant to validate the deployed function (new revision from Template) is actually responsive
func NewRevisionFunctionHttpTest(t *testing.T, knFunc *TestShellCmdRunner, project FunctionTestProject) {
if project.IsNewRevision && project.Trigger == "http" {
if project.IsNewRevision && project.Template == "http" {
for _, v := range newRevisionFunctionsHttpValidators {
v.Validate(t, project)
}
}
}
// HttpGet Convenient wrapper that calls an URL and returns just the
// body and status code. It fails in case some error occurs in the call
func HttpGet(t *testing.T, url string) (body string, statusCode int) {

View File

@ -11,11 +11,11 @@ import (
const updateTemplatesFolder = "update_templates"
// Update replaces the project content (source files) of the existing project in test
// by the source stored under 'update_template/<runtime>/<trigger>
// by the source stored under 'update_template/<runtime>/<template>
// Once sources are update the project is built and re-deployed
func Update(t *testing.T, knFunc *TestShellCmdRunner, project *FunctionTestProject) {
func Update(t *testing.T, knFunc *TestShellCmdRunner, project *FunctionTestProject) {
templatePath := filepath.Join(updateTemplatesFolder, project.Runtime, project.Trigger)
templatePath := filepath.Join(updateTemplatesFolder, project.Runtime, project.Template)
if _, err := os.Stat(templatePath); err != nil {
if os.IsNotExist(err) {
// skip update test when there is no template folder
@ -25,7 +25,7 @@ func Update(t *testing.T, knFunc *TestShellCmdRunner, project *FunctionTestProje
}
}
// Template folder exists for given runtime / trigger.
// Template folder exists for given runtime / template.
// Let's update the project and redeploy
err := projectUpdater{}.UpdateFolderContent(templatePath, project)
if err != nil {
@ -42,20 +42,19 @@ func Update(t *testing.T, knFunc *TestShellCmdRunner, project *FunctionTestProje
project.IsNewRevision = true
}
//
// projectUpdater offers methods to update the project source content by the
// source provided on update_templates folder
// The strategy used consists in
// 1. Create a temporary project folder with func.yaml (copied from test folder)
// 2. Copy recursivelly all files from ./update_template/<runtime>/<trigger>/** to the temporary project folder
// 2. Copy recursivelly all files from ./update_template/<runtime>/<template>/** to the temporary project folder
// 3. Replace current project folder by the temporary one (rm -rf <project folder> && mv <tmp folder> <project folder>
//
type projectUpdater struct {}
type projectUpdater struct{}
func (p projectUpdater) UpdateFolderContent(templatePath string, project *FunctionTestProject) error {
// Create temp project folder (reuse func.yaml)
projectTmp := NewFunctionTestProject(project.Runtime, project.Trigger)
projectTmp := NewFunctionTestProject(project.Runtime, project.Template)
projectTmp.ProjectPath = projectTmp.ProjectPath + "-tmp"
err := projectTmp.CreateProjectFolder()
if err != nil {
@ -118,7 +117,7 @@ func (p projectUpdater) walkThru(dir string, fn func(path string, f os.FileInfo)
return err
}
if file.IsDir() {
err := p.walkThru(filepath.Join(dir,file.Name()), fn)
err := p.walkThru(filepath.Join(dir, file.Name()), fn)
if err != nil {
return err
}