mirror of https://github.com/knative/func.git
src: testable commands (#415)
* feat: client progress listener 'stopping' state * src: testable commands Restructures commands to accept a fn.Client constructor on command instantiation. This allows the concrete implementations, or entire client to be mocked for testing. Also some minor refacotring as necessary to shoehorn into the pattern. * fix: increase default timeout to 120s for service creation * chore: bump kind, knative and kubectl versions
This commit is contained in:
parent
346cae0192
commit
afcde2d551
|
@ -80,11 +80,11 @@ jobs:
|
||||||
- name: Provision Cluster
|
- name: Provision Cluster
|
||||||
uses: container-tools/kind-action@v1 # use ./hack/allocate.sh locally
|
uses: container-tools/kind-action@v1 # use ./hack/allocate.sh locally
|
||||||
with:
|
with:
|
||||||
version: v0.10.0
|
version: v0.11.1
|
||||||
kubectl_version: v1.20.0
|
kubectl_version: v1.21.2
|
||||||
knative_serving: v0.22.0
|
knative_serving: v0.23.0
|
||||||
knative_kourier: v0.22.0
|
knative_kourier: v0.23.0
|
||||||
knative_eventing: v0.22.0
|
knative_eventing: v0.23.0
|
||||||
config: testdata/cluster.yaml
|
config: testdata/cluster.yaml
|
||||||
- name: Configure Cluster
|
- name: Configure Cluster
|
||||||
run: ./hack/configure.sh
|
run: ./hack/configure.sh
|
||||||
|
|
53
client.go
53
client.go
|
@ -112,6 +112,10 @@ type ProgressListener interface {
|
||||||
// Complete signals completion, which is expected to be somewhat different than a step increment.
|
// Complete signals completion, which is expected to be somewhat different than a step increment.
|
||||||
Complete(message string)
|
Complete(message string)
|
||||||
|
|
||||||
|
// Stopping indicates the process is in the state of stopping, such as when a context cancelation
|
||||||
|
// has been received
|
||||||
|
Stopping()
|
||||||
|
|
||||||
// Done signals a cessation of progress updates. Should be called in a defer statement to ensure
|
// Done signals a cessation of progress updates. Should be called in a defer statement to ensure
|
||||||
// the progress listener can stop any outstanding tasks such as synchronous user updates.
|
// the progress listener can stop any outstanding tasks such as synchronous user updates.
|
||||||
Done()
|
Done()
|
||||||
|
@ -278,7 +282,15 @@ func WithEmitter(e Emitter) Option {
|
||||||
// Use Create, Build and Deploy independently for lower level control.
|
// Use Create, Build and Deploy independently for lower level control.
|
||||||
func (c *Client) New(ctx context.Context, cfg Function) (err error) {
|
func (c *Client) New(ctx context.Context, cfg Function) (err error) {
|
||||||
c.progressListener.SetTotal(3)
|
c.progressListener.SetTotal(3)
|
||||||
defer c.progressListener.Done()
|
// Always start a concurrent routine listening for context cancellation.
|
||||||
|
// On this event, immediately indicate the task is canceling.
|
||||||
|
// (this is useful, for example, when a progress listener is mutating
|
||||||
|
// stdout, and a context cancelation needs to free up stdout entirely for
|
||||||
|
// the status or error from said cancelltion.
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c.progressListener.Stopping()
|
||||||
|
}()
|
||||||
|
|
||||||
// Create local template
|
// Create local template
|
||||||
err = c.Create(cfg)
|
err = c.Create(cfg)
|
||||||
|
@ -404,6 +416,10 @@ func (c *Client) Create(cfg Function) (err error) {
|
||||||
// not contain a populated Image.
|
// not contain a populated Image.
|
||||||
func (c *Client) Build(ctx context.Context, path string) (err error) {
|
func (c *Client) Build(ctx context.Context, path string) (err error) {
|
||||||
c.progressListener.Increment("Building function image")
|
c.progressListener.Increment("Building function image")
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c.progressListener.Stopping()
|
||||||
|
}()
|
||||||
|
|
||||||
f, err := NewFunction(path)
|
f, err := NewFunction(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -436,6 +452,12 @@ func (c *Client) Build(ctx context.Context, path string) (err error) {
|
||||||
// Deploy the Function at path. Errors if the Function has not been
|
// Deploy the Function at path. Errors if the Function has not been
|
||||||
// initialized with an image tag.
|
// initialized with an image tag.
|
||||||
func (c *Client) Deploy(ctx context.Context, path string) (err error) {
|
func (c *Client) Deploy(ctx context.Context, path string) (err error) {
|
||||||
|
c.progressListener.Increment("Deployin function")
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c.progressListener.Stopping()
|
||||||
|
}()
|
||||||
|
|
||||||
f, err := NewFunction(path)
|
f, err := NewFunction(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -489,6 +511,10 @@ func (c *Client) Route(path string) (err error) {
|
||||||
|
|
||||||
// Run the Function whose code resides at root.
|
// Run the Function whose code resides at root.
|
||||||
func (c *Client) Run(ctx context.Context, root string) error {
|
func (c *Client) Run(ctx context.Context, root string) error {
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c.progressListener.Stopping()
|
||||||
|
}()
|
||||||
|
|
||||||
// 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)
|
f, err := NewFunction(root)
|
||||||
|
@ -514,6 +540,10 @@ func (c *Client) List(ctx context.Context) ([]ListItem, error) {
|
||||||
// Describe a Function. Name takes precidence. If no name is provided,
|
// Describe a Function. Name takes precidence. If no name is provided,
|
||||||
// the Function defined at root is used.
|
// the Function defined at root is used.
|
||||||
func (c *Client) Describe(ctx context.Context, name, root string) (d Description, err error) {
|
func (c *Client) Describe(ctx context.Context, name, root string) (d Description, err error) {
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c.progressListener.Stopping()
|
||||||
|
}()
|
||||||
// If name is provided, it takes precidence.
|
// If name is provided, it takes precidence.
|
||||||
// Otherwise load the Function defined at root.
|
// Otherwise load the Function defined at root.
|
||||||
if name != "" {
|
if name != "" {
|
||||||
|
@ -533,6 +563,10 @@ func (c *Client) Describe(ctx context.Context, name, root string) (d Description
|
||||||
// Remove a Function. Name takes precidence. If no name is provided,
|
// Remove a Function. Name takes precidence. If no name is provided,
|
||||||
// the Function defined at root is used if it exists.
|
// the Function defined at root is used if it exists.
|
||||||
func (c *Client) Remove(ctx context.Context, cfg Function) error {
|
func (c *Client) Remove(ctx context.Context, cfg Function) error {
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c.progressListener.Stopping()
|
||||||
|
}()
|
||||||
// If name is provided, it takes precidence.
|
// If name is provided, it takes precidence.
|
||||||
// Otherwise load the Function deined at root.
|
// Otherwise load the Function deined at root.
|
||||||
if cfg.Name != "" {
|
if cfg.Name != "" {
|
||||||
|
@ -551,15 +585,23 @@ func (c *Client) Remove(ctx context.Context, cfg Function) error {
|
||||||
|
|
||||||
// Emit a CloudEvent to a function endpoint
|
// Emit a CloudEvent to a function endpoint
|
||||||
func (c *Client) Emit(ctx context.Context, endpoint string) error {
|
func (c *Client) Emit(ctx context.Context, endpoint string) error {
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
c.progressListener.Stopping()
|
||||||
|
}()
|
||||||
return c.emitter.Emit(ctx, endpoint)
|
return c.emitter.Emit(ctx, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual implementations (noops) of required interfaces.
|
// Manual implementations (noops) of required interfaces.
|
||||||
// In practice, the user of this client package (for example the CLI) will
|
// In practice, the user of this client package (for example the CLI) will
|
||||||
// provide a concrete implementation for all of the interfaces. For testing or
|
// provide a concrete implementation for only the interfaces necessary to
|
||||||
// development, however, it is usefule that they are defaulted to noops and
|
// complete the given command. Integrators importing the package would
|
||||||
// provded only when necessary. Unit tests for the concrete implementations
|
// provide a concrete implementation for all interfaces to be used. To
|
||||||
// serve to keep the core logic here separate from the imperitive.
|
// enable partial definition (in particular used for testing) they
|
||||||
|
// are defaulted to noop implementations such that they can be provded
|
||||||
|
// only when necessary. Unit tests for the concrete implementations
|
||||||
|
// serve to keep the core logic here separate from the imperitive, and
|
||||||
|
// with a minimum of external dependencies.
|
||||||
// -----------------------------------------------------
|
// -----------------------------------------------------
|
||||||
|
|
||||||
type noopBuilder struct{ output io.Writer }
|
type noopBuilder struct{ output io.Writer }
|
||||||
|
@ -597,6 +639,7 @@ type noopProgressListener struct{}
|
||||||
func (p *noopProgressListener) SetTotal(i int) {}
|
func (p *noopProgressListener) SetTotal(i int) {}
|
||||||
func (p *noopProgressListener) Increment(m string) {}
|
func (p *noopProgressListener) Increment(m string) {}
|
||||||
func (p *noopProgressListener) Complete(m string) {}
|
func (p *noopProgressListener) Complete(m string) {}
|
||||||
|
func (p *noopProgressListener) Stopping() {}
|
||||||
func (p *noopProgressListener) Done() {}
|
func (p *noopProgressListener) Done() {}
|
||||||
|
|
||||||
type noopEmitter struct{}
|
type noopEmitter struct{}
|
||||||
|
|
89
cmd/build.go
89
cmd/build.go
|
@ -14,23 +14,32 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
root.AddCommand(buildCmd)
|
// Add to the root a new "Build" command which obtains an appropriate
|
||||||
buildCmd.Flags().StringP("builder", "b", "", "Buildpack builder, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml for subsequent builds.")
|
// instance of fn.Client from the given client creator function.
|
||||||
buildCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
root.AddCommand(NewBuildCmd(newBuildClient))
|
||||||
buildCmd.Flags().StringP("image", "i", "", "Full image name in the orm [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE")
|
|
||||||
buildCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
|
||||||
buildCmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
|
|
||||||
|
|
||||||
err := buildCmd.RegisterFlagCompletionFunc("builder", CompleteBuilderList)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var buildCmd = &cobra.Command{
|
func newBuildClient(cfg buildConfig) (*fn.Client, error) {
|
||||||
Use: "build",
|
builder := buildpacks.NewBuilder()
|
||||||
Short: "Build a function project as a container image",
|
listener := progress.New()
|
||||||
Long: `Build a function project as a container image
|
|
||||||
|
builder.Verbose = cfg.Verbose
|
||||||
|
listener.Verbose = cfg.Verbose
|
||||||
|
|
||||||
|
return fn.New(
|
||||||
|
fn.WithBuilder(builder),
|
||||||
|
fn.WithProgressListener(listener),
|
||||||
|
fn.WithRegistry(cfg.Registry), // for image name when --image not provided
|
||||||
|
fn.WithVerbose(cfg.Verbose)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type buildClientFn func(buildConfig) (*fn.Client, error)
|
||||||
|
|
||||||
|
func NewBuildCmd(clientFn buildClientFn) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "build",
|
||||||
|
Short: "Build a function project as a container image",
|
||||||
|
Long: `Build a function project as a container image
|
||||||
|
|
||||||
This command builds the function project in the current directory or in the directory
|
This command builds the function project in the current directory or in the directory
|
||||||
specified by --path. The result will be a container image that is pushed to a registry.
|
specified by --path. The result will be a container image that is pushed to a registry.
|
||||||
|
@ -38,7 +47,7 @@ The func.yaml file is read to determine the image name and registry.
|
||||||
If the project has not already been built, either --registry or --image must be provided
|
If the project has not already been built, either --registry or --image must be provided
|
||||||
and the image name is stored in the configuration file.
|
and the image name is stored in the configuration file.
|
||||||
`,
|
`,
|
||||||
Example: `
|
Example: `
|
||||||
# Build from the local directory, using the given registry as target.
|
# Build from the local directory, using the given registry as target.
|
||||||
# The full image name will be determined automatically based on the
|
# The full image name will be determined automatically based on the
|
||||||
# project directory name
|
# project directory name
|
||||||
|
@ -53,12 +62,28 @@ kn func build
|
||||||
# Build with a custom buildpack builder
|
# Build with a custom buildpack builder
|
||||||
kn func build --builder cnbs/sample-builder:bionic
|
kn func build --builder cnbs/sample-builder:bionic
|
||||||
`,
|
`,
|
||||||
SuggestFor: []string{"biuld", "buidl", "built"},
|
SuggestFor: []string{"biuld", "buidl", "built"},
|
||||||
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm"),
|
PreRunE: bindEnv("image", "path", "builder", "registry", "confirm"),
|
||||||
RunE: runBuild,
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("builder", "b", "", "Buildpack builder, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml for subsequent builds.")
|
||||||
|
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
||||||
|
cmd.Flags().StringP("image", "i", "", "Full image name in the orm [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE")
|
||||||
|
cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
||||||
|
cmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
|
||||||
|
|
||||||
|
if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuilderList); err != nil {
|
||||||
|
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runBuild(cmd, args, clientFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runBuild(cmd *cobra.Command, _ []string) (err error) {
|
func runBuild(cmd *cobra.Command, _ []string, clientFn buildClientFn) (err error) {
|
||||||
config, err := newBuildConfig().Prompt()
|
config, err := newBuildConfig().Prompt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == terminal.InterruptErr {
|
if err == terminal.InterruptErr {
|
||||||
|
@ -106,26 +131,12 @@ func runBuild(cmd *cobra.Command, _ []string) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := buildpacks.NewBuilder()
|
client, err := clientFn(config)
|
||||||
builder.Verbose = config.Verbose
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
listener := progress.New()
|
return client.Build(cmd.Context(), config.Path)
|
||||||
listener.Verbose = config.Verbose
|
|
||||||
defer listener.Done()
|
|
||||||
|
|
||||||
context := cmd.Context()
|
|
||||||
go func() {
|
|
||||||
<-context.Done()
|
|
||||||
listener.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
client := fn.New(
|
|
||||||
fn.WithVerbose(config.Verbose),
|
|
||||||
fn.WithRegistry(config.Registry), // for deriving image name when --image not provided explicitly.
|
|
||||||
fn.WithBuilder(builder),
|
|
||||||
fn.WithProgressListener(listener))
|
|
||||||
|
|
||||||
return client.Build(context, config.Path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type buildConfig struct {
|
type buildConfig struct {
|
||||||
|
|
|
@ -24,13 +24,15 @@ func init() {
|
||||||
// The createClientFn is a client factory which creates a new Client for use by
|
// The createClientFn is a client factory which creates a new Client for use by
|
||||||
// the create command during normal execution (see tests for alternative client
|
// the create command during normal execution (see tests for alternative client
|
||||||
// factories which return clients with various mocks).
|
// factories which return clients with various mocks).
|
||||||
func newCreateClient(repositories string, verbose bool) *fn.Client {
|
func newCreateClient(cfg createConfig) *fn.Client {
|
||||||
return fn.New(fn.WithRepositories(repositories), fn.WithVerbose(verbose))
|
return fn.New(
|
||||||
|
fn.WithRepositories(cfg.Repositories),
|
||||||
|
fn.WithVerbose(cfg.Verbose))
|
||||||
}
|
}
|
||||||
|
|
||||||
// createClientFn is a factory function which returns a Client suitable for
|
// createClientFn is a factory function which returns a Client suitable for
|
||||||
// use with the Create command.
|
// use with the Create command.
|
||||||
type createClientFn func(repositories string, verbose bool) *fn.Client
|
type createClientFn func(createConfig) *fn.Client
|
||||||
|
|
||||||
// NewCreateCmd creates a create command using the given client creator.
|
// NewCreateCmd creates a create command using the given client creator.
|
||||||
func NewCreateCmd(clientFn createClientFn) *cobra.Command {
|
func NewCreateCmd(clientFn createClientFn) *cobra.Command {
|
||||||
|
@ -59,14 +61,10 @@ kn func create --template events myfunc
|
||||||
PreRunE: bindEnv("runtime", "template", "repositories", "confirm"),
|
PreRunE: bindEnv("runtime", "template", "repositories", "confirm"),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().BoolP("confirm", "c", false,
|
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
||||||
"Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
cmd.Flags().StringP("runtime", "l", fn.DefaultRuntime, "Function runtime language/framework. Available runtimes: "+buildpacks.Runtimes()+" (Env: $FUNC_RUNTIME)")
|
||||||
cmd.Flags().StringP("runtime", "l", fn.DefaultRuntime,
|
cmd.Flags().StringP("repositories", "r", filepath.Join(configPath(), "repositories"), "Path to extended template repositories (Env: $FUNC_REPOSITORIES)")
|
||||||
"Function runtime language/framework. Available runtimes: "+buildpacks.Runtimes()+" (Env: $FUNC_RUNTIME)")
|
cmd.Flags().StringP("template", "t", fn.DefaultTemplate, "Function template. Available templates: 'http' and 'events' (Env: $FUNC_TEMPLATE)")
|
||||||
cmd.Flags().StringP("repositories", "r", filepath.Join(configPath(), "repositories"),
|
|
||||||
"Path to extended template repositories (Env: $FUNC_REPOSITORIES)")
|
|
||||||
cmd.Flags().StringP("template", "t", fn.DefaultTemplate,
|
|
||||||
"Function template. Available templates: 'http' and 'events' (Env: $FUNC_TEMPLATE)")
|
|
||||||
|
|
||||||
// Register tab-completeion function integration
|
// Register tab-completeion function integration
|
||||||
if err := cmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList); err != nil {
|
if err := cmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList); err != nil {
|
||||||
|
@ -103,7 +101,7 @@ func runCreate(cmd *cobra.Command, args []string, clientFn createClientFn) (err
|
||||||
Template: config.Template,
|
Template: config.Template,
|
||||||
}
|
}
|
||||||
|
|
||||||
client := clientFn(config.Repositories, config.Verbose)
|
client := clientFn(config)
|
||||||
|
|
||||||
return client.Create(function)
|
return client.Create(function)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ func TestCreateValidatesName(t *testing.T) {
|
||||||
|
|
||||||
// Create a new Create command with a fn.Client construtor
|
// Create a new Create command with a fn.Client construtor
|
||||||
// which returns a default (noop) client suitable for tests.
|
// which returns a default (noop) client suitable for tests.
|
||||||
cmd := NewCreateCmd(func(string, bool) *fn.Client {
|
cmd := NewCreateCmd(func(createConfig) *fn.Client {
|
||||||
return fn.New()
|
return fn.New()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
138
cmd/delete.go
138
cmd/delete.go
|
@ -13,11 +13,33 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
root.AddCommand(deleteCmd)
|
// Create a new delete command with a reference to
|
||||||
|
// a function which yields an appropriate concrete client instance.
|
||||||
|
root.AddCommand(NewDeleteCmd(newDeleteClient))
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeleteCmd(newRemover func(ns string, verbose bool) (fn.Remover, error)) *cobra.Command {
|
// newDeleteClient returns an instance of a Client using the
|
||||||
delCmd := &cobra.Command{
|
// final config state.
|
||||||
|
// Testing note: This method is swapped out during testing to allow
|
||||||
|
// mocking the remover or the client itself to fabricate test states.
|
||||||
|
func newDeleteClient(cfg deleteConfig) (*fn.Client, error) {
|
||||||
|
remover, err := knative.NewRemover(cfg.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
remover.Verbose = cfg.Verbose
|
||||||
|
|
||||||
|
return fn.New(
|
||||||
|
fn.WithRemover(remover),
|
||||||
|
fn.WithVerbose(cfg.Verbose)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A deleteClientFn is a function which yields a Client instance from a config
|
||||||
|
type deleteClientFn func(deleteConfig) (*fn.Client, error)
|
||||||
|
|
||||||
|
func NewDeleteCmd(clientFn deleteClientFn) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
Use: "delete [NAME]",
|
Use: "delete [NAME]",
|
||||||
Short: "Undeploy a function",
|
Short: "Undeploy a function",
|
||||||
Long: `Undeploy a function
|
Long: `Undeploy a function
|
||||||
|
@ -38,71 +60,65 @@ kn func delete -n apps myfunc
|
||||||
SuggestFor: []string{"remove", "rm", "del"},
|
SuggestFor: []string{"remove", "rm", "del"},
|
||||||
ValidArgsFunction: CompleteFunctionList,
|
ValidArgsFunction: CompleteFunctionList,
|
||||||
PreRunE: bindEnv("path", "confirm", "namespace"),
|
PreRunE: bindEnv("path", "confirm", "namespace"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
|
||||||
config, err := newDeleteConfig(args).Prompt()
|
|
||||||
if err != nil {
|
|
||||||
if err == terminal.InterruptErr {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var function fn.Function
|
|
||||||
|
|
||||||
// Initialize func with explicit name (when provided)
|
|
||||||
if len(args) > 0 && args[0] != "" {
|
|
||||||
pathChanged := cmd.Flags().Changed("path")
|
|
||||||
if pathChanged {
|
|
||||||
return fmt.Errorf("Only one of --path and [NAME] should be provided")
|
|
||||||
}
|
|
||||||
function = fn.Function{
|
|
||||||
Name: args[0],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
function, err = fn.NewFunction(config.Path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the Function has been initialized
|
|
||||||
if !function.Initialized() {
|
|
||||||
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ns := config.Namespace
|
|
||||||
if ns == "" {
|
|
||||||
ns = function.Namespace
|
|
||||||
}
|
|
||||||
|
|
||||||
remover, err := newRemover(ns, config.Verbose)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client := fn.New(
|
|
||||||
fn.WithVerbose(config.Verbose),
|
|
||||||
fn.WithRemover(remover))
|
|
||||||
|
|
||||||
return client.Remove(cmd.Context(), function)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
||||||
delCmd.Flags().StringP("path", "p", cwd(), "Path to the function project that should be undeployed (Env: $FUNC_PATH)")
|
cmd.Flags().StringP("path", "p", cwd(), "Path to the function project that should be undeployed (Env: $FUNC_PATH)")
|
||||||
delCmd.Flags().StringP("namespace", "n", "", "Namespace of the function to undeploy. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
|
cmd.Flags().StringP("namespace", "n", "", "Namespace of the function to undeploy. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
|
||||||
|
|
||||||
return delCmd
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runDelete(cmd, args, clientFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
var deleteCmd = NewDeleteCmd(func(ns string, verbose bool) (fn.Remover, error) {
|
func runDelete(cmd *cobra.Command, args []string, clientFn deleteClientFn) (err error) {
|
||||||
r, err := knative.NewRemover(ns)
|
config, err := newDeleteConfig(args).Prompt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if err == terminal.InterruptErr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
r.Verbose = verbose
|
|
||||||
return r, nil
|
var function fn.Function
|
||||||
})
|
|
||||||
|
// Initialize func with explicit name (when provided)
|
||||||
|
if len(args) > 0 && args[0] != "" {
|
||||||
|
pathChanged := cmd.Flags().Changed("path")
|
||||||
|
if pathChanged {
|
||||||
|
return fmt.Errorf("Only one of --path and [NAME] should be provided")
|
||||||
|
}
|
||||||
|
function = fn.Function{
|
||||||
|
Name: args[0],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
function, err = fn.NewFunction(config.Path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the Function has been initialized
|
||||||
|
if !function.Initialized() {
|
||||||
|
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not provided, use the function's extant namespace
|
||||||
|
if config.Namespace == "" {
|
||||||
|
config.Namespace = function.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a client instance from the now-final config
|
||||||
|
client, err := clientFn(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke remove using the concrete client impl
|
||||||
|
return client.Remove(cmd.Context(), function)
|
||||||
|
}
|
||||||
|
|
||||||
type deleteConfig struct {
|
type deleteConfig struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -1,48 +1,57 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
fn "github.com/boson-project/func"
|
fn "github.com/boson-project/func"
|
||||||
|
"github.com/boson-project/func/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testRemover struct {
|
// TestDeleteByName ensures that running delete specifying the name of the Funciton
|
||||||
invokedWith *string
|
// explicitly as an argument invokes the remover appropriately.
|
||||||
}
|
func TestDeleteByName(t *testing.T) {
|
||||||
|
var (
|
||||||
|
testname = "testname" // explict name with which to create the Funciton
|
||||||
|
args = []string{testname} // passed as the lone argument
|
||||||
|
remover = mock.NewRemover() // with a mock remover
|
||||||
|
)
|
||||||
|
|
||||||
func (t *testRemover) Remove(ctx context.Context, name string) error {
|
// Remover fails the test if it receives the incorrect name
|
||||||
t.invokedWith = &name
|
// an incorrect name.
|
||||||
return nil
|
remover.RemoveFn = func(n string) error {
|
||||||
}
|
if n != testname {
|
||||||
|
t.Fatalf("expected delete name %v, got %v", testname, n)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// test delete outside project just using function name
|
// Create a command with a client constructor fn that instantiates a client
|
||||||
func TestDeleteCmdWithoutProject(t *testing.T) {
|
// with a the mocked remover.
|
||||||
tr := &testRemover{}
|
cmd := NewDeleteCmd(func(_ deleteConfig) (*fn.Client, error) {
|
||||||
cmd := NewDeleteCmd(func(ns string, verbose bool) (fn.Remover, error) {
|
return fn.New(fn.WithRemover(remover)), nil
|
||||||
return tr, nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd.SetArgs([]string{"foo"})
|
// Execute the command
|
||||||
|
cmd.SetArgs(args)
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tr.invokedWith == nil {
|
// Fail if remover's .Remove not invoked at all
|
||||||
t.Fatal("fn.Remover has not been invoked")
|
if !remover.RemoveInvoked {
|
||||||
}
|
t.Fatal("fn.Remover not invoked")
|
||||||
|
|
||||||
if *tr.invokedWith != "foo" {
|
|
||||||
t.Fatalf("expected fn.Remover to be called with 'foo', but was called with '%s'", *tr.invokedWith)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// test delete from inside project directory (reading func name from func.yaml)
|
// TestDeleteByProject ensures that running delete with a valid project as its
|
||||||
func TestDeleteCmdWithProject(t *testing.T) {
|
// context invokes remove and with the correct name (reads name from func.yaml)
|
||||||
|
func TestDeleteByProject(t *testing.T) {
|
||||||
|
// from within a new temporary directory
|
||||||
|
defer fromTempDir(t)()
|
||||||
|
|
||||||
|
// Write a func.yaml config which specifies a name
|
||||||
funcYaml := `name: bar
|
funcYaml := `name: bar
|
||||||
namespace: ""
|
namespace: ""
|
||||||
runtime: go
|
runtime: go
|
||||||
|
@ -54,73 +63,65 @@ builderMap:
|
||||||
envs: []
|
envs: []
|
||||||
annotations: {}
|
annotations: {}
|
||||||
`
|
`
|
||||||
tmpDir, err := ioutil.TempDir("", "bar")
|
if err := ioutil.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
f, err := os.Create(filepath.Join(tmpDir, "func.yaml"))
|
// A mock remover which fails if the name from the func.yaml is not received.
|
||||||
if err != nil {
|
remover := mock.NewRemover()
|
||||||
t.Fatal(err)
|
remover.RemoveFn = func(n string) error {
|
||||||
}
|
if n != "bar" {
|
||||||
defer f.Close()
|
t.Fatalf("expected name 'bar', got '%v'", n)
|
||||||
|
|
||||||
_, err = f.WriteString(funcYaml)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
oldWD, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err = os.Chdir(oldWD)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
}()
|
return nil
|
||||||
err = os.Chdir(tmpDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tr := &testRemover{}
|
// Command with a Client constructor that returns client with the
|
||||||
cmd := NewDeleteCmd(func(ns string, verbose bool) (fn.Remover, error) {
|
// mocked remover.
|
||||||
return tr, nil
|
cmd := NewDeleteCmd(func(_ deleteConfig) (*fn.Client, error) {
|
||||||
|
return fn.New(fn.WithRemover(remover)), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd.SetArgs([]string{"-p", "."})
|
// Execute the command simulating no arguments.
|
||||||
err = cmd.Execute()
|
cmd.SetArgs([]string{})
|
||||||
|
err := cmd.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tr.invokedWith == nil {
|
// Also fail if remover's .Remove is not invoked
|
||||||
t.Fatal("fn.Remover has not been invoked")
|
if !remover.RemoveInvoked {
|
||||||
}
|
t.Fatal("fn.Remover not invoked")
|
||||||
|
|
||||||
if *tr.invokedWith != "bar" {
|
|
||||||
t.Fatalf("expected fn.Remover to be called with 'bar', but was called with '%s'", *tr.invokedWith)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// test where both name and path are provided
|
// TestDeleteNameAndPathExclusivity ensures that providing both a name and a
|
||||||
func TestDeleteCmdWithBothPathAndName(t *testing.T) {
|
// path generates an error.
|
||||||
tr := &testRemover{}
|
// Providing the --path (-p) flag indicates the name of the funciton to delete
|
||||||
cmd := NewDeleteCmd(func(ns string, verbose bool) (fn.Remover, error) {
|
// is to be taken from the Function at the given path. Providing the name as
|
||||||
return tr, nil
|
// an argument as well is therefore redundant and an error.
|
||||||
|
func TestDeleteNameAndPathExclusivity(t *testing.T) {
|
||||||
|
|
||||||
|
// A mock remover which will be sampled to ensure it is not invoked.
|
||||||
|
remover := mock.NewRemover()
|
||||||
|
|
||||||
|
// Command with a Client constructor using the mock remover.
|
||||||
|
cmd := NewDeleteCmd(func(_ deleteConfig) (*fn.Client, error) {
|
||||||
|
return fn.New(fn.WithRemover(remover)), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
cmd.SetArgs([]string{"foo", "-p", "/adir/"})
|
// Execute the command simulating the invalid argument combination of both
|
||||||
|
// a path and an explicit name.
|
||||||
|
cmd.SetArgs([]string{"-p", "./testpath", "testname"})
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("error was expected as both name an path cannot be used together")
|
// TODO should really either parse the output or use typed errors to ensure it's
|
||||||
|
// failing for the expected reason.
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tr.invokedWith != nil {
|
// Also fail if remover's .Remove is invoked.
|
||||||
t.Fatal("fn.Remove was call when it shouldn't have been")
|
if remover.RemoveInvoked {
|
||||||
|
t.Fatal("fn.Remover invoked despite invalid combination and an error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
123
cmd/deploy.go
123
cmd/deploy.go
|
@ -21,22 +21,46 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
root.AddCommand(deployCmd)
|
root.AddCommand(NewDeployCmd(newDeployClient))
|
||||||
deployCmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
|
||||||
deployCmd.Flags().StringArrayP("env", "e", []string{}, "Environment variable to set in the form NAME=VALUE. "+
|
|
||||||
"You may provide this flag multiple times for setting multiple environment variables. "+
|
|
||||||
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
|
|
||||||
deployCmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE")
|
|
||||||
deployCmd.Flags().StringP("namespace", "n", "", "Namespace of the function to undeploy. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
|
|
||||||
deployCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
|
||||||
deployCmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
|
|
||||||
deployCmd.Flags().BoolP("build", "b", true, "Build the image before deploying (Env: $FUNC_BUILD)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var deployCmd = &cobra.Command{
|
func newDeployClient(cfg deployConfig) (*fn.Client, error) {
|
||||||
Use: "deploy",
|
listener := progress.New()
|
||||||
Short: "Deploy a function",
|
|
||||||
Long: `Deploy a function
|
builder := buildpacks.NewBuilder()
|
||||||
|
|
||||||
|
pusher, err := docker.NewPusher(docker.WithCredentialsProvider(credentialsProvider))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
deployer, err := knative.NewDeployer(cfg.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.Verbose = cfg.Verbose
|
||||||
|
builder.Verbose = cfg.Verbose
|
||||||
|
pusher.Verbose = cfg.Verbose
|
||||||
|
deployer.Verbose = cfg.Verbose
|
||||||
|
|
||||||
|
return fn.New(
|
||||||
|
fn.WithProgressListener(listener),
|
||||||
|
fn.WithBuilder(builder),
|
||||||
|
fn.WithPusher(pusher),
|
||||||
|
fn.WithDeployer(deployer),
|
||||||
|
fn.WithRegistry(cfg.Registry), // for deriving name when no --image value
|
||||||
|
fn.WithVerbose(cfg.Verbose),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type deployClientFn func(deployConfig) (*fn.Client, error)
|
||||||
|
|
||||||
|
func NewDeployCmd(clientFn deployClientFn) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "deploy",
|
||||||
|
Short: "Deploy a function",
|
||||||
|
Long: `Deploy a function
|
||||||
|
|
||||||
Builds a container image for the function and deploys it to the connected Knative enabled cluster.
|
Builds a container image for the function and deploys it to the connected Knative enabled cluster.
|
||||||
The function is picked up from the project in the current directory or from the path provided
|
The function is picked up from the project in the current directory or from the path provided
|
||||||
|
@ -47,7 +71,7 @@ in the configuration file.
|
||||||
If the function is already deployed, it is updated with a new container image
|
If the function is already deployed, it is updated with a new container image
|
||||||
that is pushed to an image registry, and finally the function's Knative service is updated.
|
that is pushed to an image registry, and finally the function's Knative service is updated.
|
||||||
`,
|
`,
|
||||||
Example: `
|
Example: `
|
||||||
# Build and deploy the function from the current directory's project. The image will be
|
# Build and deploy the function from the current directory's project. The image will be
|
||||||
# pushed to "quay.io/myuser/<function name>" and deployed as Knative service with the
|
# pushed to "quay.io/myuser/<function name>" and deployed as Knative service with the
|
||||||
# same name as the function to the currently connected cluster.
|
# same name as the function to the currently connected cluster.
|
||||||
|
@ -57,17 +81,33 @@ kn func deploy --registry quay.io/myuser
|
||||||
# the namespace "myns"
|
# the namespace "myns"
|
||||||
kn func deploy --image quay.io/myuser/myfunc -n myns
|
kn func deploy --image quay.io/myuser/myfunc -n myns
|
||||||
`,
|
`,
|
||||||
SuggestFor: []string{"delpoy", "deplyo"},
|
SuggestFor: []string{"delpoy", "deplyo"},
|
||||||
PreRunE: bindEnv("image", "namespace", "path", "registry", "confirm", "build"),
|
PreRunE: bindEnv("image", "namespace", "path", "registry", "confirm", "build"),
|
||||||
RunE: runDeploy,
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
|
||||||
|
cmd.Flags().StringArrayP("env", "e", []string{}, "Environment variable to set in the form NAME=VALUE. "+
|
||||||
|
"You may provide this flag multiple times for setting multiple environment variables. "+
|
||||||
|
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
|
||||||
|
cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE")
|
||||||
|
cmd.Flags().StringP("namespace", "n", "", "Namespace of the function to undeploy. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
|
||||||
|
cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
||||||
|
cmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
|
||||||
|
cmd.Flags().BoolP("build", "b", true, "Build the image before deploying (Env: $FUNC_BUILD)")
|
||||||
|
|
||||||
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runDeploy(cmd, args, clientFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDeploy(cmd *cobra.Command, _ []string) (err error) {
|
func runDeploy(cmd *cobra.Command, _ []string, clientFn deployClientFn) (err error) {
|
||||||
|
|
||||||
config, err := newDeployConfig(cmd)
|
config, err := newDeployConfig(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err = config.Prompt()
|
config, err = config.Prompt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == terminal.InterruptErr {
|
if err == terminal.InterruptErr {
|
||||||
|
@ -120,55 +160,26 @@ func runDeploy(cmd *cobra.Command, _ []string) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := buildpacks.NewBuilder()
|
// Deafult conig namespace is the function's namespace
|
||||||
builder.Verbose = config.Verbose
|
if config.Namespace == "" {
|
||||||
|
config.Namespace = function.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
pusher, err := docker.NewPusher(docker.WithCredentialsProvider(credentialsProvider))
|
client, err := clientFn(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == terminal.InterruptErr {
|
if err == terminal.InterruptErr {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pusher.Verbose = config.Verbose
|
|
||||||
|
|
||||||
ns := config.Namespace
|
|
||||||
if ns == "" {
|
|
||||||
ns = function.Namespace
|
|
||||||
}
|
|
||||||
|
|
||||||
deployer, err := knative.NewDeployer(ns)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
listener := progress.New()
|
|
||||||
defer listener.Done()
|
|
||||||
|
|
||||||
deployer.Verbose = config.Verbose
|
|
||||||
listener.Verbose = config.Verbose
|
|
||||||
|
|
||||||
context := cmd.Context()
|
|
||||||
go func() {
|
|
||||||
<-context.Done()
|
|
||||||
listener.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
client := fn.New(
|
|
||||||
fn.WithVerbose(config.Verbose),
|
|
||||||
fn.WithRegistry(config.Registry), // for deriving image name when --image not provided explicitly.
|
|
||||||
fn.WithBuilder(builder),
|
|
||||||
fn.WithPusher(pusher),
|
|
||||||
fn.WithDeployer(deployer),
|
|
||||||
fn.WithProgressListener(listener))
|
|
||||||
|
|
||||||
if config.Build {
|
if config.Build {
|
||||||
if err := client.Build(context, config.Path); err != nil {
|
if err := client.Build(cmd.Context(), config.Path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.Deploy(context, config.Path)
|
return client.Deploy(cmd.Context(), config.Path)
|
||||||
|
|
||||||
// NOTE: Namespace is optional, default is that used by k8s client
|
// NOTE: Namespace is optional, default is that used by k8s client
|
||||||
// (for example kubectl usually uses ~/.kube/config)
|
// (for example kubectl usually uses ~/.kube/config)
|
||||||
|
|
|
@ -16,39 +16,62 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
root.AddCommand(describeCmd)
|
root.AddCommand(NewDescribeCmd(newDescribeClient))
|
||||||
describeCmd.Flags().StringP("namespace", "n", "", "Namespace of the function. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
|
|
||||||
describeCmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml|url) (Env: $FUNC_OUTPUT)")
|
|
||||||
describeCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
|
||||||
|
|
||||||
err := describeCmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var describeCmd = &cobra.Command{
|
func newDescribeClient(cfg describeConfig) (*fn.Client, error) {
|
||||||
Use: "describe <name>",
|
describer, err := knative.NewDescriber(cfg.Namespace)
|
||||||
Short: "Show details of a function",
|
if err != nil {
|
||||||
Long: `Show details of a function
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
describer.Verbose = cfg.Verbose
|
||||||
|
|
||||||
|
return fn.New(
|
||||||
|
fn.WithDescriber(describer),
|
||||||
|
fn.WithVerbose(cfg.Verbose),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type describeClientFn func(describeConfig) (*fn.Client, error)
|
||||||
|
|
||||||
|
func NewDescribeCmd(clientFn describeClientFn) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "describe <name>",
|
||||||
|
Short: "Show details of a function",
|
||||||
|
Long: `Show details of a function
|
||||||
|
|
||||||
Prints the name, route and any event subscriptions for a deployed function in
|
Prints the name, route and any event subscriptions for a deployed function in
|
||||||
the current directory or from the directory specified with --path.
|
the current directory or from the directory specified with --path.
|
||||||
`,
|
`,
|
||||||
Example: `
|
Example: `
|
||||||
# Show the details of a function as declared in the local func.yaml
|
# Show the details of a function as declared in the local func.yaml
|
||||||
kn func describe
|
kn func describe
|
||||||
|
|
||||||
# Show the details of the function in the myotherfunc directory with yaml output
|
# Show the details of the function in the myotherfunc directory with yaml output
|
||||||
kn func describe --output yaml --path myotherfunc
|
kn func describe --output yaml --path myotherfunc
|
||||||
`,
|
`,
|
||||||
SuggestFor: []string{"desc", "get"},
|
SuggestFor: []string{"desc", "get"},
|
||||||
ValidArgsFunction: CompleteFunctionList,
|
ValidArgsFunction: CompleteFunctionList,
|
||||||
PreRunE: bindEnv("namespace", "output", "path"),
|
PreRunE: bindEnv("namespace", "output", "path"),
|
||||||
RunE: runDescribe,
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("namespace", "n", "", "Namespace of the function. By default, the namespace in func.yaml is used or the actual active namespace if not set in the configuration. (Env: $FUNC_NAMESPACE)")
|
||||||
|
cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml|url) (Env: $FUNC_OUTPUT)")
|
||||||
|
cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
||||||
|
|
||||||
|
if err := cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList); err != nil {
|
||||||
|
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runDescribe(cmd, args, clientFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDescribe(cmd *cobra.Command, args []string) (err error) {
|
func runDescribe(cmd *cobra.Command, args []string, clientFn describeClientFn) (err error) {
|
||||||
config := newDescribeConfig(args)
|
config := newDescribeConfig(args)
|
||||||
|
|
||||||
function, err := fn.NewFunction(config.Path)
|
function, err := fn.NewFunction(config.Path)
|
||||||
|
@ -61,16 +84,13 @@ func runDescribe(cmd *cobra.Command, args []string) (err error) {
|
||||||
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
|
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
describer, err := knative.NewDescriber(config.Namespace)
|
// Create a client
|
||||||
|
client, err := clientFn(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
describer.Verbose = config.Verbose
|
|
||||||
|
|
||||||
client := fn.New(
|
|
||||||
fn.WithVerbose(config.Verbose),
|
|
||||||
fn.WithDescriber(describer))
|
|
||||||
|
|
||||||
|
// Get the description
|
||||||
d, err := client.Describe(cmd.Context(), config.Name, config.Path)
|
d, err := client.Describe(cmd.Context(), config.Name, config.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
193
cmd/emit.go
193
cmd/emit.go
|
@ -1,7 +1,8 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
fn "github.com/boson-project/func"
|
fn "github.com/boson-project/func"
|
||||||
|
@ -13,27 +14,41 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
e := cloudevents.NewEmitter()
|
root.AddCommand(NewEmitCmd(newEmitClient))
|
||||||
root.AddCommand(emitCmd)
|
|
||||||
// TODO: do these env vars make sense?
|
|
||||||
emitCmd.Flags().StringP("sink", "k", "", "Send the CloudEvent to the function running at [sink]. The special value \"local\" can be used to send the event to a function running on the local host. When provided, the --path flag is ignored (Env: $FUNC_SINK)")
|
|
||||||
emitCmd.Flags().StringP("source", "s", e.Source, "CloudEvent source (Env: $FUNC_SOURCE)")
|
|
||||||
emitCmd.Flags().StringP("type", "t", e.Type, "CloudEvent type (Env: $FUNC_TYPE)")
|
|
||||||
emitCmd.Flags().StringP("id", "i", uuid.NewString(), "CloudEvent ID (Env: $FUNC_ID)")
|
|
||||||
emitCmd.Flags().StringP("data", "d", "", "Any arbitrary string to be sent as the CloudEvent data. Ignored if --file is provided (Env: $FUNC_DATA)")
|
|
||||||
emitCmd.Flags().StringP("file", "f", "", "Path to a local file containing CloudEvent data to be sent (Env: $FUNC_FILE)")
|
|
||||||
emitCmd.Flags().StringP("content-type", "c", "application/json", "The MIME Content-Type for the CloudEvent data (Env: $FUNC_CONTENT_TYPE)")
|
|
||||||
emitCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory. Ignored when --sink is provided (Env: $FUNC_PATH)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var emitCmd = &cobra.Command{
|
// create a fn.Client with an instance of a
|
||||||
Use: "emit",
|
func newEmitClient(cfg emitConfig) (*fn.Client, error) {
|
||||||
Short: "Emit a CloudEvent to a function endpoint",
|
e := cloudevents.NewEmitter()
|
||||||
Long: `Emit event
|
e.Id = cfg.Id
|
||||||
|
e.Source = cfg.Source
|
||||||
|
e.Type = cfg.Type
|
||||||
|
e.ContentType = cfg.ContentType
|
||||||
|
e.Data = cfg.Data
|
||||||
|
if cfg.File != "" {
|
||||||
|
// See config.Validate for --Data and --file exclusivity enforcement
|
||||||
|
b, err := ioutil.ReadFile(cfg.File)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.Data = string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn.New(fn.WithEmitter(e)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type emitClientFn func(emitConfig) (*fn.Client, error)
|
||||||
|
|
||||||
|
func NewEmitCmd(clientFn emitClientFn) *cobra.Command {
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "emit",
|
||||||
|
Short: "Emit a CloudEvent to a function endpoint",
|
||||||
|
Long: `Emit event
|
||||||
|
|
||||||
Emits a CloudEvent, sending it to the deployed function.
|
Emits a CloudEvent, sending it to the deployed function.
|
||||||
`,
|
`,
|
||||||
Example: `
|
Example: `
|
||||||
# Send a CloudEvent to the deployed function with no data and default values
|
# Send a CloudEvent to the deployed function with no data and default values
|
||||||
# for source, type and ID
|
# for source, type and ID
|
||||||
kn func emit
|
kn func emit
|
||||||
|
@ -54,66 +69,102 @@ kn func emit --path /path/to/fn -i fn.test
|
||||||
# Send a CloudEvent to an arbitrary endpoint
|
# Send a CloudEvent to an arbitrary endpoint
|
||||||
kn func emit --sink "http://my.event.broker.com"
|
kn func emit --sink "http://my.event.broker.com"
|
||||||
`,
|
`,
|
||||||
SuggestFor: []string{"meit", "emti", "send"},
|
SuggestFor: []string{"meit", "emti", "send"},
|
||||||
PreRunE: bindEnv("source", "type", "id", "data", "file", "path", "sink", "content-type"),
|
PreRunE: bindEnv("source", "type", "id", "data", "file", "path", "sink", "content-type"),
|
||||||
RunE: runEmit,
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringP("sink", "k", "", "Send the CloudEvent to the function running at [sink]. The special value \"local\" can be used to send the event to a function running on the local host. When provided, the --path flag is ignored (Env: $FUNC_SINK)")
|
||||||
|
cmd.Flags().StringP("source", "s", cloudevents.DefaultSource, "CloudEvent source (Env: $FUNC_SOURCE)")
|
||||||
|
cmd.Flags().StringP("type", "t", cloudevents.DefaultType, "CloudEvent type (Env: $FUNC_TYPE)")
|
||||||
|
cmd.Flags().StringP("id", "i", uuid.NewString(), "CloudEvent ID (Env: $FUNC_ID)")
|
||||||
|
cmd.Flags().StringP("data", "d", "", "Any arbitrary string to be sent as the CloudEvent data. Ignored if --file is provided (Env: $FUNC_DATA)")
|
||||||
|
cmd.Flags().StringP("file", "f", "", "Path to a local file containing CloudEvent data to be sent (Env: $FUNC_FILE)")
|
||||||
|
cmd.Flags().StringP("content-type", "c", "application/json", "The MIME Content-Type for the CloudEvent data (Env: $FUNC_CONTENT_TYPE)")
|
||||||
|
cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory. Ignored when --sink is provided (Env: $FUNC_PATH)")
|
||||||
|
|
||||||
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runEmit(cmd, args, clientFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runEmit(cmd *cobra.Command, args []string) (err error) {
|
func runEmit(cmd *cobra.Command, _ []string, clientFn emitClientFn) (err error) {
|
||||||
config := newEmitConfig()
|
config := newEmitConfig()
|
||||||
var endpoint string
|
|
||||||
if config.Sink != "" {
|
// Validate things like invalid config combinations.
|
||||||
if config.Sink == "local" {
|
if err := config.Validate(); err != nil {
|
||||||
endpoint = "http://localhost:8080"
|
return err
|
||||||
} else {
|
|
||||||
endpoint = config.Sink
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var f fn.Function
|
|
||||||
f, err = fn.NewFunction(config.Path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// What happens if the function hasn't been deployed but they don't run with --local=true
|
|
||||||
// Maybe we should be thinking about saving the endpoint URL in func.yaml after each deploy
|
|
||||||
var d *knative.Describer
|
|
||||||
d, err = knative.NewDescriber("")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var desc fn.Description
|
|
||||||
desc, err = d.Describe(cmd.Context(), f.Name)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Use the first available route
|
|
||||||
endpoint = desc.Routes[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter := cloudevents.NewEmitter()
|
// Determine the final endpoint, taking into account the special value "local",
|
||||||
emitter.Source = config.Source
|
// and sampling the function's current route if not explicitly provided
|
||||||
emitter.Type = config.Type
|
endpoint, err := endpoint(cmd.Context(), config)
|
||||||
emitter.Id = config.Id
|
if err != nil {
|
||||||
emitter.ContentType = config.ContentType
|
return err
|
||||||
emitter.Data = config.Data
|
|
||||||
if config.File != "" {
|
|
||||||
var buf []byte
|
|
||||||
if emitter.Data != "" && config.Verbose {
|
|
||||||
return fmt.Errorf("Only one of --data and --file may be specified \n")
|
|
||||||
}
|
|
||||||
buf, err = ioutil.ReadFile(config.File)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
emitter.Data = string(buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client := fn.New(
|
// Instantiate a client based on the final value of config
|
||||||
fn.WithEmitter(emitter),
|
client, err := clientFn(config)
|
||||||
)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit the event to the endpoint
|
||||||
return client.Emit(cmd.Context(), endpoint)
|
return client.Emit(cmd.Context(), endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// endpoint returns the final effective endpoint.
|
||||||
|
// By default, the contextually active Function is queried for it's current
|
||||||
|
// address (route).
|
||||||
|
// If "local" is specified in cfg.Sink, localhost is used.
|
||||||
|
// Otherwise the value of Sink is used verbatim if defined.
|
||||||
|
func endpoint(ctx context.Context, cfg emitConfig) (url string, err error) {
|
||||||
|
var (
|
||||||
|
f fn.Function
|
||||||
|
d fn.Describer
|
||||||
|
desc fn.Description
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the special value "local" was requested,
|
||||||
|
// use localhost.
|
||||||
|
if cfg.Sink == "local" {
|
||||||
|
return "http://localhost:8080", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a sink was expressly provided, use that verbatim
|
||||||
|
if cfg.Sink != "" {
|
||||||
|
return cfg.Sink, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no sink was specified, use the route to the currently
|
||||||
|
// contectually active function
|
||||||
|
if f, err = fn.NewFunction(cfg.Path); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Decide what happens if the function hasn't been deployed but they
|
||||||
|
// don't run with --local=true. Perhaps an error in .Validate()?
|
||||||
|
if d, err = knative.NewDescriber(""); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current state of the function.
|
||||||
|
if desc, err = d.Describe(ctx, f.Name); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probably wise to be defensive here:
|
||||||
|
if len(desc.Routes) == 0 {
|
||||||
|
err = errors.New("function has no active routes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first route should be the destination.
|
||||||
|
return desc.Routes[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
type emitConfig struct {
|
type emitConfig struct {
|
||||||
Path string
|
Path string
|
||||||
Source string
|
Source string
|
||||||
|
@ -139,3 +190,11 @@ func newEmitConfig() emitConfig {
|
||||||
Verbose: viper.GetBool("verbose"),
|
Verbose: viper.GetBool("verbose"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c emitConfig) Validate() error {
|
||||||
|
if c.Data != "" && c.File != "" {
|
||||||
|
return errors.New("Only one of --data or --file may be specified")
|
||||||
|
}
|
||||||
|
// TODO: should we verify that sink is a url or "local"?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
115
cmd/list.go
115
cmd/list.go
|
@ -3,6 +3,7 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -17,24 +18,36 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
root.AddCommand(listCmd)
|
root.AddCommand(NewListCmd(newListClient))
|
||||||
listCmd.Flags().BoolP("all-namespaces", "A", false, "List functions in all namespaces. If set, the --namespace flag is ignored.")
|
|
||||||
listCmd.Flags().StringP("namespace", "n", "", "Namespace to search for functions. By default, the functions of the actual active namespace are listed. (Env: $FUNC_NAMESPACE)")
|
|
||||||
listCmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml) (Env: $FUNC_OUTPUT)")
|
|
||||||
err := listCmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var listCmd = &cobra.Command{
|
func newListClient(cfg listConfig) (*fn.Client, error) {
|
||||||
Use: "list",
|
// TODO(lkingland): does an empty namespace mean all namespaces
|
||||||
Short: "List functions",
|
// or the default namespace as defined in user's config?
|
||||||
Long: `List functions
|
lister, err := knative.NewLister(cfg.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lister.Verbose = cfg.Verbose
|
||||||
|
|
||||||
|
return fn.New(
|
||||||
|
fn.WithLister(lister),
|
||||||
|
fn.WithVerbose(cfg.Verbose),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type listClientFn func(listConfig) (*fn.Client, error)
|
||||||
|
|
||||||
|
func NewListCmd(clientFn listClientFn) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List functions",
|
||||||
|
Long: `List functions
|
||||||
|
|
||||||
Lists all deployed functions in a given namespace.
|
Lists all deployed functions in a given namespace.
|
||||||
`,
|
`,
|
||||||
Example: `
|
Example: `
|
||||||
# List all functions in the current namespace with human readable output
|
# List all functions in the current namespace with human readable output
|
||||||
kn func list
|
kn func list
|
||||||
|
|
||||||
|
@ -44,40 +57,51 @@ kn func list --namespace test --output yaml
|
||||||
# List all functions in all namespaces with JSON output
|
# List all functions in all namespaces with JSON output
|
||||||
kn func list --all-namespaces --output json
|
kn func list --all-namespaces --output json
|
||||||
`,
|
`,
|
||||||
SuggestFor: []string{"ls", "lsit"},
|
SuggestFor: []string{"ls", "lsit"},
|
||||||
PreRunE: bindEnv("namespace", "output"),
|
PreRunE: bindEnv("namespace", "output"),
|
||||||
RunE: runList,
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolP("all-namespaces", "A", false, "List functions in all namespaces. If set, the --namespace flag is ignored.")
|
||||||
|
cmd.Flags().StringP("namespace", "n", "", "Namespace to search for functions. By default, the functions of the actual active namespace are listed. (Env: $FUNC_NAMESPACE)")
|
||||||
|
cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml) (Env: $FUNC_OUTPUT)")
|
||||||
|
|
||||||
|
if err := cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList); err != nil {
|
||||||
|
fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runList(cmd, args, clientFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runList(cmd *cobra.Command, args []string) (err error) {
|
func runList(cmd *cobra.Command, _ []string, clientFn listClientFn) (err error) {
|
||||||
config := newListConfig()
|
config := newListConfig()
|
||||||
|
|
||||||
lister, err := knative.NewLister(config.Namespace)
|
if err := config.Validate(); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return
|
|
||||||
}
|
|
||||||
lister.Verbose = config.Verbose
|
|
||||||
|
|
||||||
a, err := cmd.Flags().GetBool("all-namespaces")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if a {
|
|
||||||
lister.Namespace = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client := fn.New(
|
client, err := clientFn(config)
|
||||||
fn.WithVerbose(config.Verbose),
|
if err != nil {
|
||||||
fn.WithLister(lister))
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
items, err := client.List(cmd.Context())
|
items, err := client.List(cmd.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(items) < 1 {
|
if len(items) == 0 {
|
||||||
fmt.Printf("No functions found in %v namespace\n", lister.Namespace)
|
// TODO(lkingland): this isn't particularly script friendly. Suggest this
|
||||||
return
|
// prints bo only on --verbose. Possible future tweak, as I don't want to
|
||||||
|
// make functional changes during a refactor.
|
||||||
|
if config.Namespace != "" && !config.AllNamespaces {
|
||||||
|
fmt.Printf("No functions found in '%v' namespace\n", config.Namespace)
|
||||||
|
} else {
|
||||||
|
fmt.Println("No functions found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write(os.Stdout, listItems(items), config.Output)
|
write(os.Stdout, listItems(items), config.Output)
|
||||||
|
@ -89,19 +113,28 @@ func runList(cmd *cobra.Command, args []string) (err error) {
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
|
|
||||||
type listConfig struct {
|
type listConfig struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
Output string
|
Output string
|
||||||
Verbose bool
|
AllNamespaces bool
|
||||||
|
Verbose bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newListConfig() listConfig {
|
func newListConfig() listConfig {
|
||||||
return listConfig{
|
return listConfig{
|
||||||
Namespace: viper.GetString("namespace"),
|
Namespace: viper.GetString("namespace"),
|
||||||
Output: viper.GetString("output"),
|
Output: viper.GetString("output"),
|
||||||
Verbose: viper.GetBool("verbose"),
|
AllNamespaces: viper.GetBool("all-namespaces"),
|
||||||
|
Verbose: viper.GetBool("verbose"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c listConfig) Validate() error {
|
||||||
|
if c.Namespace != "" && c.AllNamespaces {
|
||||||
|
return errors.New("Both --namespace and --all-namespaces specified.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Output Formatting (serializers)
|
// Output Formatting (serializers)
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
|
|
||||||
|
|
63
cmd/run.go
63
cmd/run.go
|
@ -12,35 +12,54 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add the run command as a subcommand of root.
|
root.AddCommand(NewRunCmd(newRunClient))
|
||||||
root.AddCommand(runCmd)
|
|
||||||
runCmd.Flags().StringArrayP("env", "e", []string{}, "Environment variable to set in the form NAME=VALUE. "+
|
|
||||||
"You may provide this flag multiple times for setting multiple environment variables. "+
|
|
||||||
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
|
|
||||||
runCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var runCmd = &cobra.Command{
|
func newRunClient(cfg runConfig) *fn.Client {
|
||||||
Use: "run",
|
runner := docker.NewRunner()
|
||||||
Short: "Run the function locally",
|
runner.Verbose = cfg.Verbose
|
||||||
Long: `Run the function locally
|
return fn.New(
|
||||||
|
fn.WithRunner(runner),
|
||||||
|
fn.WithVerbose(cfg.Verbose))
|
||||||
|
}
|
||||||
|
|
||||||
|
type runClientFn func(runConfig) *fn.Client
|
||||||
|
|
||||||
|
func NewRunCmd(clientFn runClientFn) *cobra.Command {
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "Run the function locally",
|
||||||
|
Long: `Run the function locally
|
||||||
|
|
||||||
Runs the function locally in the current directory or in the directory
|
Runs the function locally in the current directory or in the directory
|
||||||
specified by --path flag. The function must already have been built with the 'build' command.
|
specified by --path flag. The function must already have been built with the 'build' command.
|
||||||
`,
|
`,
|
||||||
Example: `
|
Example: `
|
||||||
# Build function's image first
|
# Build function's image first
|
||||||
kn func build
|
kn func build
|
||||||
|
|
||||||
# Run it locally as a container
|
# Run it locally as a container
|
||||||
kn func run
|
kn func run
|
||||||
`,
|
`,
|
||||||
SuggestFor: []string{"rnu"},
|
SuggestFor: []string{"rnu"},
|
||||||
PreRunE: bindEnv("path"),
|
PreRunE: bindEnv("path"),
|
||||||
RunE: runRun,
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringArrayP("env", "e", []string{},
|
||||||
|
"Environment variable to set in the form NAME=VALUE. "+
|
||||||
|
"You may provide this flag multiple times for setting multiple environment variables. "+
|
||||||
|
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
|
||||||
|
cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
|
||||||
|
|
||||||
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runRun(cmd, args, clientFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRun(cmd *cobra.Command, args []string) (err error) {
|
func runRun(cmd *cobra.Command, args []string, clientFn runClientFn) (err error) {
|
||||||
config, err := newRunConfig(cmd)
|
config, err := newRunConfig(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -66,15 +85,9 @@ func runRun(cmd *cobra.Command, args []string) (err error) {
|
||||||
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
|
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
runner := docker.NewRunner()
|
client := clientFn(config)
|
||||||
runner.Verbose = config.Verbose
|
|
||||||
|
|
||||||
client := fn.New(
|
return client.Run(cmd.Context(), config.Path)
|
||||||
fn.WithRunner(runner),
|
|
||||||
fn.WithVerbose(config.Verbose))
|
|
||||||
|
|
||||||
err = client.Run(cmd.Context(), config.Path)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type runConfig struct {
|
type runConfig struct {
|
||||||
|
@ -92,10 +105,10 @@ type runConfig struct {
|
||||||
EnvToRemove []string
|
EnvToRemove []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRunConfig(cmd *cobra.Command) (runConfig, error) {
|
func newRunConfig(cmd *cobra.Command) (c runConfig, err error) {
|
||||||
envToUpdate, envToRemove, err := envFromCmd(cmd)
|
envToUpdate, envToRemove, err := envFromCmd(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return runConfig{}, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return runConfig{
|
return runConfig{
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultWaitingTimeout = 60 * time.Second
|
DefaultWaitingTimeout = 120 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewServingClient(namespace string) (clientservingv1.KnServingClient, error) {
|
func NewServingClient(namespace string) (clientservingv1.KnServingClient, error) {
|
||||||
|
|
|
@ -153,6 +153,13 @@ func (b *Bar) Complete(text string) {
|
||||||
b.Done() // stop spinner
|
b.Done() // stop spinner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stopping indicates the process is stopping, such as having received a context
|
||||||
|
// cancellation.
|
||||||
|
func (b *Bar) Stopping() {
|
||||||
|
// currently stopping is equivalent in effect to Done
|
||||||
|
b.Done()
|
||||||
|
}
|
||||||
|
|
||||||
// Done cancels the write loop if being used.
|
// Done cancels the write loop if being used.
|
||||||
// Call in a defer statement after creation to ensure that the spinner stops
|
// Call in a defer statement after creation to ensure that the spinner stops
|
||||||
func (b *Bar) Done() {
|
func (b *Bar) Done() {
|
||||||
|
|
Loading…
Reference in New Issue