feat: cli usability enhancements and API simplification

This commit is contained in:
Luke K 2020-08-24 20:46:21 +09:00
parent 2fcbe740e9
commit 4918cc7eef
No known key found for this signature in database
GPG Key ID: 4896F75BAF2E1966
39 changed files with 1326 additions and 1096 deletions

View File

@ -80,5 +80,5 @@ release: build test
git tag $(VTAG)
clean:
rm -f $(WINDOWS) $(LINUX) $(DARWIN)
-@rm -f coverage.out
rm -f $(BIN) $(WINDOWS) $(LINUX) $(DARWIN)
-rm -f coverage.out

View File

@ -9,7 +9,7 @@ import (
"path/filepath"
)
// Builder of images from function source using appsody.
// Builder of images from Function source using appsody.
type Builder struct {
// Verbose logging flag.
Verbose bool
@ -25,7 +25,7 @@ func NewBuilder(registry, namespace string) *Builder {
namespace: namespace}
}
// Build an image from the function source at path.
// Build an image from the Function source at path.
func (n *Builder) Build(name, runtime, path string) (image string, err error) {
// Check for the appsody binary explicitly so that we can return
// an extra-friendly error message.
@ -70,7 +70,7 @@ func (n *Builder) Build(name, runtime, path string) (image string, err error) {
if _, err = os.Stat(cfg); err == nil {
err = os.Remove(cfg)
if err != nil {
fmt.Fprintf(os.Stderr,"unable to remove superfluous appsody config: %v\n", err)
fmt.Fprintf(os.Stderr, "unable to remove superfluous appsody config: %v\n", err)
}
}
return

View File

@ -19,7 +19,7 @@ var StackShortNames = map[string]string{
"quarkus": "quarkus-ce-functions",
}
// Initializer of functions using the appsody binary.
// Initializer of Functions using the appsody binary.
type Initializer struct {
// Verbose logging flag.
Verbose bool
@ -30,7 +30,7 @@ func NewInitializer() *Initializer {
return &Initializer{}
}
// Initialize a new function of the given name, of the given runtime, at the given path.
// Initialize a new Function of the given name, of the given runtime, at the given path.
func (n *Initializer) Initialize(name, runtime, path string) error {
// Check for the appsody binary explicitly so that we can return
// an extra-friendly error message.

View File

@ -28,7 +28,7 @@ func TestInitialize(t *testing.T) {
t.Skip()
}
// Create the directory in which the service function will be initialized,
// Create the directory in which the Function will be initialized,
// removing it on test completion.
if err := os.Mkdir("testdata/example.com/www", 0700); err != nil {
t.Fatal(err)

View File

@ -5,9 +5,11 @@ import (
"fmt"
"os"
"os/exec"
"github.com/boson-project/faas"
)
// Runner of functions using the appsody binary.
// Runner of Functions using the appsody binary.
type Runner struct {
// Verbose logging flag.
Verbose bool
@ -18,8 +20,8 @@ func NewRunner() *Runner {
return &Runner{}
}
// Run the function at path
func (n *Runner) Run(path string) error {
// Run the Function at path
func (n *Runner) Run(f faas.Function) error {
// Check for the appsody binary explicitly so that we can return
// an extra-friendly error message.
_, err := exec.LookPath("appsody")
@ -30,14 +32,14 @@ func (n *Runner) Run(path string) error {
// Extra arguments to appsody
args := []string{"run"}
// If verbosity is enabled, pass along as an environment variable to the function.
// If verbosity is enabled, pass along as an environment variable to the Function.
if n.Verbose {
args = append(args, []string{"--docker-options", "-e VERBOSE=true"}...)
}
// Set up the command with extra arguments and to run rooted at path
cmd := exec.Command("appsody", args...)
cmd.Dir = path
cmd.Dir = f.Root
// If verbose logging is enabled, echo command
if n.Verbose {

View File

@ -16,16 +16,16 @@ func TestRun(t *testing.T) {
// Testdata Function
//
// The directory has been pre-populated with a runnable base function by running
// The directory has been pre-populated with a runnable base Function by running
// init and committing the result, such that this test is not dependent on a
// functioning creation step. This code will need to be updated for any
// backwards incompatible changes to the templated base go func.
// Run the function
// Run the Function
// TODO: in a separate goroutine, submit an HTTP or CloudEvent request to the
// running function, and confirm expected OK response, then close down the
// running function by interrupting/canceling run. This may requre an update
// running Function, and confirm expected OK response, then close down the
// running Function by interrupting/canceling run. This may requre an update
// to the runner to run the command with a cancelable context, and cancel on
// normal signals as well as a signal specific to these tests such as SIGUSR1.

View File

@ -9,7 +9,7 @@ import (
)
// Handle a CloudEvent.
// Supported function signatures:
// Supported Function signatures:
// func()
// func() error
// func(context.Context)

View File

@ -8,18 +8,18 @@ import (
"io"
"os"
"github.com/boson-project/faas"
"github.com/buildpacks/pack"
"github.com/buildpacks/pack/logging"
"github.com/boson-project/faas"
)
type Builder struct {
Verbose bool
Tag string
}
func NewBuilder(tag string) *Builder {
return &Builder{Tag: tag}
func NewBuilder() *Builder {
return &Builder{}
}
var runtime2pack = map[string]string{
@ -28,19 +28,22 @@ var runtime2pack = map[string]string{
"go": "quay.io/boson/faas-go-builder",
}
func (builder *Builder) Build(path string) (image string, err error) {
f, err := faas.NewFunction(path)
if err != nil {
return
}
runtime := f.Runtime
packBuilder, ok := runtime2pack[runtime]
// Build the Function at path.
func (builder *Builder) Build(f faas.Function) (err error) {
// dervive the builder from the specificed runtime
packBuilder, ok := runtime2pack[f.Runtime]
if !ok {
err = errors.New(fmt.Sprint("unsupported runtime: ", runtime))
return
return errors.New(fmt.Sprint("unsupported runtime: ", f.Runtime))
}
// Build options for the pack client.
packOpts := pack.BuildOptions{
AppPath: f.Root,
Image: f.Image,
Builder: packBuilder,
}
// log output is either STDOUt or kept in a buffer to be printed on error.
var logWriter io.Writer
if builder.Verbose {
logWriter = os.Stdout
@ -48,24 +51,19 @@ func (builder *Builder) Build(path string) (image string, err error) {
logWriter = &bytes.Buffer{}
}
logger := logging.New(logWriter)
packClient, err := pack.NewClient(pack.WithLogger(logger))
// Client with a logger which is enabled if in Verbose mode.
packClient, err := pack.NewClient(pack.WithLogger(logging.New(logWriter)))
if err != nil {
return
}
packOpts := pack.BuildOptions{
AppPath: path,
Image: builder.Tag,
Builder: packBuilder,
}
err = packClient.Build(context.Background(), packOpts)
if err != nil {
// Build based using the given builder.
if err = packClient.Build(context.Background(), packOpts); err != nil {
// If the builder was not showing logs, embed the full logs in the error.
if !builder.Verbose {
err = fmt.Errorf("%v\noutput: %s\n", err, logWriter.(*bytes.Buffer).String())
}
}
return builder.Tag, err
return
}

402
client.go
View File

@ -1,79 +1,79 @@
package faas
import (
"errors"
"fmt"
"io"
"os"
)
const DefaultNamespace = "faas"
const (
DefaultNamespace = "faas"
DefaultRegistry = "docker.io"
DefaultRuntime = "go"
DefaultTrigger = "http"
DefaultMaxRecursion = 5 // when determining a name from path
)
// Client for a given Function.
// 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
initializer Initializer // Creates initial local function implementation
builder Builder // Builds a runnable image from function source
pusher Pusher // Pushes a built image to a registry
deployer Deployer // Deploys a Function
updater Updater // Updates a deployed Function
runner Runner // Runs the function locally
remover Remover // Removes remote services
lister Lister // Lists remote services
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
updater Updater // Updates a deployed Function
runner Runner // Runs the Function locally
remover Remover // Removes remote services
lister Lister // Lists remote services
describer Describer
dnsProvider DNSProvider // Provider of DNS services
domainSearchLimit int // max dirs to recurse up when deriving domain
templates string // path to extensible templates
repository string // default repo for OCI image tags
domainSearchLimit int // max recursion when deriving domain
progressListener ProgressListener // progress listener
}
// Initializer creates the initial/stub Function code on first create.
type Initializer interface {
// Initialize a Function of the given name, template configuration `
// (expected signature) using a context template.
Initialize(runtime, template, path string) error
}
// Builder of function source to runnable image.
// Builder of Function source to runnable image.
type Builder interface {
// Build a function project with source located at path.
// returns the image name built.
Build(path string) (image string, err error)
// Build a Function project with source located at path.
Build(Function) error
}
// Pusher of function image to a registry.
// Pusher of Function image to a registry.
type Pusher interface {
// Push the image of the service function.
Push(tag string) error
// Push the image of the Function.
Push(Function) error
}
// Deployer of function source to running status.
// Deployer of Function source to running status.
type Deployer interface {
// Deploy a service function of given name, using given backing image.
Deploy(name, image string) (address string, err error)
// Deploy a Function of given name, using given backing image.
Deploy(Function) error
}
// Updater of a deployed service function with new image.
// Updater of a deployed Function with new image.
type Updater interface {
// Deploy a service function of given name, using given backing image.
Update(name, image string) error
// Update a Function
Update(Function) error
}
// Runner runs the function locally.
// Runner runs the Function locally.
type Runner interface {
// Run the function locally.
Run(path string) error
// Run the Function locally.
Run(Function) error
}
// Remover of deployed services.
type Remover interface {
// Remove the service function from remote.
// Remove the Function from remote.
Remove(name string) error
}
// Lister of deployed services.
type Lister interface {
// List the service functions currently deployed.
// List the Functions currently deployed.
List() ([]string, error)
}
@ -93,10 +93,10 @@ type ProgressListener interface {
Done()
}
type Subscription struct {
Source string `json:"source" yaml:"source"`
Type string `json:"type" yaml:"type"`
Broker string `json:"broker" yaml:"broker"`
// Describer of Functions' remote deployed aspect.
type Describer interface {
// Describe the running state of the service as reported by the underlyng platform.
Describe(name string) (description FunctionDescription, err error)
}
type FunctionDescription struct {
@ -105,21 +105,22 @@ type FunctionDescription struct {
Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"`
}
type Describer interface {
Describe(name string) (description FunctionDescription, err error)
type Subscription struct {
Source string `json:"source" yaml:"source"`
Type string `json:"type" yaml:"type"`
Broker string `json:"broker" yaml:"broker"`
}
// DNSProvider exposes DNS services necessary for serving the Function.
type DNSProvider interface {
// Provide the given name by routing requests to address.
Provide(name, address string) (n string)
Provide(Function) error
}
// New client for Function management.
func New(options ...Option) (c *Client, err error) {
func New(options ...Option) *Client {
// Instantiate client with static defaults.
c = &Client{
initializer: &noopInitializer{output: os.Stdout},
c := &Client{
builder: &noopBuilder{output: os.Stdout},
pusher: &noopPusher{output: os.Stdout},
deployer: &noopDeployer{output: os.Stdout},
@ -129,17 +130,17 @@ func New(options ...Option) (c *Client, err error) {
lister: &noopLister{output: os.Stdout},
dnsProvider: &noopDNSProvider{output: os.Stdout},
progressListener: &noopProgressListener{},
domainSearchLimit: -1, // no recursion limit deriving domain by default.
domainSearchLimit: DefaultMaxRecursion, // no recursion limit deriving domain by default.
}
// Apply passed options, which take ultimate precidence.
for _, o := range options {
o(c)
}
return
return c
}
// Option defines a function which when passed to the Client constructor optionally
// Option defines a Function which when passed to the Client constructor optionally
// mutates private members at time of instantiation.
type Option func(*Client)
@ -157,21 +158,13 @@ func WithLocal(l bool) Option {
}
}
// WithInternal sets the internal (no public route) mode for deployed function.
// WithInternal sets the internal (no public route) mode for deployed Function.
func WithInternal(i bool) Option {
return func(c *Client) {
c.internal = i
}
}
// WithInitializer provides the concrete implementation of the Function
// initializer (generates stub code on initial create).
func WithInitializer(i Initializer) Option {
return func(c *Client) {
c.initializer = i
}
}
// WithBuilder provides the concrete implementation of a builder.
func WithBuilder(d Builder) Option {
return func(c *Client) {
@ -221,7 +214,7 @@ func WithLister(l Lister) Option {
}
}
// WithDescriber provides a concrete implementation of a function describer.
// WithDescriber provides a concrete implementation of a Function describer.
func WithDescriber(describer Describer) Option {
return func(c *Client) {
c.describer = describer
@ -254,45 +247,149 @@ func WithDomainSearchLimit(limit int) Option {
}
}
// Initialize creates a new function project locally
func (c *Client) Initialize(runtime, template, name, tag, root string) (f *Function, err error) {
// Create an instance of a function representation at the given root.
f, err = NewFunction(root)
// WithTemplates sets the location to use for extensible templates.
// Extensible templates are additional templates that exist on disk and are
// not built into the binary.
func WithTemplates(templates string) Option {
return func(c *Client) {
c.templates = templates
}
}
// WithRepository sets the default registry which is consulted when an image name/tag
// is not explocitly provided. Can be fully qualified, including the registry
// (ex: 'quay.io/myname') or simply the namespace 'myname' which indicates the
// the use of the default registry.
func WithRepository(repository string) Option {
return func(c *Client) {
c.repository = repository
}
}
// Initialize creates a new Function project locally using the settings
// provided on a Function object.
func (c *Client) Initialize(cfg Function) (err error) {
// Create Function of the given root path.
f, err := NewFunction(cfg.Root)
if err != nil {
return nil, err
return
}
// 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.
err = f.Initialize(runtime, template, name, tag, c.domainSearchLimit, c.initializer)
if err != nil {
return nil, err
// 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.
if err = assertEmptyRoot(f.Root); err != nil {
return
}
// Set the name to that provided, defaulting to path derivation if empty.
f.Name = cfg.Name
if cfg.Name == "" {
f.Name = pathToDomain(f.Root, c.domainSearchLimit)
if f.Name == "" {
err = errors.New("Function name must be deriveable from path or explicitly provided")
return
}
}
// Assert runtime was provided, or default.
f.Runtime = cfg.Runtime
if f.Runtime == "" {
f.Runtime = DefaultRuntime
} else {
f.Runtime = cfg.Runtime
}
// Assert trigger was provided, or default.
f.Trigger = cfg.Trigger
if f.Trigger == "" {
f.Trigger = DefaultTrigger
}
// Write out a template.
w := templateWriter{templates: c.templates, verbose: c.verbose}
if err = w.Write(f.Runtime, f.Trigger, f.Root); err != nil {
return
}
// Write out the config.
if err = writeConfig(f); err != nil {
return
}
// TODO: Create a status structure and return it for clients to use
// for output, such as from the CLI.
fmt.Printf("Created function project %v in %v\n", f.Name, root)
return f, nil
if c.verbose {
fmt.Printf("OK %v %v\n", f.Name, f.Root)
}
return
}
func (c *Client) Build(path string) (image string, err error) {
return c.builder.Build(path)
}
func (c *Client) Deploy(name, tag string) (address string, err error) {
err = c.pusher.Push(tag) // First push the image to an image registry
// Build the Function at path. Errors if the funciton is either unloadable or does
// not contain a populated Image.
func (c *Client) Build(path string) (err error) {
f, err := NewFunction(path)
if err != nil {
return
}
address, err = c.deployer.Deploy(name, tag)
return address, err
// Derive Image from the path (preceidence is given to extant config)
if f.Image, err = DerivedImage(path, c.repository); err != nil {
return
}
if err = c.builder.Build(f); err != nil {
return
}
// Write out config, which will now contain a populated image tag
// if it had not already
if err = writeConfig(f); err != nil {
return
}
// TODO: create a statu structure and return it here for optional
// use by the cli for user echo (rather than rely on verbose mode here)
if c.verbose {
fmt.Printf("OK %v\n", f.Image)
}
return
}
func (c *Client) Route(name, address string) (route string) {
// Deploy the Function at path. Errors if the Function has not been
// initialized with an image tag.
func (c *Client) Deploy(path string) (err error) {
f, err := NewFunction(path)
if err != nil {
return
}
if f.Image == "" {
return errors.New("Function needs to have Image tag calculated prior to building.")
}
err = c.pusher.Push(f) // First push the image to an image registry
if err != nil {
return
}
if err = c.deployer.Deploy(f); err != nil {
return
}
if c.verbose {
// TODO: aspirational. Should be an actual route echo.
fmt.Printf("OK https://%v/\n", f.Image)
}
return
}
func (c *Client) Route(path string) (err error) {
// Ensure that the allocated final address is enabled with the
// configured DNS provider.
// NOTE:
@ -300,74 +397,72 @@ func (c *Client) Route(name, address string) (route string) {
// but DNS subdomain CNAME to the Kourier Load Balancer is
// still manual, and the initial cluster config to suppot the TLD
// is still manual.
return c.dnsProvider.Provide(name, address)
f, err := NewFunction(path)
if err != nil {
return
}
return c.dnsProvider.Provide(f)
}
// Create a service function of the given runtime.
// 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(runtime, template, name, tag, root string) (err error) {
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
// 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")
f, err := c.Initialize(runtime, template, name, tag, root)
if f == nil || !f.Initialized() {
return fmt.Errorf("Unable to initialize function")
}
c.progressListener.Increment("Initializing new Function project")
err = c.Initialize(cfg)
if err != nil {
return err
return
}
// Build the now-initialized service function
// 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)
err = c.Build(f.Root)
if err != nil {
return
}
if c.local {
c.progressListener.Complete("Created function project (local only)")
return
}
// TODO: cluster-local deploy mode
// if c.internal {
// return errors.New("Deploying in cluster-internal mode (no public route) not yet available.")
// }
// Deploy the initialized service function, returning its publicly
// Deploy the initialized Function, returning its publicly
// addressible name for possible registration.
c.progressListener.Increment("Deploying function to cluster")
address, err := c.Deploy(f.Name, f.Tag)
if err != nil {
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")
c.Route(f.Name, address)
// 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: Create a status structure and return it for clients to use
// for output, such as from the CLI.
fmt.Printf("https://%v/\n", address)
// 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 service function.
// Update a previously created Function.
func (c *Client) Update(root string) (err error) {
// Create an instance of a function representation at the given root.
// Create an instance of a Function representation at the given root.
f, err := NewFunction(root)
if err != nil {
return
@ -378,26 +473,32 @@ func (c *Client) Update(root string) (err error) {
return fmt.Errorf("the given path '%v' does not contain an initialized Function. Please create one at this path before updating.", root)
}
// Build an image from the current state of the service function's implementation.
image, err := c.builder.Build(f.Root)
// Build an image from the current state of the Function's implementation.
err = c.Build(f.Root)
if err != nil {
return
}
// reload the Function as it will now have the Image populated if it had not yet been set.
f, err = NewFunction(f.Root)
if err != nil {
return
}
// Push the image for the named service to the configured registry
if err = c.pusher.Push(image); err != nil {
if err = c.pusher.Push(f); err != nil {
return
}
// Update the previously-deployed service function, returning its publicly
// Update the previously-deployed Function, returning its publicly
// addressible name for possible registration.
return c.updater.Update(f.Name, image)
return c.updater.Update(f)
}
// Run the function whose code resides at root.
// Run the Function whose code resides at root.
func (c *Client) Run(root string) error {
// Create an instance of a function representation at the given root.
// Create an instance of a Function representation at the given root.
f, err := NewFunction(root)
if err != nil {
return err
@ -409,20 +510,20 @@ func (c *Client) Run(root string) error {
}
// delegate to concrete implementation of runner entirely.
return c.runner.Run(f.Root)
return c.runner.Run(f)
}
// List currently deployed service functions.
// List currently deployed Functions.
func (c *Client) List() ([]string, error) {
// delegate to concrete implementation of lister entirely.
return c.lister.List()
}
// Describe a function. Name takes precidence. If no name is provided,
// the function defined at root is used.
// Describe a Function. Name takes precidence. If no name is provided,
// the Function defined at root is used.
func (c *Client) Describe(name, root string) (fd FunctionDescription, err error) {
// If name is provided, it takes precidence.
// Otherwise load the function defined at root.
// Otherwise load the Function defined at root.
if name != "" {
return c.describer.Describe(name)
}
@ -437,11 +538,11 @@ func (c *Client) Describe(name, root string) (fd FunctionDescription, err error)
return c.describer.Describe(f.Name)
}
// Remove a function. Name takes precidence. If no name is provided,
// the function defined at root is used.
// 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 {
// If name is provided, it takes precidence.
// Otherwise load the function deined at root.
// Otherwise load the Function deined at root.
if name != "" {
return c.remover.Remove(name)
}
@ -464,51 +565,44 @@ func (c *Client) Remove(name, root string) error {
// serve to keep the core logic here separate from the imperitive.
// -----------------------------------------------------
type noopInitializer struct{ output io.Writer }
func (n *noopInitializer) Initialize(runtime, template, root string) error {
fmt.Fprintln(n.output, "skipping initialize: client not initialized WithInitializer")
return nil
}
type noopBuilder struct{ output io.Writer }
func (n *noopBuilder) Build(path string) (image string, err error) {
func (n *noopBuilder) Build(_ Function) error {
fmt.Fprintln(n.output, "skipping build: client not initialized WithBuilder")
return "", nil
return nil
}
type noopPusher struct{ output io.Writer }
func (n *noopPusher) Push(tag string) error {
func (n *noopPusher) Push(_ Function) error {
fmt.Fprintln(n.output, "skipping push: client not initialized WithPusher")
return nil
}
type noopDeployer struct{ output io.Writer }
func (n *noopDeployer) Deploy(name, image string) (string, error) {
func (n *noopDeployer) Deploy(_ Function) error {
fmt.Fprintln(n.output, "skipping deploy: client not initialized WithDeployer")
return "", nil
return nil
}
type noopUpdater struct{ output io.Writer }
func (n *noopUpdater) Update(name, image string) error {
func (n *noopUpdater) Update(_ Function) error {
fmt.Fprintln(n.output, "skipping deploy: client not initialized WithDeployer")
return nil
}
type noopRunner struct{ output io.Writer }
func (n *noopRunner) Run(root string) error {
func (n *noopRunner) Run(_ Function) error {
fmt.Fprintln(n.output, "skipping run: client not initialized WithRunner")
return nil
}
type noopRemover struct{ output io.Writer }
func (n *noopRemover) Remove(name string) error {
func (n *noopRemover) Remove(string) error {
fmt.Fprintln(n.output, "skipping remove: client not initialized WithRemover")
return nil
}
@ -522,9 +616,9 @@ func (n *noopLister) List() ([]string, error) {
type noopDNSProvider struct{ output io.Writer }
func (n *noopDNSProvider) Provide(name, address string) string {
func (n *noopDNSProvider) Provide(_ Function) error {
// Note: at this time manual DNS provisioning required for name -> knative serving netowrk load-balancer
return ""
return nil
}
type noopProgressListener struct{}

View File

@ -42,7 +42,7 @@ func TestCreate(t *testing.T) {
t.Fatal(err)
}
// Create the test function root
// Create the test Function root
root := "testdata/example.com/admin"
err = os.MkdirAll(root, 0700)
if err != nil {
@ -56,12 +56,12 @@ func TestCreate(t *testing.T) {
}
}
// TestCreateUnderivableName ensures that attempting to create a new function
// TestCreateUnderivableName ensures that attempting to create a new Function
// when the name is underivable (and no explicit name is provided) generates
// an error.
func TestCreateUnderivableName(t *testing.T) {
// Create the test function root
// Create the test Function root
root := "testdata/example.com/admin"
err := os.MkdirAll(root, 0700)
if err != nil {
@ -131,7 +131,7 @@ func TestCreateDelegates(t *testing.T) {
deployer = mock.NewDeployer()
)
// Create the test function root
// Create the test Function root
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
@ -148,7 +148,7 @@ func TestCreateDelegates(t *testing.T) {
t.Fatal(err)
}
// Register function delegates on the mocks which validate assertions
// Register Function delegates on the mocks which validate assertions
// -------------
// The initializer should receive the name expected from the path,
@ -170,7 +170,7 @@ func TestCreateDelegates(t *testing.T) {
return nil
}
// The builder should be invoked with a path to a function project's source
// The builder should be invoked with a path to a Function project's source
// An example image name is returned.
builder.BuildFn = func(name2 string) (string, error) {
expectedPath, err := filepath.Abs(root)
@ -212,8 +212,8 @@ func TestCreateDelegates(t *testing.T) {
// Invocation
// -------------
// Invoke the creation, triggering the function delegates, and
// perform follow-up assertions that the functions were indeed invoked.
// Invoke the creation, triggering the Function delegates, and
// perform follow-up assertions that the Functions were indeed invoked.
if err := client.Create("go", "", name, image, root); err != nil {
t.Fatal(err)
}
@ -245,14 +245,14 @@ func TestCreateLocal(t *testing.T) {
dnsProvider = mock.NewDNSProvider()
)
// Create the test function root
// Create the test Function root
err := os.MkdirAll(root, 0700)
if err != nil {
panic(err)
}
defer os.RemoveAll(root)
// Create the test function root
// Create the test Function root
err = os.MkdirAll(root, 0700)
if err != nil {
panic(err)
@ -265,7 +265,7 @@ func TestCreateLocal(t *testing.T) {
faas.WithPusher(pusher), // pushes images to a registry
faas.WithDeployer(deployer), // deploys images as a running service
faas.WithDNSProvider(dnsProvider), // will receive the final value
faas.WithLocal(true), // set to local function mode.
faas.WithLocal(true), // set to local Function mode.
)
if err != nil {
t.Fatal(err)
@ -332,7 +332,7 @@ func TestCreateDomain(t *testing.T) {
// when calculating final domain. See the unit tests for pathToDomain for the
// details and edge cases of this caluclation.
func TestCreateSubdomain(t *testing.T) {
// Create the test function root
// Create the test Function root
root := "testdata/example.com/admin"
err := os.MkdirAll(root, 0700)
if err != nil {
@ -361,7 +361,7 @@ func TestCreateSubdomain(t *testing.T) {
// TestRun ensures that the runner is invoked with the absolute path requested.
func TestRun(t *testing.T) {
// a previously-initilized function's root
// a previously-initilized Function's root
root := "testdata/example.com/www"
runner := mock.NewRunner()
@ -406,10 +406,10 @@ func TestUpdate(t *testing.T) {
t.Fatal(err)
}
// Register function delegates on the mocks which validate assertions
// Register Function delegates on the mocks which validate assertions
// -------------
// The builder should be invoked with a path to the function source.
// The builder should be invoked with a path to the Function source.
// An example image name is returned.
builder.BuildFn = func(expectedPath string) (string, error) {
rootPath, err := filepath.Abs(root)
@ -449,8 +449,8 @@ func TestUpdate(t *testing.T) {
// Invocation
// -------------
// Invoke the creation, triggering the function delegates, and
// perform follow-up assertions that the functions were indeed invoked.
// Invoke the creation, triggering the Function delegates, and
// perform follow-up assertions that the Functions were indeed invoked.
if err := client.Update(root); err != nil {
t.Fatal(err)
}
@ -495,9 +495,9 @@ func TestRemove(t *testing.T) {
}
}
// TestRemoveUninitializedFailes ensures that attempting to remove a function
// TestRemoveUninitializedFailes ensures that attempting to remove a Function
// by path only (no name) fails unless the funciton has been initialized. I.e.
// the name will not be derived from path and the function removed by thi
// the name will not be derived from path and the Function removed by thi
// derived name; that could be unexpected and destructive.
func TestRemoveUninitializedFails(t *testing.T) {
var (
@ -512,7 +512,7 @@ func TestRemoveUninitializedFails(t *testing.T) {
// Create a remover delegate which fails if invoked.
remover.RemoveFn = func(name string) error {
return fmt.Errorf("remove invoked for unitialized function %v", name)
return fmt.Errorf("remove invoked for unitialized Function %v", name)
}
// Instantiate the client with the failing remover.
@ -530,12 +530,12 @@ func TestRemoveUninitializedFails(t *testing.T) {
// TestRemoveDefaultCurrent ensures that, if a name is not provided but a path is,
// the funciton defined by path will be removed.
// Note that the prior test RemoveUninitializedFails ensures that only
// initialized functions are removed, which prevents the case of a function
// initialized Functions are removed, which prevents the case of a Function
// being removed by path-derived name without having been initialized (an
// easily destrcutive and likely unwanted behavior)
func TestRemoveDefaultCurrent(t *testing.T) {
var (
root = "testdata/example.com/www" // an initialized function (has config)
root = "testdata/example.com/www" // an initialized Function (has config)
remover = mock.NewRemover()
)
@ -552,7 +552,7 @@ func TestRemoveDefaultCurrent(t *testing.T) {
t.Fatal(err)
}
// Without a name provided, but with a path, the function will be loaded and
// Without a name provided, but with a path, the Function will be loaded and
// its name used by default. Note that it will fail if uninitialized (no path
// derivation for removal.)
if err := client.Remove("", root); err != nil {
@ -562,7 +562,7 @@ func TestRemoveDefaultCurrent(t *testing.T) {
// TestWithName ensures that an explicitly passed name is used in leau of the
// path derived name when provided, and persists through instantiations.
// This also ensures that an initialized service function's name persists if
// This also ensures that an initialized Function's name persists if
// the path is changed after creation.
func TestWithName(t *testing.T) {
// Explicit name to use
@ -633,7 +633,7 @@ func TestList(t *testing.T) {
var lister = mock.NewLister()
client, err := faas.New(
faas.WithLister(lister)) // lists deployed service functions.
faas.WithLister(lister)) // lists deployed Functions.
if err != nil {
t.Fatal(err)
}
@ -648,10 +648,10 @@ func TestList(t *testing.T) {
}
}
// TestListOutsideRoot ensures that a call to a function (in this case list)
// that is not contextually dependent on being associated with a function,
// TestListOutsideRoot ensures that a call to a Function (in this case list)
// that is not contextually dependent on being associated with a Function,
// can be run from anywhere, thus ensuring that the client itself makes
// a distinction between function-scoped methods and not.
// a distinction between Function-scoped methods and not.
func TestListOutsideRoot(t *testing.T) {
var lister = mock.NewLister()

View File

@ -1,87 +1,97 @@
package cmd
import (
"os"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas"
"github.com/boson-project/faas/buildpacks"
"github.com/boson-project/faas/docker"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas/prompt"
)
var buildCmd = &cobra.Command{
Use: "build",
Short: "Build an existing function project as an OCI image",
SuggestFor: []string{"biuld", "buidl", "built", "image"},
// TODO: Add completions for build
// ValidArgsFunction: CompleteRuntimeList,
RunE: buildImage,
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
flags := []string{"path", "tag", "push"}
for _, f := range flags {
if err = viper.BindPFlag(f, cmd.Flags().Lookup(f)); err != nil {
return err
}
}
return
},
func init() {
root.AddCommand(buildCmd)
buildCmd.Flags().StringP("image", "i", "", "Optional full image name, in form [registry]/[namespace]/[name]:[tag] for example quay.io/myrepo/project.name:latest (overrides --repository) - $FAAS_IMAGE")
buildCmd.Flags().StringP("path", "p", cwd(), "Path to the Function project directory - $FAAS_PATH")
buildCmd.Flags().StringP("repository", "r", "", "Repository for built images, ex 'docker.io/myuser' or just 'myuser'. Optional if --image provided. - $FAAS_REPOSITORY")
buildCmd.Flags().BoolP("yes", "y", false, "When in interactive mode (attached to a TTY) skip prompts. - $FAAS_YES")
}
func init() {
cwd, err := os.Getwd()
if err != nil {
panic(err)
var buildCmd = &cobra.Command{
Use: "build [options]",
Short: "Build an existing Function project as an OCI image",
SuggestFor: []string{"biuld", "buidl", "built"},
PreRunE: bindEnv("image", "path", "repository", "yes"),
RunE: runBuild,
}
func runBuild(cmd *cobra.Command, _ []string) (err error) {
config := newBuildConfig().Prompt()
builder := buildpacks.NewBuilder()
builder.Verbose = config.Verbose
client := faas.New(
faas.WithVerbose(config.Verbose),
faas.WithRepository(config.Repository), // for deriving image name when --image not provided explicitly.
faas.WithBuilder(builder))
// overrideImage name for built images, if --image provided.
if err = overrideImage(config.Path, config.Image); err != nil {
return
}
root.AddCommand(buildCmd)
buildCmd.Flags().StringP("path", "p", cwd, "Path to the function project directory")
buildCmd.Flags().StringP("tag", "t", "", "Specify an image tag, for example quay.io/myrepo/project.name:latest")
return client.Build(config.Path)
}
type buildConfig struct {
// Image name in full, including registry, repo and tag (overrides
// image name derivation based on Repository and Function Name)
Image string
// Path of the Function implementation on local disk. Defaults to current
// working directory of the process.
Path string
// Push the resultnat image to the repository after building.
Push bool
// Repository at which interstitial build artifacts should be kept.
// Registry is optional and is defaulted to faas.DefaultRegistry.
// ex: "quay.io/myrepo" or "myrepo"
// This setting is ignored if Image is specified, which includes the full
Repository string
// Verbose logging.
Verbose bool
Path string
Tag string
Push bool
// Yes: agree to values arrived upon from environment plus flags plus defaults,
// and skip the interactive prompting (only applicable when attached to a TTY).
Yes bool
}
func buildImage(cmd *cobra.Command, args []string) (err error) {
var config = buildConfig{
Verbose: viper.GetBool("verbose"),
Path: viper.GetString("path"),
Tag: viper.GetString("tag"),
func newBuildConfig() buildConfig {
return buildConfig{
Image: viper.GetString("image"),
Path: viper.GetString("path"),
Repository: viper.GetString("repository"),
Verbose: viper.GetBool("verbose"), // defined on root
Yes: viper.GetBool("yes"),
}
return Build(config)
}
// Build will build a function project image and optionally
// push it to a remote registry
func Build(config buildConfig) (err error) {
f, err := faas.FunctionConfiguration(config.Path, config.Tag)
if err != nil {
return err
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
// all prompts) was explicitly set.
func (c buildConfig) Prompt() buildConfig {
if !interactiveTerminal() || c.Yes {
return c
}
builder := buildpacks.NewBuilder(f.Tag)
builder.Verbose = config.Verbose
var client *faas.Client
if config.Push {
client, err = faas.New(
faas.WithVerbose(config.Verbose),
faas.WithBuilder(builder),
faas.WithPusher(docker.NewPusher()),
)
} else {
client, err = faas.New(
faas.WithVerbose(config.Verbose),
faas.WithBuilder(builder),
)
return buildConfig{
Path: prompt.ForString("Path to project directory", c.Path),
Image: prompt.ForString("Resulting image name", deriveImage(c.Image, c.Repository, c.Path), prompt.WithRequired(true)),
Verbose: prompt.ForBool("Verbose logging", c.Verbose),
// Repository not prompted for as it would be confusing when combined with explicit image. Instead it is
// inferred by the derived default for Image, which uses Repository for derivation.
}
if err != nil {
return err
}
_, err = client.Build(f.Root)
return err // will be nil if no error
}

View File

@ -13,8 +13,8 @@ func init() {
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion",
Short: "Generates bash/zsh completion scripts",
Use: "completion <bash|zsh>",
Short: "Generate bash/zsh completion scripts",
Long: `To load completion run
For zsh:

View File

@ -37,7 +37,7 @@ func CompleteRuntimeList(cmd *cobra.Command, args []string, toComplete string) (
}
func CompleteOutputFormatList(cmd *cobra.Command, args []string, toComplete string) (strings []string, directive cobra.ShellCompDirective) {
directive = cobra.ShellCompDirectiveDefault
strings = []string{"yaml", "xml", "json"}
strings = []string{"plain", "yaml", "xml", "json"}
return
}

View File

@ -1,151 +1,124 @@
package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas"
"github.com/boson-project/faas/buildpacks"
"github.com/boson-project/faas/docker"
"github.com/boson-project/faas/embedded"
"github.com/boson-project/faas/knative"
"github.com/boson-project/faas/progress"
"github.com/boson-project/faas/prompt"
)
func init() {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
// Add the `create` command as a subcommand to root.
root.AddCommand(createCmd)
// createCmd.Flags().BoolP("internal", "i", false, "Create a cluster-local function without a publicly accessible route - $FAAS_INTERNAL")
createCmd.Flags().StringP("name", "n", "", "Specify an explicit name for the serive, overriding path-derivation - $FAAS_NAME")
createCmd.Flags().StringP("namespace", "s", "faas", "namespace in underlying platform within which to deploy the Function - $FAAS_NAMESPACE")
createCmd.Flags().StringP("path", "p", cwd, "Path to the new project directory")
createCmd.Flags().StringP("tag", "t", "", "Specify an image tag, for example quay.io/myrepo/project.name:latest")
createCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "faas", "templates"), "Extensible templates path. $FAAS_TEMPLATES")
createCmd.Flags().StringP("trigger", "g", embedded.DefaultTemplate, "Function template (ex: 'http','events') - $FAAS_TEMPLATE")
createCmd.Flags().BoolP("local", "l", false, "Create the function locally only. Do not push to a cluster")
createCmd.Flags().StringP("image", "i", "", "Optional full image name, in form [registry]/[namespace]/[name]:[tag] for example quay.io/myrepo/project.name:latest (overrides --repository) - $FAAS_IMAGE")
createCmd.Flags().StringP("namespace", "n", "", "Override namespace into which the funciton is deployed (on supported platforms). Default is to use currently active underlying platform setting - $FAAS_NAMESPACE")
createCmd.Flags().StringP("path", "p", cwd(), "Path to the new project directory - $FAAS_PATH")
createCmd.Flags().StringP("repository", "r", "", "Repository for built images, ex 'docker.io/myuser' or just 'myuser'. Optional if --image provided. - $FAAS_REPOSITORY")
createCmd.Flags().StringP("runtime", "l", faas.DefaultRuntime, "Function runtime language/framework. - $FAAS_RUNTIME")
createCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "faas", "templates"), "Extensible templates path. - $FAAS_TEMPLATES")
createCmd.Flags().StringP("trigger", "t", faas.DefaultTrigger, "Function trigger (ex: 'http','events') - $FAAS_TRIGGER")
createCmd.Flags().BoolP("yes", "y", false, "When in interactive mode (attached to a TTY) skip prompts. - $FAAS_YES")
err = createCmd.MarkFlagRequired("tag")
var err error
err = createCmd.RegisterFlagCompletionFunc("image", CompleteRegistryList)
if err != nil {
fmt.Println("Error while marking 'tag' required")
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
err = createCmd.RegisterFlagCompletionFunc("tag", CompleteRegistryList)
err = createCmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList)
if err != nil {
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
}
// The create command invokes the Funciton Client to create a new,
// functional, deployed service function with a noop implementation. It
// can be optionally created only locally (no deploy) using --local.
var createCmd = &cobra.Command{
Use: "create <runtime>",
Short: "Create a new Function project, build it, and push it to a cluster",
SuggestFor: []string{"cerate", "new"},
ValidArgsFunction: CompleteRuntimeList,
Args: cobra.ExactArgs(1),
RunE: create,
PreRun: func(cmd *cobra.Command, args []string) {
flags := []string{"local", "name", "namespace", "tag", "path", "trigger", "templates"}
for _, f := range flags {
if err := viper.BindPFlag(f, cmd.Flags().Lookup(f)); err != nil {
panic(err)
}
}
},
Use: "create <name> [options]",
Short: "Create a new Function, including initialization of local files and deployment.",
SuggestFor: []string{"cerate", "new"},
PreRunE: bindEnv("image", "namespace", "path", "repository", "runtime", "templates", "trigger", "yes"),
RunE: runCreate,
}
// The create command expects several parameters, most of which can be
// defaulted. When an interactive terminal is detected, these parameters,
// which are gathered into this config object, are passed through the shell
// allowing the user to interactively confirm and optionally modify values.
type createConfig struct {
initConfig
// Namespace on the cluster where the function will be deployed
Namespace string
func runCreate(cmd *cobra.Command, args []string) (err error) {
config := newCreateConfig(args).Prompt()
// Local mode flag only builds a function locally, with no deployed counterpart
Local bool
}
// create a new service function using the client about config.
func create(cmd *cobra.Command, args []string) (err error) {
// Assert a runtime parameter was provided
if len(args) == 0 {
return errors.New("'faas create' requires a runtime argument")
function := faas.Function{
Name: config.Name,
Root: config.initConfig.Path,
Runtime: config.initConfig.Runtime,
Trigger: config.Trigger,
}
// Create a deafult configuration populated first with environment variables,
// followed by overrides by flags.
var config = createConfig{
Namespace: viper.GetString("namespace"),
}
config.Verbose = viper.GetBool("verbose")
config.Local = viper.GetBool("local")
config.Name = viper.GetString("name")
config.Tag = viper.GetString("tag")
config.Trigger = viper.GetString("trigger")
config.Templates = viper.GetString("templates")
config.Path = viper.GetString("path")
config.Runtime = args[0]
builder := buildpacks.NewBuilder()
builder.Verbose = config.initConfig.Verbose
// If we are running as an interactive terminal, allow the user
// to mutate default config prior to execution.
if interactiveTerminal() {
config.initConfig, err = promptWithDefaults(config.initConfig)
if err != nil {
return err
}
}
// Initializer creates a deployable noop function implementation in the
// configured path.
initializer := embedded.NewInitializer(config.Templates)
initializer.Verbose = config.Verbose
// Builder creates images from function source.
builder := buildpacks.NewBuilder(config.Tag)
builder.Verbose = config.Verbose
// Pusher of images
pusher := docker.NewPusher()
pusher.Verbose = config.Verbose
pusher.Verbose = config.initConfig.Verbose
// Deployer of built images.
deployer := knative.NewDeployer()
deployer.Verbose = config.Verbose
deployer.Namespace = config.Namespace
deployer.Verbose = config.initConfig.Verbose
// Progress bar
listener := progress.New(progress.WithVerbose(config.Verbose))
listener := progress.New()
listener.Verbose = config.initConfig.Verbose
// Instantiate a client, specifying concrete implementations for
// Initializer and Deployer, as well as setting the optional verbosity param.
client, err := faas.New(
faas.WithVerbose(config.Verbose),
faas.WithInitializer(initializer),
client := faas.New(
faas.WithVerbose(config.initConfig.Verbose),
faas.WithTemplates(config.Templates),
faas.WithRepository(config.Repository), // for deriving image name when --image not provided explicitly.
faas.WithBuilder(builder),
faas.WithPusher(pusher),
faas.WithDeployer(deployer),
faas.WithLocal(config.Local),
faas.WithProgressListener(listener),
)
if err != nil {
faas.WithProgressListener(listener))
// overrideImage name for built images, if --image provided.
if err = overrideImage(config.initConfig.Path, config.Image); err != nil {
return
}
// Invoke the creation of the new Function locally.
// Returns the final address.
// Name can be empty string (path-dervation will be attempted)
// Path can be empty, defaulting to current working directory.
return client.Create(config.Runtime, config.Trigger, config.Name, config.Tag, config.Path)
return client.Create(function)
}
type createConfig struct {
initConfig
buildConfig
deployConfig
// Note that ambiguous references set to assume .initConfig
}
func newCreateConfig(args []string) createConfig {
return createConfig{
initConfig: newInitConfig(args),
buildConfig: newBuildConfig(),
deployConfig: newDeployConfig(),
}
}
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
// all prompts) was explicitly set.
func (c createConfig) Prompt() createConfig {
if !interactiveTerminal() || c.initConfig.Yes {
return c
}
return createConfig{
initConfig: initConfig{
Path: prompt.ForString("Path to project directory", c.initConfig.Path),
Name: prompt.ForString("Function project name", deriveName(c.Name, c.initConfig.Path), prompt.WithRequired(true)),
Verbose: prompt.ForBool("Verbose logging", c.initConfig.Verbose),
Runtime: prompt.ForString("Runtime of source", c.Runtime),
Trigger: prompt.ForString("Function Trigger", c.Trigger),
// Templates intentiopnally omitted from prompt for being an edge case.
},
buildConfig: buildConfig{
Repository: prompt.ForString("Repository for Function images", c.buildConfig.Repository),
},
deployConfig: deployConfig{
Namespace: prompt.ForString("Override default deploy namespace", c.Namespace),
},
}
}

View File

@ -1,57 +1,74 @@
package cmd
import (
"fmt"
"github.com/boson-project/faas"
"github.com/boson-project/faas/knative"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas"
"github.com/boson-project/faas/knative"
"github.com/boson-project/faas/prompt"
)
func init() {
root.AddCommand(deleteCmd)
deleteCmd.Flags().StringP("name", "n", "", "optionally specify an explicit name to remove, overriding path-derivation. $FAAS_NAME")
err := deleteCmd.RegisterFlagCompletionFunc("name", CompleteFunctionList)
if err != nil {
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
deleteCmd.Flags().StringP("path", "p", cwd(), "Path to the project which should be deleted - $FAAS_PATH")
deleteCmd.Flags().BoolP("yes", "y", false, "When in interactive mode (attached to a TTY), skip prompting the user. - $FAAS_YES")
}
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete deployed Function",
Long: `Removes the deployed Function for the current directory, but does not delete anything locally. If no code updates have been made beyond the defaults, this would bring the current codebase back to a state equivalent to having run "create --local".`,
SuggestFor: []string{"remove", "rm"},
RunE: delete,
PreRun: func(cmd *cobra.Command, args []string) {
err := viper.BindPFlag("name", cmd.Flags().Lookup("name"))
if err != nil {
panic(err)
}
},
Use: "delete <name>",
Short: "Delete a Function deployment",
Long: `Removes the deployed Function by name, by explicit path, or by default for the current directory. No local files are deleted.`,
SuggestFor: []string{"remove", "rm", "del"},
ValidArgsFunction: CompleteFunctionList,
PreRunE: bindEnv("path", "yes"),
RunE: runDelete,
}
func delete(cmd *cobra.Command, args []string) (err error) {
var (
verbose = viper.GetBool("verbose")
remover = knative.NewRemover()
name = viper.GetString("name") // Explicit name override (by default uses path argument)
path = "" // defaults to current working directory
)
remover.Verbose = verbose
// If provided use the path as the first argument.
if len(args) == 1 {
func runDelete(cmd *cobra.Command, args []string) (err error) {
config := newDeleteConfig(args).Prompt()
remover := knative.NewRemover()
remover.Verbose = config.Verbose
client := faas.New(
faas.WithVerbose(verbose),
faas.WithRemover(remover))
return client.Remove(config.Name, config.Path)
}
type deleteConfig struct {
Name string
Path string
Verbose bool
Yes bool
}
// newDeleteConfig returns a config populated from the current execution context
// (args, flags and environment variables)
func newDeleteConfig(args []string) deleteConfig {
var name string
if len(args) > 0 {
name = args[0]
}
client, err := faas.New(
faas.WithVerbose(verbose),
faas.WithRemover(remover),
)
if err != nil {
return
return deleteConfig{
Path: viper.GetString("path"),
Name: deriveName(name, viper.GetString("path")), // args[0] or derived
Verbose: viper.GetBool("verbose"), // defined on root
Yes: viper.GetBool("yes"),
}
}
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
// all prompts) was explicitly set.
func (c deleteConfig) Prompt() deleteConfig {
if !interactiveTerminal() || c.Yes {
return c
}
return deleteConfig{
// TODO: Path should be prompted for and set prior to name attempting path derivation. Test/fix this if necessary.
Name: prompt.ForString("Function to remove", deriveName(c.Name, c.Path), prompt.WithRequired(true)),
}
// Remove name (if provided), or the (initialized) function at path.
return client.Remove(name, path)
}

View File

@ -1,77 +1,97 @@
package cmd
import (
"os"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas"
"github.com/boson-project/faas/docker"
"github.com/boson-project/faas/knative"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas/prompt"
)
func init() {
root.AddCommand(deployCmd)
deployCmd.Flags().StringP("namespace", "n", "", "Override namespace into which the funciton is deployed (on supported platforms). Default is to use currently active underlying platform setting - $FAAS_NAMESPACE")
deployCmd.Flags().StringP("path", "p", cwd(), "Path to the function project directory - $FAAS_PATH")
deployCmd.Flags().BoolP("yes", "y", false, "When in interactive mode (attached to a TTY) skip prompts. - $FAAS_YES")
}
var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy an existing Function project to a cluster",
SuggestFor: []string{"delpoy", "deplyo"},
RunE: deployImage,
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
flags := []string{"build", "namespace", "path", "tag"}
for _, f := range flags {
if err = viper.BindPFlag(f, cmd.Flags().Lookup(f)); err != nil {
return err
}
}
return
},
PreRunE: bindEnv("namespace", "path", "yes"),
RunE: runDeploy,
}
func init() {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
func runDeploy(cmd *cobra.Command, _ []string) (err error) {
config := newDeployConfig().Prompt()
root.AddCommand(deployCmd)
deployCmd.Flags().BoolP("build", "b", false, "Build the image prior to deployment")
deployCmd.Flags().BoolP("expose", "e", true, "Create a publicly accessible route to the Function")
deployCmd.Flags().StringP("namespace", "s", "default", "Cluster namespace to deploy the Function in")
deployCmd.Flags().StringP("path", "p", cwd, "Path to the function project directory")
deployCmd.Flags().StringP("tag", "t", "", "Specify an image tag, for example quay.io/myrepo/project.name:latest")
}
func deployImage(cmd *cobra.Command, args []string) (err error) {
var config = buildConfig{
Verbose: viper.GetBool("verbose"),
Path: viper.GetString("path"),
Tag: viper.GetString("tag"),
Push: true,
}
f, err := faas.FunctionConfiguration(config.Path, config.Tag)
if err != nil {
return err
}
if viper.GetBool("build") {
if err = Build(config); err != nil {
return err
}
}
pusher := docker.NewPusher()
pusher.Verbose = config.Verbose
deployer := knative.NewDeployer()
deployer.Verbose = config.Verbose
deployer.Namespace = viper.GetString("namespace")
client, err := faas.New(
client := faas.New(
faas.WithVerbose(config.Verbose),
faas.WithDeployer(deployer),
faas.WithPusher(docker.NewPusher()),
)
if err != nil {
return err
faas.WithPusher(pusher),
faas.WithDeployer(deployer))
// overrieNamespace into which the function is deployed, if --namespace provided.
if err = overrideNamespace(config.Path, config.Namespace); err != nil {
return
}
return client.Deploy(config.Path)
// NOTE: Namespace is optional, default is that used by k8s client
// (for example kubectl usually uses ~/.kube/config)
}
type deployConfig struct {
// Namespace override for the deployed function. If provided, the
// underlying platform will be instructed to deploy the function to the given
// namespace (if such a setting is applicable; such as for Kubernetes
// clusters). If not provided, the currently configured namespace will be
// used. For instance, that which would be used by default by `kubectl`
// (~/.kube/config) in the case of Kubernetes.
Namespace string
// Path of the Function implementation on local disk. Defaults to current
// working directory of the process.
Path string
// Verbose logging.
Verbose bool
// Yes: agree to values arrived upon from environment plus flags plus defaults,
// and skip the interactive prompting (only applicable when attached to a TTY).
Yes bool
}
// newDeployConfig creates a buildConfig populated from command flags and
// environment variables; in that precedence.
func newDeployConfig() deployConfig {
return deployConfig{
Namespace: viper.GetString("namespace"),
Path: viper.GetString("path"),
Verbose: viper.GetBool("verbose"), // defined on root
Yes: viper.GetBool("yes"),
}
}
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
// all prompts) was explicitly set.
func (c deployConfig) Prompt() deployConfig {
if !interactiveTerminal() || c.Yes {
return c
}
return deployConfig{
Namespace: prompt.ForString("Override default namespace (optional)", c.Namespace),
Path: prompt.ForString("Path to project directory", c.Path),
Verbose: prompt.ForBool("Verbose logging", c.Verbose),
}
// TODO: Handle -e flag
_, err = client.Deploy(f.Name, f.Tag)
return err
}

View File

@ -3,102 +3,107 @@ package cmd
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"github.com/ory/viper"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"github.com/boson-project/faas"
"github.com/boson-project/faas/knative"
"github.com/ory/viper"
"github.com/spf13/cobra"
)
func init() {
root.AddCommand(describeCmd)
describeCmd.Flags().StringP("namespace", "n", "", "Override namespace in which to search for the Function. Default is to use currently active underlying platform setting - $FAAS_NAMESPACE")
describeCmd.Flags().StringP("output", "o", "yaml", "optionally specify output format (yaml,xml,json). - $FAAS_OUTPUT")
describeCmd.Flags().StringP("path", "p", cwd(), "Path to the project which should be described - $FAAS_PATH")
describeCmd.Flags().StringP("output", "o", "yaml", "optionally specify output format (yaml,xml,json).")
describeCmd.Flags().StringP("name", "n", "", "optionally specify an explicit name for the serive, overriding path-derivation. $FAAS_NAME")
err := describeCmd.RegisterFlagCompletionFunc("name", CompleteFunctionList)
if err != nil {
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
err = describeCmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList)
err := describeCmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList)
if err != nil {
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
}
var describeCmd = &cobra.Command{
Use: "describe",
Use: "describe <name> [options]",
Short: "Describe Function",
Long: `Describe Function`,
SuggestFor: []string{"desc"},
Long: `Describes the Function by name, by explicit path, or by default the current directory.`,
SuggestFor: []string{"desc", "get"},
ValidArgsFunction: CompleteFunctionList,
Args: cobra.ExactArgs(1),
RunE: describe,
PreRun: func(cmd *cobra.Command, args []string) {
err := viper.BindPFlag("output", cmd.Flags().Lookup("output"))
if err != nil {
panic(err)
}
err = viper.BindPFlag("name", cmd.Flags().Lookup("name"))
if err != nil {
panic(err)
}
},
PreRunE: bindEnv("namespace", "output", "path"),
RunE: runDescribe,
}
func describe(cmd *cobra.Command, args []string) (err error) {
var (
verbose = viper.GetBool("verbose")
format = viper.GetString("output")
name = viper.GetString("name")
path = "" // default to current working directory
)
// If provided use the path as the first argument
if len(args) == 1 {
name = args[0]
}
func runDescribe(cmd *cobra.Command, args []string) (err error) {
config := newDescribeConfig(args)
describer, err := knative.NewDescriber(faas.DefaultNamespace)
describer, err := knative.NewDescriber(config.Namespace)
if err != nil {
return
}
describer.Verbose = verbose
describer.Verbose = config.Verbose
client, err := faas.New(
client := faas.New(
faas.WithVerbose(verbose),
faas.WithDescriber(describer),
)
faas.WithDescriber(describer))
description, err := client.Describe(config.Name, config.Path)
if err != nil {
return
}
// describe the given name, or path if not provided.
description, err := client.Describe(name, path)
formatted, err := formatDescription(description, config.Output)
if err != nil {
return
}
formatFunctions := map[string]func(interface{}) ([]byte, error){
"json": json.Marshal,
"yaml": yaml.Marshal,
"xml": xml.Marshal,
}
formatFun, found := formatFunctions[format]
if !found {
return errors.New("unknown output format")
}
data, err := formatFun(description)
if err != nil {
return
}
fmt.Println(string(data))
fmt.Println(formatted)
return
}
// TODO: Placeholder. Create a fit-for-purpose Description plaintext formatter.
func fmtDescriptionPlain(i interface{}) ([]byte, error) {
return []byte(fmt.Sprintf("%v", i)), nil
}
// format the description as json|yaml|xml
func formatDescription(desc faas.FunctionDescription, format string) (string, error) {
formatters := map[string]func(interface{}) ([]byte, error){
"plain": fmtDescriptionPlain,
"json": json.Marshal,
"yaml": yaml.Marshal,
"xml": xml.Marshal,
}
formatFn, ok := formatters[format]
if !ok {
return "", fmt.Errorf("unknown format '%s'", format)
}
bytes, err := formatFn(desc)
if err != nil {
return "", err
}
return string(bytes), nil
}
type describeConfig struct {
Name string
Namespace string
Output string
Path string
Verbose bool
}
func newDescribeConfig(args []string) describeConfig {
var name string
if len(args) > 0 {
name = args[0]
}
return describeConfig{
Name: deriveName(name, viper.GetString("path")),
Namespace: viper.GetString("namespace"),
Output: viper.GetString("output"),
Path: viper.GetString("path"),
Verbose: viper.GetBool("verbose"),
}
}

View File

@ -1,71 +1,66 @@
package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/boson-project/faas"
"github.com/boson-project/faas/embedded"
"github.com/boson-project/faas/prompt"
"github.com/mitchellh/go-homedir"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas"
"github.com/boson-project/faas/prompt"
)
func init() {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
root.AddCommand(initCmd)
initCmd.Flags().StringP("name", "n", "", "A name for the project, overriding the default path based name")
initCmd.Flags().StringP("path", "p", cwd, "Path to the new project directory")
initCmd.Flags().StringP("tag", "t", "", "Specify an image tag, for example quay.io/myrepo/project.name:latest")
initCmd.Flags().StringP("trigger", "g", embedded.DefaultTemplate, "Function trigger (ex: 'http','events')")
initCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "faas", "templates"), "Extensible templates path")
if err = initCmd.MarkFlagRequired("tag"); err != nil {
fmt.Println("Error marking 'tag' flag required")
initCmd.Flags().StringP("path", "p", cwd(), "Path to the new project directory - $FAAS_PATH")
initCmd.Flags().StringP("runtime", "l", faas.DefaultRuntime, "Function runtime language/framework. - $FAAS_RUNTIME")
initCmd.Flags().StringP("templates", "", filepath.Join(configPath(), "faas", "templates"), "Extensible templates path. - $FAAS_TEMPLATES")
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 {
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
}
// The init command creates a new function project with a noop implementation.
var initCmd = &cobra.Command{
Use: "init <runtime> --tag=\"image tag\"",
Short: "Create a new function project",
Use: "init <name> [options]",
Short: "Initialize a new Function project",
SuggestFor: []string{"inti", "new"},
// TODO: Add completions for init
// ValidArgsFunction: CompleteRuntimeList,
RunE: initializeProject,
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
flags := []string{"name", "path", "tag", "trigger", "templates"}
for _, f := range flags {
err := viper.BindPFlag(f, cmd.Flags().Lookup(f))
if err != nil {
return err
}
}
return
},
PreRunE: bindEnv("path", "runtime", "templates", "trigger", "yes"),
RunE: runInit,
// TODO: autocomplate Functions for runtime and trigger.
}
// The init command expects a runtime language/framework, and optionally
// a couple of flags.
type initConfig struct {
// Verbose mode instructs the system to output detailed logs as the command
// progresses.
Verbose bool
func runInit(cmd *cobra.Command, args []string) error {
config := newInitConfig(args).Prompt()
function := faas.Function{
Name: config.Name,
Root: config.Path,
Runtime: config.Runtime,
Trigger: config.Trigger,
}
client := faas.New(
faas.WithVerbose(config.Verbose),
faas.WithTemplates(config.Templates))
return client.Initialize(function)
}
type initConfig struct {
// Name of the service in DNS-compatible format (ex myfunc.example.com)
Name string
// Trigger is the form of the resultant function, i.e. the function signature
// and contextually avaialable resources. For example 'http' for a funciton
// expected to be invoked via straight HTTP requests, or 'events' for a
// function which will be invoked with CloudEvents.
Trigger string
// Path to files on disk. Defaults to current working directory.
Path string
// Runtime language/framework.
Runtime string
// Templates is an optional path that, if it exists, will be used as a source
// for additional templates not included in the binary. If not provided
@ -73,106 +68,52 @@ type initConfig struct {
// location is $XDG_CONFIG_HOME/templates ($HOME/.config/faas/templates)
Templates string
// Runtime is the first argument, and specifies the resultant Function
// implementation runtime.
Runtime string
// Trigger is the form of the resultant Function, i.e. the Function signature
// and contextually avaialable resources. For example 'http' for a funciton
// expected to be invoked via straight HTTP requests, or 'events' for a
// Function which will be invoked with CloudEvents.
Trigger string
// Path of the Function implementation on local disk. Defaults to current
// working directory of the process.
Path string
// Verbose logging enabled.
Verbose bool
// Image tag for the resulting Function
Tag string
// Yes: agree to values arrived upon from environment plus flags plus defaults,
// and skip the interactive prompting (only applicable when attached to a TTY).
Yes bool
}
func initializeProject(cmd *cobra.Command, args []string) (err error) {
if len(args) == 0 {
return errors.New("'faas init' requires a runtime argument")
// newInitConfig returns a config populated from the current execution context
// (args, flags and environment variables)
func newInitConfig(args []string) initConfig {
var name string
if len(args) > 0 {
name = args[0] // If explicitly provided, use.
}
var config = initConfig{
Runtime: args[0],
Verbose: viper.GetBool("verbose"),
Name: viper.GetString("name"),
return initConfig{
Name: deriveName(name, viper.GetString("path")), // args[0] or derived
Path: viper.GetString("path"),
Trigger: viper.GetString("trigger"),
Runtime: viper.GetString("runtime"),
Templates: viper.GetString("templates"),
Tag: viper.GetString("tag"),
Trigger: viper.GetString("trigger"),
Verbose: viper.GetBool("verbose"), // defined on root
Yes: viper.GetBool("yes"),
}
// If we are running as an interactive terminal, allow the user
// to mutate default config prior to execution.
if interactiveTerminal() {
config, err = promptWithDefaults(config)
if err != nil {
return err
}
}
// Initializer creates a deployable noop function implementation in the
// configured path.
initializer := embedded.NewInitializer(config.Templates)
initializer.Verbose = config.Verbose
client, err := faas.New(
faas.WithVerbose(config.Verbose),
faas.WithInitializer(initializer),
)
if err != nil {
return err
}
// Invoke the creation of the new Function locally.
// Returns the final address.
// Name can be empty string (path-dervation will be attempted)
// Path can be empty, defaulting to current working directory.
_, err = client.Initialize(config.Runtime, config.Trigger, config.Name, config.Tag, config.Path)
// If no error this returns nil
return err
}
func promptWithDefaults(config initConfig) (c initConfig, err error) {
config.Path = prompt.ForString("Path to project directory", config.Path)
config.Name, err = promptForName("Function project name", config)
if err != nil {
return config, err
// Prompt the user with value of config members, allowing for interaractive changes.
// Skipped if not in an interactive terminal (non-TTY), or if --yes (agree to
// all prompts) was explicitly set.
func (c initConfig) Prompt() initConfig {
if !interactiveTerminal() || c.Yes {
return c
}
return initConfig{
// TODO: Path should be prompted for and set prior to name attempting path derivation. Test/fix this if necessary.
Path: prompt.ForString("Path to project directory", c.Path),
Name: prompt.ForString("Function project name", deriveName(c.Name, c.Path), prompt.WithRequired(true)),
Verbose: prompt.ForBool("Verbose logging", c.Verbose),
Runtime: prompt.ForString("Runtime of source", c.Runtime),
Trigger: prompt.ForString("Function Trigger", c.Trigger),
// Templates intentiopnally omitted from prompt for being an edge case.
}
config.Runtime = prompt.ForString("Runtime of source", config.Runtime)
config.Trigger = prompt.ForString("Function Template", config.Trigger)
config.Tag = prompt.ForString("Image tag", config.Tag)
return config, nil
}
// Prompting for Name with Default
// Early calclation of service function name is required to provide a sensible
// default. If the user did not provide a --name parameter or FAAS_NAME,
// this funciton sets the default to the value that the client would have done
// on its own if non-interactive: by creating a new function rooted at config.Path
// and then calculate from that path.
func promptForName(label string, config initConfig) (string, error) {
// Pre-calculate the function name derived from path
if config.Name == "" {
f, err := faas.NewFunction(config.Path)
if err != nil {
return "", err
}
maxRecursion := 5 // TODO synchronize with that used in actual initialize step.
return prompt.ForString("Name of service function", f.DerivedName(maxRecursion), prompt.WithRequired(true)), nil
}
// The user provided a --name or FAAS_NAME; just confirm it.
return prompt.ForString("Name of service function", config.Name, prompt.WithRequired(true)), nil
}
func configPath() (path string) {
if path = os.Getenv("XDG_CONFIG_HOME"); path != "" {
return
}
path, err := homedir.Expand("~/.config")
if err != nil {
fmt.Fprintf(os.Stderr, "could not derive home directory for use as default templates path: %v", err)
}
return
}

View File

@ -2,43 +2,26 @@ package cmd
import (
"encoding/json"
"encoding/xml"
"fmt"
"gopkg.in/yaml.v2"
"io"
"os"
"github.com/ory/viper"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"github.com/boson-project/faas"
"github.com/boson-project/faas/knative"
"github.com/spf13/cobra"
)
var formats = map[string]fmtFn{
"plain": fmtPlain,
"json": fmtJSON,
"yaml": fmtYAML,
}
var validFormats []string
func completeFormats(cmd *cobra.Command, args []string, toComplete string) (formats []string, directive cobra.ShellCompDirective) {
formats = validFormats
directive = cobra.ShellCompDirectiveDefault
return
}
func init() {
root.AddCommand(listCmd)
validFormats = make([]string, 0, len(formats))
for name := range formats {
validFormats = append(validFormats, name)
}
listCmd.Flags().StringP("namespace", "s", "", "cluster namespace to list functions from")
listCmd.Flags().StringP("namespace", "n", "", "Override namespace in which to search for Functions. Default is to use currently active underlying platform setting - $FAAS_NAMESPACE")
listCmd.Flags().StringP("output", "o", "plain", "optionally specify output format (plain,json,yaml)")
err := listCmd.RegisterFlagCompletionFunc("output", completeFormats)
err := listCmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList)
if err != nil {
fmt.Fprintln(os.Stderr, err)
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
}
@ -46,8 +29,83 @@ var listCmd = &cobra.Command{
Use: "list",
Short: "Lists deployed Functions",
Long: `Lists deployed Functions`,
SuggestFor: []string{"ls"},
RunE: list,
SuggestFor: []string{"ls", "lsit"},
PreRunE: bindEnv("namespace", "output"),
RunE: runList,
}
func runList(cmd *cobra.Command, args []string) (err error) {
config := newListConfig()
lister, err := knative.NewLister(config.Namespace)
if err != nil {
return
}
lister.Verbose = config.Verbose
client := faas.New(
faas.WithVerbose(verbose),
faas.WithLister(lister))
names, err := client.List()
if err != nil {
return
}
formatted, err := formatNames(names, config.Output)
if err != nil {
return
}
fmt.Println(formatted)
return
}
// TODO: placeholder. Create a fit-for-purpose Names plaintext formatter
func fmtNamesPlain(i interface{}) ([]byte, error) {
return []byte(fmt.Sprintf("%v", i)), nil
}
func formatNames(names []string, format string) (string, error) {
formatters := map[string]func(interface{}) ([]byte, error){
"plain": fmtNamesPlain,
"json": json.Marshal,
"yaml": yaml.Marshal,
"xml": xml.Marshal,
}
formatFn, ok := formatters[format]
if !ok {
return "", fmt.Errorf("Unknown format '%v'", format)
}
bytes, err := formatFn(names)
if err != nil {
return "", err
}
return string(bytes), nil
}
type listConfig struct {
Namespace string
Output string
Verbose bool
}
func newListConfig() listConfig {
return listConfig{
Namespace: viper.GetString("namespace"),
Output: viper.GetString("output"),
Verbose: viper.GetBool("verbose"),
}
}
// DEPRECATED BELOW (?):
var validFormats []string
func completeFormats(cmd *cobra.Command, args []string, toComplete string) (formats []string, directive cobra.ShellCompDirective) {
formats = validFormats
directive = cobra.ShellCompDirectiveDefault
return
}
type fmtFn func(writer io.Writer, names []string) error
@ -71,47 +129,3 @@ func fmtYAML(writer io.Writer, names []string) error {
encoder := yaml.NewEncoder(writer)
return encoder.Encode(names)
}
func list(cmd *cobra.Command, args []string) (err error) {
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return
}
format, err := cmd.Flags().GetString("output")
if err != nil {
return
}
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
return
}
lister, err := knative.NewLister(namespace)
if err != nil {
return
}
lister.Verbose = verbose
client, err := faas.New(
faas.WithVerbose(verbose),
faas.WithLister(lister),
)
if err != nil {
return
}
names, err := client.List()
if err != nil {
return
}
fmtFn, ok := formats[format]
if !ok {
return fmt.Errorf("invalid format name: %s", format)
}
return fmtFn(os.Stdout, names)
}

View File

@ -4,8 +4,11 @@ import (
"fmt"
"os"
"github.com/mitchellh/go-homedir"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas"
)
var (
@ -58,7 +61,7 @@ func init() {
// Environment Variables, command arguments and flags.
func Execute() {
// Sets version to a string partially populated by compile-time flags.
root.Version = version(verbose)
root.Version = version.String()
// Execute the root of the command tree.
if err := root.Execute(); err != nil {
@ -68,6 +71,9 @@ func Execute() {
}
}
// Helper functions used by multiple commands
// ------------------------------------------
// interactiveTerminal returns whether or not the currently attached process
// terminal is interactive. Used for determining whether or not to
// interactively prompt the user to confirm default choices, etc.
@ -75,3 +81,128 @@ func interactiveTerminal() bool {
fi, err := os.Stdin.Stat()
return err == nil && ((fi.Mode() & os.ModeCharDevice) != 0)
}
// cwd returns the current working directory or exits 1 printing the error.
func cwd() (cwd string) {
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to determine current working directory: %v", err)
os.Exit(1)
}
return cwd
}
// configPath is the effective path to the optional config directory used for
// function defaults and extensible templates.
func configPath() (path string) {
if path = os.Getenv("XDG_CONFIG_HOME"); path != "" {
return
}
path, err := homedir.Expand("~/.config")
if err != nil {
fmt.Fprintf(os.Stderr, "could not derive home directory for use as default templates path: %v", err)
}
return
}
// bindFunc which conforms to the cobra PreRunE method signature
type bindFunc func(*cobra.Command, []string) error
// bindEnv returns a bindFunc that binds env vars to the namd flags.
func bindEnv(flags ...string) bindFunc {
return func(cmd *cobra.Command, args []string) (err error) {
for _, flag := range flags {
if err = viper.BindPFlag(flag, cmd.Flags().Lookup(flag)); err != nil {
return
}
}
return
}
}
// overrideImage overwrites (or sets) the value of the Function's .Image
// property, which preempts the default funcitonality of deriving the value as:
// Deafult: [config.Repository]/[config.Name]:latest
func overrideImage(root, override string) (err error) {
if override == "" {
return
}
f, err := faas.NewFunction(root)
if err != nil {
return err
}
f.Image = override
return f.WriteConfig()
}
// overrideNamespace overwrites (or sets) the value of the Function's .Namespace
// property, which preempts the default functionality of using the underlying
// platform configuration (if supported). In the case of Kubernetes, this
// overrides the configured namespace (usually) set in ~/.kube.config.
func overrideNamespace(root, override string) (err error) {
if override == "" {
return
}
f, err := faas.NewFunction(root)
if err != nil {
return err
}
f.Namespace = override
return f.WriteConfig()
}
// deriveName returns the explicit value (if provided) or attempts to derive
// from the given path. Path is defaulted to current working directory, where
// a function configuration, if it exists and contains a name, is used. Lastly
// derivation using the path us used.
func deriveName(explicitName string, path string) string {
// If the name was explicitly provided, use it.
if explicitName != "" {
return explicitName
}
// If the directory at path contains an initialized funciton, use the name therein
f, err := faas.NewFunction(path)
if err == nil && f.Name != "" {
return f.Name
}
maxRecursion := faas.DefaultMaxRecursion
derivedName, _ := faas.DerivedName(path, maxRecursion)
return derivedName
}
// deriveImage returns the same image name which will be used if no explicit
// image is provided. I.e. derived from the configured repository (registry
// plus username) and the Function's name.
//
// This is calculated preemptively here in the CLI (prior to invoking the
// client), only in order to provide information to the user via the prompt.
// The client will calculate this same value if the image override is not
// provided.
//
// Derivation logic:
// deriveImage attempts to arrive at a final, full image name:
// format: [registry]/[username]/[FunctionName]:[tag]
// example: quay.io/myname/my.function.name:tag.
//
// Registry can optionally be omitted, in which case DefaultRegistry
// will be prepended.
//
// If the image flag is provided, this value is used directly (the user supplied
// --image or $FAAS_IMAGE). Otherwise, the Function at 'path' is loaded, and
// the Image name therein is used (i.e. it was previously calculated).
// Finally, the default repository is used, which is prepended to the Function
// name, and appended with ':latest':
func deriveImage(explicitImage, defaultRepo, path string) string {
if explicitImage != "" {
return explicitImage // use the explicit value provided.
}
f, err := faas.NewFunction(path)
if err != nil {
return "" // unable to derive due to load error (uninitialized?)
}
if f.Image != "" {
return f.Image // use value previously provided or derived.
}
derivedValue, _ := faas.DerivedImage(path, defaultRepo)
return derivedValue // Use the faas system's derivation logic.
}

View File

@ -16,7 +16,7 @@ func init() {
var runCmd = &cobra.Command{
Use: "run",
Short: "Run Function locally",
Long: "Runs the function locally within an isolated environment. Modifications to the function trigger a reload. This holds open the current window with the logs from the running function, and the run is canceled on interrupt.",
Long: "Runs the function locally within an isolated environment. Modifications to the Function trigger a reload. This holds open the current window with the logs from the running Function, and the run is canceled on interrupt.",
RunE: run,
}
@ -33,12 +33,9 @@ func run(cmd *cobra.Command, args []string) (err error) {
runner := appsody.NewRunner()
runner.Verbose = verbose
client, err := faas.New(
client := faas.New(
faas.WithRunner(runner),
faas.WithVerbose(verbose))
if err != nil {
return
}
return client.Run(path)
}

View File

@ -1,88 +1,88 @@
package cmd
import (
"errors"
"fmt"
"github.com/ory/viper"
"github.com/spf13/cobra"
"github.com/boson-project/faas"
"github.com/boson-project/faas/buildpacks"
"github.com/boson-project/faas/docker"
"github.com/boson-project/faas/knative"
)
func init() {
root.AddCommand(updateCmd)
updateCmd.Flags().StringP("registry", "r", "quay.io", "image registry (ex: quay.io). $FAAS_REGISTRY")
updateCmd.Flags().StringP("namespace", "s", "", "namespace at image registry (usually username or org name). $FAAS_NAMESPACE")
err := updateCmd.RegisterFlagCompletionFunc("registry", CompleteRegistryList)
if err != nil {
fmt.Println("Error while calling RegisterFlagCompletionFunc: ", err)
}
updateCmd.Flags().StringP("namespace", "n", "", "Override namespace for the Function (on supported platforms). Default is to use currently active underlying platform setting - $FAAS_NAMESPACE")
updateCmd.Flags().StringP("path", "p", cwd(), "Path to the Function project directory - $FAAS_PATH")
updateCmd.Flags().StringP("repository", "r", "", "Repository for built images, ex 'docker.io/myuser' or just 'myuser'. - $FAAS_REPOSITORY")
}
var updateCmd = &cobra.Command{
Use: "update",
Use: "update [options]",
Short: "Update or create a deployed Function",
Long: `Update deployed function to match the current local state.`,
Long: `Update deployed Function to match the current local state.`,
SuggestFor: []string{"push", "deploy"},
RunE: update,
PreRun: func(cmd *cobra.Command, args []string) {
err := viper.BindPFlag("registry", cmd.Flags().Lookup("registry"))
if err != nil {
panic(err)
}
err = viper.BindPFlag("namespace", cmd.Flags().Lookup("namespace"))
if err != nil {
panic(err)
}
},
PreRunE: bindEnv("namespace", "path", "repository"),
RunE: runUpdate,
}
func update(cmd *cobra.Command, args []string) (err error) {
var (
path = "" // defaults to current working directory
verbose = viper.GetBool("verbose") // Verbose logging
registry = viper.GetString("registry") // Registry (ex: docker.io)
namespace = viper.GetString("namespace") // namespace at registry (user or org name)
)
func runUpdate(cmd *cobra.Command, args []string) (err error) {
config := newUpdateConfig()
if len(args) == 1 {
path = args[0]
}
// Namespace can not be defaulted.
if namespace == "" {
return errors.New("image registry namespace (--namespace or FAAS_NAMESPACE is required)")
}
// Builder creates images from function source.
// TODO: FIX ME - param should be an image tag
builder := buildpacks.NewBuilder(registry)
builder := buildpacks.NewBuilder()
builder.Verbose = verbose
// Pusher of images
// pusher := docker.NewPusher()
// pusher.Verbose = verbose
pusher := docker.NewPusher()
pusher.Verbose = config.Verbose
// Deployer of built images.
updater, err := knative.NewUpdater(faas.DefaultNamespace)
if err != nil {
return fmt.Errorf("couldn't create updater: %v", err)
}
updater.Verbose = verbose
client, err := faas.New(
faas.WithVerbose(verbose),
faas.WithBuilder(builder),
// TODO: FIX ME
// faas.WithPusher(pusher),
faas.WithUpdater(updater),
)
updater, err := knative.NewUpdater(config.Namespace)
if err != nil {
return
}
updater.Verbose = verbose
return client.Update(path)
client := faas.New(
faas.WithVerbose(verbose),
faas.WithBuilder(builder),
faas.WithPusher(pusher),
faas.WithUpdater(updater))
// overrieNamespace to which the Function is pinned (deployed/updated etc)
if err = overrideNamespace(config.Path, config.Namespace); err != nil {
return
}
return client.Update(config.Path)
}
type updateConfig struct {
// Namespace override for the deployed Function. If provided, the
// underlying platform will be instructed to deploy the Function to the given
// namespace (if such a setting is applicable; such as for Kubernetes
// clusters). If not provided, the currently configured namespace will be
// used. For instance, that which would be used by default by `kubectl`
// (~/.kube/config) in the case of Kubernetes.
Namespace string
// Path of the Function implementation on local disk. Defaults to current
// working directory of the process.
Path string
// Repository at which interstitial build artifacts should be kept.
// Registry is optional and is defaulted to faas.DefaultRegistry.
// ex: "quay.io/myrepo" or "myrepo"
// This setting is ignored if Image is specified, which includes the full
Repository string
// Verbose logging.
Verbose bool
}
func newUpdateConfig() updateConfig {
return updateConfig{
Namespace: viper.GetString("namespace"),
Path: viper.GetString("path"),
Repository: viper.GetString("repository"),
Verbose: viper.GetBool("verbose"), // defined on root
}
}

View File

@ -8,63 +8,78 @@ import (
"github.com/spf13/cobra"
)
// metadata about the build process/binary etc.
// Not populated if building from source with go build.
// Set by the `make` targets.
var version = Version{}
// SetMeta is called by `main` with any provided build metadata
func SetMeta(date, vers, hash string) {
version.Date = date // build timestamp
version.Vers = vers // version tag
version.Hash = hash // git commit hash
}
func init() {
root.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version. With --verbose the build date stamp and commit hash are included if available.",
Run: printVersion,
Use: "version",
Short: "Print version. With --verbose the build date stamp and commit hash are included if available.",
SuggestFor: []string{"vers", "verison"},
Run: runVersion,
}
func printVersion(cmd *cobra.Command, args []string) {
fmt.Println(version(viper.GetBool("verbose")))
func runVersion(cmd *cobra.Command, args []string) {
// update version with the value of the (global) flag 'verbose'
version.Verbose = viper.GetBool("verbose")
// version is the metadata, serialized.
fmt.Println(version)
}
// Populated at build time by `make build`, plumbed through
// main using SetMeta()
var (
date string // datestamp
vers string // version of git commit or `tip`
hash string // git hash built from
)
// SetMeta from the build process, used for verbose version tagging.
func SetMeta(buildTimestamp, commitVersionTag, commitHash string) {
date = buildTimestamp
vers = commitVersionTag
hash = commitHash
// versionMetadata is set by the main package.
// When compiled from source, they remain the zero value.
// When compiled via `make`, they are initialized to the noted values.
type Version struct {
// Date of compilation
Date string
// Version tag of the git commit, or 'tip' if no tag.
Vers string
// Hash of the currently active git commit on build.
Hash string
// Verbose printing enabled for the string representation.
Verbose bool
}
// return the version, optionally with verbose details as the suffix
func version(verbose bool) string {
func (v Version) String() string {
// If 'vers' is not a semver already, then the binary was built either
// from an untagged git commit (set semver to v0.0.0), or was built
// directly from source (set semver to v0.0.0-source).
if strings.HasPrefix(vers, "v") {
if strings.HasPrefix(v.Vers, "v") {
// Was built via make with a tagged commit
if verbose {
return fmt.Sprintf("%s-%s-%s", vers, hash, date)
return fmt.Sprintf("%s-%s-%s", v.Vers, v.Hash, v.Date)
} else {
return vers
return v.Vers
}
} else if vers == "tip" {
} else if v.Vers == "tip" {
// Was built via make from an untagged commit
vers = "v0.0.0"
v.Vers = "v0.0.0"
if verbose {
return fmt.Sprintf("%s-%s-%s", vers, hash, date)
return fmt.Sprintf("%s-%s-%s", v.Vers, v.Hash, v.Date)
} else {
return vers
return v.Vers
}
} else {
// Was likely built from source
vers = "v0.0.0"
hash = "source"
v.Vers = "v0.0.0"
v.Hash = "source"
if verbose {
return fmt.Sprintf("%s-%s", vers, hash)
return fmt.Sprintf("%s-%s", v.Vers, v.Hash)
} else {
return vers
return v.Vers
}
}
}

124
config.go
View File

@ -8,86 +8,66 @@ import (
"gopkg.in/yaml.v2"
)
// ConfigFileName is an optional file checked for in the function root.
const ConfigFileName = ".faas.yaml"
// ConfigFileName is the name of the config's serialized form.
const ConfigFileName = ".faas.config"
// Config object which provides another mechanism for overriding client static
// defaults. Applied prior to the WithX options, such that the options take
// precedence if they are provided.
// Config represents the serialized state of a Function's metadata.
// See the Function struct for attribute documentation.
type Config struct {
// Name specifies the name to be used for this function. As a config option,
// this value, if provided, takes precidence over the path-derived name but
// not over the Option WithName, if provided.
Name string `yaml:"name"`
// Runtime of the implementation.
Runtime string `yaml:"runtime"`
// OCI image tag for the function
// typically of the form "registry/namespace/repository:tag"
Tag string `yaml:"tag"`
// Add new values to the applyConfig function as necessary.
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
Runtime string `yaml:"runtime"`
Image string `yaml:"image"`
// Add new values to the toConfig/fromConfig functions.
}
// newConfig creates a config object from a function, effectively exporting mutable
// fields for the config file while preserving the immutability of the client
// post-instantiation.
func newConfig(f *Function) Config {
return Config{
Name: f.Name,
Runtime: f.Runtime,
Tag: f.Tag,
// newConfig returns a Config populated from data serialized to disk if it is
// available. Errors are returned if the path is not valid, if there are
// 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)
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
}
}
// writeConfig out to disk.
func writeConfig(f *Function) (err error) {
var (
cfg = newConfig(f)
cfgFile = filepath.Join(f.Root, ConfigFileName)
bb []byte
)
if bb, err = yaml.Marshal(&cfg); err != nil {
bb, err := ioutil.ReadFile(filename)
if err != nil {
return
}
return ioutil.WriteFile(cfgFile, bb, 0644)
err = yaml.Unmarshal(bb, &c)
return
}
// Apply the config, if it exists, to the function struct.
// if an entry exists in the config file and is empty, this is interpreted as
// the intent to zero-value that field.
func applyConfig(f *Function, root string) error {
// abort if the config file does not exist.
filename := filepath.Join(root, ConfigFileName)
if _, err := os.Stat(filename); os.IsNotExist(err) {
return nil
// 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) {
return Function{
Name: c.Name,
Namespace: c.Namespace,
Runtime: c.Runtime,
Image: c.Image,
}
// Read in as bytes
bb, err := ioutil.ReadFile(filepath.Join(root, ConfigFileName))
if err != nil {
return err
}
// Create a config with defaults set to the current value of the Client object.
// These gymnastics are necessary because we want the Client's members to be
// private to disallow mutation post instantiation, and thus they are unavailable
// to be set automatically
cfg := newConfig(f)
// Decode yaml, overriding values in the config if they were defined in the yaml.
if err := yaml.Unmarshal(bb, &cfg); err != nil {
return err
}
// Apply the config to the client object, which effectiely writes back the default
// if it was not defined in the yaml.
f.Name = cfg.Name
f.Runtime = cfg.Runtime
f.Tag = cfg.Tag
// NOTE: cleverness < clarity
return nil
}
// toConfig serializes a Function to a config object.
func toConfig(f Function) Config {
return Config{
Name: f.Name,
Namespace: f.Namespace,
Runtime: f.Runtime,
Image: f.Image,
}
}
// writeConfig for the given Function out to disk at root.
func writeConfig(f Function) (err error) {
path := filepath.Join(f.Root, ConfigFileName)
c := toConfig(f)
bb := []byte{}
if bb, err = yaml.Marshal(&c); err != nil {
return
}
return ioutil.WriteFile(path, bb, 0644)
}

View File

@ -6,6 +6,8 @@ import (
"fmt"
"os"
"os/exec"
"github.com/boson-project/faas"
)
// Pusher of images from local to remote registry.
@ -19,8 +21,8 @@ func NewPusher() *Pusher {
return &Pusher{}
}
// Push an image by name. Docker is expected to be already authenticated.
func (n *Pusher) Push(tag string) (err error) {
// Push the image of the Function.
func (n *Pusher) Push(f faas.Function) (err error) {
// Check for the docker binary explicitly so that we can return
// an extra-friendly error message.
_, err = exec.LookPath("docker")
@ -29,9 +31,13 @@ func (n *Pusher) Push(tag string) (err error) {
return
}
if f.Image == "" {
return errors.New("Function has no associated image. Has it been built?")
}
// set up the command, specifying a sanitized project name and connecting
// standard output and error.
cmd := exec.Command("docker", "push", tag)
cmd := exec.Command("docker", "push", f.Image)
// If verbose logging is enabled, echo appsody's chatty stdout.
if n.Verbose {

View File

@ -12,132 +12,158 @@ import (
)
type Function struct {
Root string
Runtime string // will be empty unless initialized/until initialized
Name string // will be empty unless initialized/until initialized
Tag string // will be empty unless initialized/until initialized
// Root on disk at which to find/create source and config files.
Root string
// Name of the Function. If not provided, path derivation is attempted when
// requried (such as for initialization).
Name string
// Namespace into which the Function is deployed on supported platforms.
Namespace string
// Runtime is the language plus context. nodejs|go|quarkus|rust etc.
Runtime string
// Trigger of the Function. http|events etc.
Trigger string
// Repository at which to store interstitial containers, in the form
// [registry]/[user]. If omitted, "Image" must be provided.
Repo string
// Optional full OCI image tag in form:
// [registry]/[namespace]/[name]:[tag]
// example:
// quay.io/alice/my.function.name
// Registry is optional and is defaulted to DefaultRegistry
// example:
// alice/my.function.name
// If Image is provided, it overrides the default of concatenating
// "Repo+Name:latest" to derive the Image.
Image string
}
func NewFunction(root string) (f *Function, err error) {
f = &Function{}
// Default root to current directory, as an absolute path.
if root == "" {
root = "."
}
// NewFunction loads a Function from a path on disk. use .Initialized() to determine if
// the path contained an initialized Function.
// NewFunction creates a funciton struct whose attributes are loaded from the
// configuraiton located at path.
func NewFunction(root string) (f Function, err error) {
// Expand the passed root to its absolute path (default current dir)
if root, err = filepath.Abs(root); err != nil {
return
}
// Load a Config from the given absolute path
c, err := newConfig(root)
if err != nil {
return
}
// set Function to the value of the config loaded from disk.
f = fromConfig(c)
// The only value not included in the config is the effective path on disk
f.Root = root
// Populate with data from config if it exists.
err = applyConfig(f, root)
return
}
// DerivedName returns the name that will be used if path derivation is choosen, limited in its upward recursion.
// This is exposed for preemptive calculation for interactive confirmation, such as via a CLI.
func (f *Function) DerivedName(searchLimit int) string {
return pathToDomain(f.Root, searchLimit)
// WriteConfig writes this Function's configuration to disk.
func (f Function) WriteConfig() (err error) {
return writeConfig(f)
}
func (f *Function) Initialize(runtime, context, name, tag string, domainSearchLimit int, initializer Initializer) (err error) {
// Assert runtime is provided
if runtime == "" {
err = errors.New("runtime not specified")
// Initialized returns if the Function has been initialized.
// Any errors are considered failure (invalid or inaccessible root, config file, etc).
func (f Function) Initialized() bool {
// Load the Function's configuration from disk and check if the (required) value Runtime is populated.
c, err := newConfig(f.Root)
if err != nil {
return false
}
return c.Name != "" // TODO: use a dedicated initialized bool?
}
// DerivedImage returns the derived image name (OCI container tag) of the
// Function whose source is at root, with the default repository for when
// the image has to be calculated (derived).
// repository can be either with or without prefixed registry.
// The following are eqivalent due to the use of DefaultRegistry:
// repository: docker.io/myname
// myname
// A full image name consists of registry, repository, name and tag.
// in form [registry]/[repository]/[name]:[tag]
// example docker.io/alice/my.example.func:latest
// 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) {
f, err := NewFunction(root)
if err != nil {
// an inability to load the funciton means it is not yet initialized
// We could try to be smart here and fall through to the Function name
// deriviation logic, but that's likely to be confusing. Better to
// stay simple and say that derivation of Image depends on first having
// the funciton initialized.
return
}
// If there exists contentious files (congig files for instance), this function may have already been initialized.
files, err := contentiousFilesIn(f.Root)
// If the Function has already had image populated, use this pre-calculated value.
if f.Image != "" {
image = f.Image
return
}
// If the funciton loaded, and there is not yet an Image set, then this is
// the first build and no explicit image override was specified. We should
// therefore derive the image tag from the defined repository and name.
// form: [registry]/[user]/[function]:latest
// example: quay.io/alice/my.function.name:latest
repository = strings.Trim(repository, "/") // too defensive?
repositoryTokens := strings.Split(repository, "/")
if len(repositoryTokens) == 1 {
image = DefaultRegistry + "/" + repository + "/" + f.Name
} else if len(repositoryTokens) == 2 {
image = repository + "/" + f.Name
} else {
err = fmt.Errorf("repository should be either 'namespace' or 'registry/namespace'")
}
return
}
// DerivedName returns a name derived from the path, limited in its upward
// recursion along path to searchLimit.
func DerivedName(root string, searchLimit int) (string, error) {
root, err := filepath.Abs(root)
if err != nil {
return "", err
}
return pathToDomain(root, searchLimit), nil
}
// assertEmptyRoot ensures that the directory is empty enough to be used for
// initializing a new Function.
func assertEmptyRoot(path string) (err error) {
// If there exists contentious files (congig files for instance), this Function may have already been initialized.
files, err := contentiousFilesIn(path)
if err != nil {
return
} else if len(files) > 0 {
return fmt.Errorf("The chosen directory '%v' contains contentious files: %v. Has the Service Function already been created? Try either using a different directory, deleting the service function if it exists, or manually removing the files.", f.Root, files)
return fmt.Errorf("The chosen directory '%v' contains contentious files: %v. Has the Service Function already been created? Try either using a different directory, deleting the Function if it exists, or manually removing the files.", path, files)
}
// Ensure there are no non-hidden files, and again none of the aforementioned contentious files.
empty, err := isEffectivelyEmpty(f.Root)
empty, err := isEffectivelyEmpty(path)
if err != nil {
return
} else if !empty {
err = errors.New("The directory must be empty of visible files and recognized config files before it can be initialized.")
return
}
// Derive a name if not provided
if name == "" {
name = pathToDomain(f.Root, domainSearchLimit)
}
if name == "" {
err = errors.New("Function name must be provided or be derivable from path")
return
}
f.Name = name
// Write the template implementation in the appropriate runtime
if err = initializer.Initialize(runtime, context, f.Root); err != nil {
return
}
// runtime was validated
f.Runtime = runtime
// An image tag for the function
f.Tag = tag
// Write out the state as a config file and return.
return writeConfig(f)
}
// WriteConfig writes this function's configuration to disk.
func (f *Function) WriteConfig() (err error) {
return writeConfig(f)
}
// Initialized returns true if the function has been initialized with a name and a runtime
func (f *Function) Initialized() bool {
// TODO: this should probably be more robust than checking what amounts to a
// side-effect of the initialization process.
return (f.Runtime != "" && f.Name != "")
}
// FunctionConfiguration accepts a path to a function project
// and an image tag. It loads the existing function configuration
// from disk at path. If the image tag parameter is not empty,
// this will be used to update the function config file.
// TODO: This should probably be changed to take a path
// with optional overrides for both tag and name
func FunctionConfiguration(path, tag string) (f *Function, err error) {
f, err = NewFunction(path)
if err != nil {
return nil, err
}
if f.Name == "" || f.Runtime == "" {
return nil, fmt.Errorf("Unable to find a function project at %s", path)
}
// Allow caller to override pre-configured t name
if tag != "" {
f.Tag = tag
}
if f.Tag == "" {
f.Tag = fmt.Sprintf("quay.io/%s:latest", f.Name)
fmt.Printf("No tag provided, using %s\n", f.Tag)
}
// Write the image tag to the function configuration
if err = f.WriteConfig(); err != nil {
fmt.Printf("Error writing configuration %v\n", err)
return nil, err
}
return f, nil
return
}
// contentiousFiles are files which, if extant, preclude the creation of a
// service function rooted in the given directory.
// Function rooted in the given directory.
var contentiousFiles = []string{
".faas.yaml",
".appsody-config.yaml",

3
go.mod
View File

@ -4,9 +4,6 @@ go 1.13
require (
github.com/buildpacks/pack v0.11.0
github.com/docker/cli v0.0.0-20200312141509-ef2f64abbd37
github.com/fatih/color v1.9.0 // indirect
github.com/git-chglog/git-chglog v0.0.0-20200414013904-db796966b373 // indirect
github.com/golang/mock v1.4.3 // indirect
github.com/google/go-containerregistry v0.0.0-20200423114255-8f808463544c // indirect
github.com/imdario/mergo v0.3.10 // indirect

4
go.sum
View File

@ -181,7 +181,6 @@ github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5I
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
@ -189,7 +188,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/git-chglog/git-chglog v0.0.0-20200414013904-db796966b373/go.mod h1:Dcsy1kii/xFyNad5JqY/d0GO5mu91sungp5xotbm3Yk=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -379,7 +377,6 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
@ -695,7 +692,6 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -7,73 +7,89 @@ import (
"os"
"strings"
"github.com/boson-project/faas"
"github.com/boson-project/faas/k8s"
commands "knative.dev/client/pkg/kn/commands"
"knative.dev/client/pkg/kn/core"
"github.com/boson-project/faas"
"github.com/boson-project/faas/k8s"
)
// TODO: Use knative.dev/serving/pkg/client/clientset/versioned/typed/serving/v1
// NewForConfig gives you the client, and then you can do
// client.Services("ns").Get("name")
func NewDeployer() *Deployer {
return &Deployer{Namespace: faas.DefaultNamespace}
}
type Deployer struct {
// Namespace with which to override that set on the default configuration (such as the ~/.kube/config).
// If left blank, deployment will commence to the configured namespace.
Namespace string
Verbose bool
// Verbose logging enablement flag.
Verbose bool
}
func (deployer *Deployer) Deploy(name, image string) (address string, err error) {
func NewDeployer() *Deployer {
return &Deployer{}
}
project, err := k8s.ToSubdomain(name)
func (d *Deployer) Deploy(f faas.Function) (err error) {
// k8s does not support service names with dots. so encode it such that
// www.my-domain,com -> www-my--domain-com
encodedName, err := k8s.ToSubdomain(f.Name)
if err != nil {
return
}
nn := strings.Split(name, ".")
if len(nn) < 3 {
err = fmt.Errorf("invalid service name '%v', must be at least three parts.\n", name)
return
}
subDomain := nn[0]
domain := strings.Join(nn[1:], ".")
// Capture output in a buffer if verbose is not enabled for output on error.
var output io.Writer
if deployer.Verbose {
if d.Verbose {
output = os.Stdout
} else {
output = &bytes.Buffer{}
}
// FIXME(lkinglan): The labels set explicitly here may interfere with the
// cluster configuraiton steps described in the documentation, and may also
// break on multi-level subdomains or if they are out of sync with that
// configuration. These could be removed from here, and instead the cluster
// expeted to be configured correctly. It is a future enhancement that an
// attempt to deploy a publicly accessible Function of a hithertoo unseen
// TLD+1 will modify this config-map.
// See https://github.com/boson-project/faas/issues/47
nn := strings.Split(f.Name, ".")
if len(nn) < 3 {
err = fmt.Errorf("invalid service name '%v', must be at least three parts.\n", f.Name)
return
}
subDomain := nn[0]
domain := strings.Join(nn[1:], ".")
params := commands.KnParams{}
params.Initialize()
params.Output = output
c := core.NewKnCommand(params)
c.SetOut(output)
args := []string{
"service", "create", project,
"--image", image,
"--namespace", deployer.Namespace,
"service", "create", encodedName,
"--image", f.Image,
"--env", "VERBOSE=true",
"--label", fmt.Sprintf("faas.domain=%s", domain),
"--label", "bosonFunction=true",
"--annotation", fmt.Sprintf("faas.subdomain=%s", subDomain),
"--label", "bosonFunction=true",
}
if d.Namespace != "" {
args = append(args, "--namespace", d.Namespace)
}
c.SetArgs(args)
err = c.Execute()
if err != nil {
if !deployer.Verbose {
if !d.Verbose {
err = fmt.Errorf("failed to deploy the service: %v.\nStdOut: %s", err, output.(*bytes.Buffer).String())
} else {
err = fmt.Errorf("failed to deploy the service: %v", err)
}
return
}
// This does not actually return the service URL
// To do this, we need to be using the kn services client
// noted above
return project, nil
// TODO: use the KN service client noted above, such that we can return the
// final path/route of the final deployed funciton. While it can be assumed
// due to being deterministic, new users would be aided by having it echoed.
return
}

View File

@ -15,6 +15,7 @@ import (
servingv1 "knative.dev/serving/pkg/apis/serving/v1"
servingV1client "knative.dev/serving/pkg/client/clientset/versioned/typed/serving/v1"
"github.com/boson-project/faas"
"github.com/boson-project/faas/k8s"
)
@ -22,7 +23,6 @@ type Updater struct {
Verbose bool
namespace string
client *servingV1client.ServingV1Client
}
func NewUpdater(namespace string) (updater *Updater, err error) {
@ -38,10 +38,10 @@ func NewUpdater(namespace string) (updater *Updater, err error) {
return
}
func (updater *Updater) Update(name, image string) error {
func (updater *Updater) Update(f faas.Function) error {
client, namespace := updater.client, updater.namespace
project, err := k8s.ToSubdomain(name)
project, err := k8s.ToSubdomain(f.Name)
if err != nil {
return fmt.Errorf("updater call to k8s.ToSubdomain failed: %v", err)
}
@ -63,7 +63,6 @@ func (updater *Updater) Update(name, image string) error {
return fmt.Errorf("updater failed to generate revision name: %v", err)
}
_, err = client.Services(namespace).Update(service)
if err != nil {
return fmt.Errorf("updater failed to update the service: %v", err)
@ -78,7 +77,7 @@ func updateBuiltTimeStampEnvVar(container *Container) {
builtEnvVar := findEnvVar(builtEnvVarName, envs)
if builtEnvVar == nil {
envs = append(envs, EnvVar{Name: builtEnvVarName, })
envs = append(envs, EnvVar{Name: builtEnvVarName})
builtEnvVar = &envs[len(envs)-1]
}

File diff suppressed because one or more lines are too long

View File

@ -42,7 +42,7 @@ type Bar struct {
// verbose mode disables progress spinner and line overwrites, instead
// printing single, full line updates.
verbose bool
Verbose bool
// print verbose-style updates even when not attached to an interactive terminal.
printWhileHeadless bool
@ -64,7 +64,7 @@ func WithOutput(w io.Writer) Option {
// When in verbose mode, the bar will print simple status update lines.
func WithVerbose(v bool) Option {
return func(b *Bar) {
b.verbose = v
b.Verbose = v
}
}
@ -109,7 +109,7 @@ func (b *Bar) Increment(text string) {
}
// If we're in verbose mode, do a simple write
if b.verbose {
if b.Verbose {
b.write()
return
}
@ -142,7 +142,7 @@ func (b *Bar) Complete(text string) {
}
// If we're interactive, but in verbose mode do a simple write
if b.verbose {
if b.Verbose {
b.write()
return
}

View File

@ -56,7 +56,7 @@ func TestForStringLabelDefault(t *testing.T) {
// Label with default
_ = prompt.ForString("Name", "Alice",
prompt.WithInput(&in), prompt.WithOutput(&out))
if out.String()!= "Name (Alice): " {
if out.String() != "Name (Alice): " {
t.Fatalf("expected 'Name (Alice): ', got '%v'\n", out.String())
}
}

View File

@ -1,4 +1,4 @@
package embedded
package faas
import (
"errors"
@ -13,13 +13,12 @@ import (
)
// Updating Templates:
// See documentation in faas/templates for instructions on including template
// updates in the binary for access by pkger.
// See documentation in ./templates/README.md
// DefautlTemplate is the default function signature / environmental context
// 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 = "events"
const DefaultTemplate = "http"
// FileAccessor encapsulates methods for accessing template files.
type FileAccessor interface {
@ -42,18 +41,12 @@ func init() {
_ = pkger.Include("/templates")
}
type Initializer struct {
Verbose bool
type templateWriter struct {
verbose bool
templates string
}
// NewInitializer with an optional path to extended repositories directory
// (by default only internally embedded templates are used)
func NewInitializer(templates string) *Initializer {
return &Initializer{templates: templates}
}
func (n *Initializer) Initialize(runtime, template string, dest string) error {
func (n templateWriter) Write(runtime, template string, dest string) error {
if template == "" {
template = DefaultTemplate
}

View File

@ -6,7 +6,7 @@ When any templates are modified, `../pkged.go` should be regenerated.
```
go get github.com/markbates/pkger/cmd/pkger
cd embedded && pkger
pkger
```
Generates a pkged.go containing serialized versions of the contents of
the templates directory, made accessible at runtime.

View File

@ -9,7 +9,7 @@ import (
)
// Handle a CloudEvent.
// Supported function signatures:
// Supported Function signatures:
// func()
// func() error
// func(context.Context)

View File

@ -1,4 +1,4 @@
package embedded
package faas
import (
"os"
@ -10,7 +10,7 @@ import (
// (Go), the template is written.
func TestInitialize(t *testing.T) {
var (
path = "testdata/example.com/www"
path = "testdata/example.org/www"
testFile = "handle.go"
template = "http"
)
@ -34,7 +34,7 @@ func TestInitialize(t *testing.T) {
// TestDefaultTemplate ensures that if no template is provided, files are still written.
func TestDefaultTemplate(t *testing.T) {
var (
path = "testdata/example.com/www"
path = "testdata/example.org/www"
testFile = "handle.go"
template = ""
)
@ -65,7 +65,7 @@ func TestDefaultTemplate(t *testing.T) {
// $HOME/.config/templates/boson-experimental/go/json
func TestCustom(t *testing.T) {
var (
path = "testdata/example.com/www"
path = "testdata/example.org/www"
testFile = "handle.go"
template = "boson-experimental/json"
// repos = "testdata/templates"
@ -99,7 +99,7 @@ func TestCustom(t *testing.T) {
// TestEmbeddedFileMode 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.com/www"
var path = "testdata/example.org/www"
err := os.MkdirAll(path, 0744)
if err != nil {
panic(err)
@ -128,7 +128,7 @@ func TestEmbeddedFileMode(t *testing.T) {
// of templates are written with the same mode from whence they came
func TestFileMode(t *testing.T) {
var (
path = "testdata/example.com/www"
path = "testdata/example.org/www"
template = "boson-experimental/http"
)
err := os.MkdirAll(path, 0744)