diff --git a/pkg/fission-cli/cli.go b/pkg/fission-cli/cli.go index 62c550df..cb7432f5 100644 --- a/pkg/fission-cli/cli.go +++ b/pkg/fission-cli/cli.go @@ -19,6 +19,7 @@ package fission_cli import ( "encoding/json" "fmt" + "net/http" "os" "strings" "time" @@ -31,6 +32,7 @@ import ( "github.com/fission/fission/pkg/fission-cli/cmd" "github.com/fission/fission/pkg/fission-cli/cmd/canaryconfig" "github.com/fission/fission/pkg/fission-cli/cmd/environment" + "github.com/fission/fission/pkg/fission-cli/cmd/function" "github.com/fission/fission/pkg/fission-cli/cmd/httptrigger" "github.com/fission/fission/pkg/fission-cli/cmd/kubewatch" _package "github.com/fission/fission/pkg/fission-cli/cmd/package" @@ -91,7 +93,7 @@ func NewCliApp() *cli.App { canaryNamespaceFlag := cli.StringFlag{Name: "canaryNamespace, canaryns", Value: metav1.NamespaceDefault, Usage: "Namespace for canary config object"} // trigger method and url flags (used in function and route CLIs) - htMethodFlag := cli.StringFlag{Name: "method", Value: "GET", Usage: "HTTP Method: GET|POST|PUT|DELETE|HEAD"} + htMethodFlag := cli.StringFlag{Name: "method", Value: http.MethodGet, Usage: "HTTP Method: GET|POST|PUT|DELETE|HEAD"} htUrlFlag := cli.StringFlag{Name: "url", Usage: "URL pattern (See gorilla/mux supported patterns)"} // Resource & scale related flags (Used in env and function) @@ -131,18 +133,18 @@ func NewCliApp() *cli.App { fnTimeoutFlag := cli.DurationFlag{Name: "timeout, t", Value: 30 * time.Second, Usage: "The length of time to wait for the response. If set to zero or negative number, no timeout is set."} fnSubcommands := []cli.Command{ - {Name: "create", Usage: "Create new function (and optionally, an HTTP route to it)", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag, fnEnvNameFlag, envNamespaceFlag, specSaveFlag, fnCodeFlag, fnSrcArchiveFlag, fnDeployArchiveFlag, fnKeepURLFlag, fnEntryPointFlag, fnBuildCmdFlag, fnPkgNameFlag, htUrlFlag, htMethodFlag, minCpu, maxCpu, minMem, maxMem, minScale, maxScale, fnExecutorTypeFlag, targetcpu, fnCfgMapFlag, fnSecretFlag, specializationTimeoutFlag, fnExecutionTimeoutFlag}, Action: fnCreate}, - {Name: "get", Usage: "Get function source code", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag}, Action: fnGet}, - {Name: "getmeta", Usage: "Get function metadata", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag}, Action: fnGetMeta}, - {Name: "update", Usage: "Update function source code", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag, fnEnvNameFlag, envNamespaceFlag, fnCodeFlag, fnSrcArchiveFlag, fnDeployArchiveFlag, fnKeepURLFlag, fnEntryPointFlag, fnPkgNameFlag, pkgNamespaceFlag, fnBuildCmdFlag, fnForceFlag, minCpu, maxCpu, minMem, maxMem, minScale, maxScale, fnExecutorTypeFlag, targetcpu, specializationTimeoutFlag, fnExecutionTimeoutFlag, fnSecretFlag, fnCfgMapFlag}, Action: fnUpdate}, - {Name: "delete", Usage: "Delete function", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag}, Action: fnDelete}, + {Name: "create", Usage: "Create new function (and optionally, an HTTP route to it)", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag, fnEnvNameFlag, envNamespaceFlag, specSaveFlag, fnCodeFlag, fnSrcArchiveFlag, fnDeployArchiveFlag, fnKeepURLFlag, fnEntryPointFlag, fnBuildCmdFlag, fnPkgNameFlag, htUrlFlag, htMethodFlag, minCpu, maxCpu, minMem, maxMem, minScale, maxScale, fnExecutorTypeFlag, targetcpu, fnCfgMapFlag, fnSecretFlag, specializationTimeoutFlag, fnExecutionTimeoutFlag}, Action: urfavecli.Wrapper(function.Create)}, + {Name: "get", Usage: "Get function source code", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag}, Action: urfavecli.Wrapper(function.Get)}, + {Name: "getmeta", Usage: "Get function metadata", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag}, Action: urfavecli.Wrapper(function.GetMeta)}, + {Name: "update", Usage: "Update function source code", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag, fnEnvNameFlag, envNamespaceFlag, fnCodeFlag, fnSrcArchiveFlag, fnDeployArchiveFlag, fnKeepURLFlag, fnEntryPointFlag, fnPkgNameFlag, pkgNamespaceFlag, fnBuildCmdFlag, fnForceFlag, minCpu, maxCpu, minMem, maxMem, minScale, maxScale, fnExecutorTypeFlag, targetcpu, specializationTimeoutFlag, fnExecutionTimeoutFlag, fnSecretFlag, fnCfgMapFlag}, Action: urfavecli.Wrapper(function.Update)}, + {Name: "delete", Usage: "Delete function", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag}, Action: urfavecli.Wrapper(function.Delete)}, // TODO : for fnList, i feel like it's nice to allow --fns all, to list functions across all namespaces for cluster admins, although, this is against ns isolation. // so, in the future, if we end up using kubeconfig in fission cli and enforcing rolebindings to be created for users by admins etc, we can add this option at the time. - {Name: "list", Usage: "List all functions in a namespace if specified, else, list functions across all namespaces", Flags: []cli.Flag{fnNamespaceFlag}, Action: fnList}, - {Name: "logs", Usage: "Display function logs", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag, fnPodFlag, fnFollowFlag, fnDetailFlag, fnLogDBTypeFlag, fnLogReverseQueryFlag, fnLogCountFlag}, Action: fnLogs}, + {Name: "list", Usage: "List all functions in a namespace if specified, else, list functions across all namespaces", Flags: []cli.Flag{fnNamespaceFlag}, Action: urfavecli.Wrapper(function.List)}, + {Name: "logs", Usage: "Display function logs", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag, fnPodFlag, fnFollowFlag, fnDetailFlag, fnLogDBTypeFlag, fnLogReverseQueryFlag, fnLogCountFlag}, Action: urfavecli.Wrapper(function.Log)}, {Name: "test", Usage: "Test a function", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag, fnEnvNameFlag, fnCodeFlag, fnSrcArchiveFlag, htMethodFlag, fnBodyFlag, fnHeaderFlag, fnQueryFlag, fnTimeoutFlag}, - Action: fnTest}, + Action: urfavecli.Wrapper(function.Test)}, } // httptriggers diff --git a/pkg/fission-cli/cliwrapper/cli/cli.go b/pkg/fission-cli/cliwrapper/cli/cli.go index 8bfd96ca..4152f024 100644 --- a/pkg/fission-cli/cliwrapper/cli/cli.go +++ b/pkg/fission-cli/cliwrapper/cli/cli.go @@ -16,6 +16,10 @@ limitations under the License. package cli +import ( + "time" +) + type ( Input interface { //Parse(input interface{}) error @@ -30,19 +34,19 @@ type ( // String returns string value of given flag. String(key string) string - // StringSlice returns string slice of given flag.. + // StringSlice returns string slice of given flag. StringSlice(key string) []string - // Int returns int value of given flag.nd false. + // Int returns int value of given flag. Int(key string) int - // IntSlice returns int slice of given flag.lse. + // IntSlice returns int slice of given flag. IntSlice(key string) []int - // Int64 returns int64 value of given flag. false. + // Int64 returns int64 value of given flag. Int64(key string) int64 - // Int64Slice returns int64 slice of given flag.e. + // Int64Slice returns int64 slice of given flag. Int64Slice(key string) []int64 // GlobalBool returns true if given global flag has been set; @@ -66,5 +70,8 @@ type ( // GlobalInt64Slice returns global int64 slice of given flag. GlobalInt64Slice(key string) []int64 + + // Duration returns time duration of given flag. + Duration(key string) time.Duration } ) diff --git a/pkg/fission-cli/cliwrapper/driver/dummy/dummy.go b/pkg/fission-cli/cliwrapper/driver/dummy/dummy.go new file mode 100644 index 00000000..e3b0f6df --- /dev/null +++ b/pkg/fission-cli/cliwrapper/driver/dummy/dummy.go @@ -0,0 +1,166 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dummy + +import ( + "time" + + fCli "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" +) + +var _ fCli.Input = &Cli{} + +type Cli struct { + c map[string]interface{} +} + +// TestFlagSet returns a flag set for unit test purpose. +func TestFlagSet() Cli { + return Cli{c: make(map[string]interface{})} +} + +// Set allows to set any kinds of value with given key. +// The type of set value should be matched with the returned +// type of GetXXX function. +func (u Cli) Set(Key string, value interface{}) { + u.c[Key] = value +} + +func (u Cli) IsSet(key string) bool { + _, ok := u.c[key] + return ok +} + +func (u Cli) Bool(key string) bool { + val, ok := u.c[key] + if !ok || val == nil { + return false + } + return val.(bool) +} + +func (u Cli) String(key string) string { + val, ok := u.c[key] + if !ok || val == nil { + return "" + } + return val.(string) +} + +func (u Cli) StringSlice(key string) []string { + val, ok := u.c[key] + if !ok || val == nil { + return nil + } + return val.([]string) +} + +func (u Cli) Int(key string) int { + val, ok := u.c[key] + if !ok || val == nil { + return 0 + } + return val.(int) +} + +func (u Cli) IntSlice(key string) []int { + val, ok := u.c[key] + if !ok || val == nil { + return nil + } + return val.([]int) +} + +func (u Cli) Int64(key string) int64 { + val, ok := u.c[key] + if !ok || val == nil { + return 0 + } + return val.(int64) +} + +func (u Cli) Int64Slice(key string) []int64 { + val, ok := u.c[key] + if !ok || val == nil { + return nil + } + return val.([]int64) +} + +func (u Cli) GlobalBool(key string) bool { + val, ok := u.c[key] + if !ok || val == nil { + return false + } + return val.(bool) +} + +func (u Cli) GlobalString(key string) string { + val, ok := u.c[key] + if !ok || val == nil { + return "" + } + return val.(string) +} + +func (u Cli) GlobalStringSlice(key string) []string { + val, ok := u.c[key] + if !ok || val == nil { + return nil + } + return val.([]string) +} + +func (u Cli) GlobalInt(key string) int { + val, ok := u.c[key] + if !ok || val == nil { + return 0 + } + return val.(int) +} + +func (u Cli) GlobalIntSlice(key string) []int { + val, ok := u.c[key] + if !ok || val == nil { + return nil + } + return val.([]int) +} + +func (u Cli) GlobalInt64(key string) int64 { + val, ok := u.c[key] + if !ok || val == nil { + return 0 + } + return val.(int64) +} + +func (u Cli) GlobalInt64Slice(key string) []int64 { + val, ok := u.c[key] + if !ok || val == nil { + return nil + } + return val.([]int64) +} + +func (u Cli) Duration(key string) time.Duration { + val, ok := u.c[key] + if !ok || val == nil { + return 0 + } + return val.(time.Duration) +} diff --git a/pkg/fission-cli/cliwrapper/driver/urfavecli/urfavecli.go b/pkg/fission-cli/cliwrapper/driver/urfavecli/urfavecli.go index dd7a758e..b673cbf1 100644 --- a/pkg/fission-cli/cliwrapper/driver/urfavecli/urfavecli.go +++ b/pkg/fission-cli/cliwrapper/driver/urfavecli/urfavecli.go @@ -18,6 +18,7 @@ package urfavecli import ( "log" + "time" "github.com/urfave/cli" @@ -107,3 +108,7 @@ func (u Cli) GlobalInt64(key string) int64 { func (u Cli) GlobalInt64Slice(key string) []int64 { return u.c.GlobalInt64Slice(key) } + +func (u Cli) Duration(key string) time.Duration { + return u.c.Duration(key) +} diff --git a/pkg/fission-cli/cmd/function/create.go b/pkg/fission-cli/cmd/function/create.go new file mode 100644 index 00000000..d1e754f9 --- /dev/null +++ b/pkg/fission-cli/cmd/function/create.go @@ -0,0 +1,436 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" + apiv1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + fv1 "github.com/fission/fission/pkg/apis/fission.io/v1" + "github.com/fission/fission/pkg/controller/client" + ferror "github.com/fission/fission/pkg/error" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" + "github.com/fission/fission/pkg/fission-cli/cmd/httptrigger" + _package "github.com/fission/fission/pkg/fission-cli/cmd/package" + "github.com/fission/fission/pkg/fission-cli/cmd/spec" + "github.com/fission/fission/pkg/fission-cli/log" + "github.com/fission/fission/pkg/types" +) + +const ( + DEFAULT_MIN_SCALE = 1 + DEFAULT_TARGET_CPU_PERCENTAGE = 80 +) + +type CreateSubCommand struct { + client *client.Client + function *fv1.Function + specFile string +} + +func Create(flags cli.Input) error { + opts := CreateSubCommand{ + client: cmd.GetServer(flags), + } + return opts.do(flags) +} + +func (opts *CreateSubCommand) do(flags cli.Input) error { + err := opts.complete(flags) + if err != nil { + return err + } + return opts.run(flags) +} + +// complete creates a environment objects and populates it with default value and CLI inputs. +func (opts *CreateSubCommand) complete(flags cli.Input) error { + fnNamespace := flags.String("fnNamespace") + envNamespace := flags.String("envNamespace") + + fnName := flags.String("name") + if len(fnName) == 0 { + return errors.New("need --name argument") + } + + // user wants a spec, create a yaml file with package and function + toSpec := false + if flags.Bool("spec") { + toSpec = true + opts.specFile = fmt.Sprintf("function-%v.yaml", fnName) + } + specDir := cmd.GetSpecDir(flags) + + // check for unique function names within a namespace + metadata, err := cmd.GetMetadata("name", "fnNamespace", flags) + if err != nil { + return err + } + + fn, err := opts.client.FunctionGet(metadata) + if err != nil && !ferror.IsNotFound(err) { + return err + } else if fn != nil { + return errors.New("a function with the same name already exists") + } + + entrypoint := flags.String("entrypoint") + + fnTimeout := flags.Int("fntimeout") + if fnTimeout <= 0 { + return errors.New("fntimeout must be greater than 0") + } + + pkgName := flags.String("pkg") + + secretNames := flags.StringSlice("secret") + cfgMapNames := flags.StringSlice("configmap") + + invokeStrategy, err := getInvokeStrategy(flags, nil) + if err != nil { + return err + } + resourceReq, err := cmd.GetResourceReqs(flags, &apiv1.ResourceRequirements{}) + if err != nil { + return err + } + + var pkgMetadata *metav1.ObjectMeta + var envName string + if len(pkgName) > 0 { + // use existing package + pkg, err := opts.client.PackageGet(&metav1.ObjectMeta{ + Namespace: fnNamespace, + Name: pkgName, + }) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("read package in '%v' in Namespace: %s. Package needs to be present in the same namespace as function", pkgName, fnNamespace)) + } + pkgMetadata = &pkg.Metadata + envName = pkg.Spec.Environment.Name + if envName != flags.String("env") { + log.Warn("Function's environment is different than package's environment, package's environment will be used for creating function") + } + envNamespace = pkg.Spec.Environment.Namespace + } else { + // need to specify environment for creating new package + envName = flags.String("env") + if len(envName) == 0 { + return errors.New("need --env argument") + } + + // examine existence of given environment. If specs - then spec validate will do it, don't check here. + if !toSpec { + _, err := opts.client.EnvironmentGet(&metav1.ObjectMeta{ + Namespace: envNamespace, + Name: envName, + }) + if err != nil { + if e, ok := err.(ferror.Error); ok && e.Code == ferror.ErrorNotFound { + log.Warn(fmt.Sprintf("Environment \"%v\" does not exist. Please create the environment before executing the function. \nFor example: `fission env create --name %v --envns %v --image `\n", envName, envName, envNamespace)) + } else { + return errors.Wrap(err, "error retrieving environment information") + } + } + } + + srcArchiveFiles := flags.StringSlice("src") + var deployArchiveFiles []string + noZip := false + code := flags.String("code") + if len(code) == 0 { + deployArchiveFiles = flags.StringSlice("deploy") + } else { + deployArchiveFiles = append(deployArchiveFiles, flags.String("code")) + noZip = true + } + // return error when both src & deploy archive are empty + if len(srcArchiveFiles) == 0 && len(deployArchiveFiles) == 0 { + return errors.New("need --code or --deploy or --src argument") + } + + buildcmd := flags.String("buildcmd") + keepURL := flags.Bool("keepurl") + + // create new package in the same namespace as the function. + pkgMetadata, err = _package.CreatePackage(flags, opts.client, fnNamespace, envName, envNamespace, + srcArchiveFiles, deployArchiveFiles, buildcmd, specDir, opts.specFile, noZip, keepURL) + if err != nil { + return errors.Wrap(err, "error creating package") + } + } + + var secrets []fv1.SecretReference + var cfgmaps []fv1.ConfigMapReference + + if len(secretNames) > 0 { + // check the referenced secret is in the same ns as the function, if not give a warning. + for _, secretName := range secretNames { + _, err := opts.client.SecretGet(&metav1.ObjectMeta{ + Namespace: fnNamespace, + Name: secretName, + }) + if err != nil { + if k8serrors.IsNotFound(err) { + log.Warn(fmt.Sprintf("Secret %s not found in Namespace: %s. Secret needs to be present in the same namespace as function", secretName, fnNamespace)) + } else { + return errors.Wrap(err, "error checking secret") + } + } + } + for _, secretName := range secretNames { + newSecret := fv1.SecretReference{ + Name: secretName, + Namespace: fnNamespace, + } + secrets = append(secrets, newSecret) + } + } + + if len(cfgMapNames) > 0 { + // check the referenced cfgmap is in the same ns as the function, if not give a warning. + for _, cfgMapName := range cfgMapNames { + _, err := opts.client.ConfigMapGet(&metav1.ObjectMeta{ + Namespace: fnNamespace, + Name: cfgMapName, + }) + if err != nil { + if k8serrors.IsNotFound(err) { + log.Warn(fmt.Sprintf("ConfigMap %s not found in Namespace: %s. ConfigMap needs to be present in the same namespace as function", cfgMapName, fnNamespace)) + } else { + return errors.Wrap(err, "error checking configmap") + } + } + } + for _, cfgMapName := range cfgMapNames { + newCfgMap := fv1.ConfigMapReference{ + Name: cfgMapName, + Namespace: fnNamespace, + } + cfgmaps = append(cfgmaps, newCfgMap) + } + } + + opts.function = &fv1.Function{ + Metadata: metav1.ObjectMeta{ + Name: fnName, + Namespace: fnNamespace, + }, + Spec: fv1.FunctionSpec{ + Environment: fv1.EnvironmentReference{ + Name: envName, + Namespace: envNamespace, + }, + Package: fv1.FunctionPackageRef{ + FunctionName: entrypoint, + PackageRef: fv1.PackageRef{ + Namespace: pkgMetadata.Namespace, + Name: pkgMetadata.Name, + ResourceVersion: pkgMetadata.ResourceVersion, + }, + }, + Secrets: secrets, + ConfigMaps: cfgmaps, + Resources: *resourceReq, + InvokeStrategy: *invokeStrategy, + FunctionTimeout: fnTimeout, + }, + } + + return nil +} + +// run write the resource to a spec file or create a fission CRD with remote fission server. +// It also prints warning/error if necessary. +func (opts *CreateSubCommand) run(flags cli.Input) error { + // if we're writing a spec, don't create the function + if flags.Bool("spec") { + err := spec.SpecSave(*opts.function, opts.specFile) + if err != nil { + return errors.Wrap(err, "error creating function spec") + } + return nil + } + + _, err := opts.client.FunctionCreate(opts.function) + if err != nil { + return errors.Wrap(err, "error creating function") + } + + fmt.Printf("function '%v' created\n", opts.function.Metadata.Name) + + // Allow the user to specify an HTTP trigger while creating a function. + triggerUrl := flags.String("url") + if len(triggerUrl) == 0 { + return nil + } + if !strings.HasPrefix(triggerUrl, "/") { + triggerUrl = fmt.Sprintf("/%s", triggerUrl) + } + + method, err := httptrigger.GetMethod(flags.String("method")) + if err != nil { + return errors.Wrap(err, "error getting HTTP trigger method") + } + + triggerName := uuid.NewV4().String() + ht := &fv1.HTTPTrigger{ + Metadata: metav1.ObjectMeta{ + Name: triggerName, + Namespace: opts.function.Metadata.Namespace, + }, + Spec: fv1.HTTPTriggerSpec{ + RelativeURL: triggerUrl, + Method: method, + FunctionReference: fv1.FunctionReference{ + Type: fv1.FunctionReferenceTypeFunctionName, + Name: opts.function.Metadata.Name, + }, + }, + } + _, err = opts.client.HTTPTriggerCreate(ht) + if err != nil { + return errors.Wrap(err, "error creating HTTP trigger") + } + + fmt.Printf("route created: %v %v -> %v\n", method, triggerUrl, opts.function.Metadata.Name) + return nil +} + +func getInvokeStrategy(flags cli.Input, existingInvokeStrategy *fv1.InvokeStrategy) (strategy *fv1.InvokeStrategy, err error) { + + var fnExecutor, newFnExecutor fv1.ExecutorType + + switch flags.String("executortype") { + case "": + fallthrough + case types.ExecutorTypePoolmgr: + newFnExecutor = types.ExecutorTypePoolmgr + case types.ExecutorTypeNewdeploy: + newFnExecutor = types.ExecutorTypeNewdeploy + default: + return nil, errors.New("executor type must be one of 'poolmgr' or 'newdeploy', defaults to 'poolmgr'") + } + + if existingInvokeStrategy != nil { + fnExecutor = existingInvokeStrategy.ExecutionStrategy.ExecutorType + + // override the executor type if user specified a new executor type + if flags.IsSet("executortype") { + fnExecutor = newFnExecutor + } + } else { + fnExecutor = newFnExecutor + } + + if flags.IsSet("specializationtimeout") && fnExecutor != types.ExecutorTypeNewdeploy { + return nil, errors.New("specializationtimeout flag is only applicable for newdeploy type of executor") + } + + if fnExecutor == types.ExecutorTypePoolmgr { + if flags.IsSet("targetcpu") || flags.IsSet("minscale") || flags.IsSet("maxscale") { + return nil, errors.New("to set target CPU or min/max scale for function, please specify \"--executortype newdeploy\"") + } + + if flags.IsSet("mincpu") || flags.IsSet("maxcpu") || flags.IsSet("minmemory") || flags.IsSet("maxmemory") { + log.Warn("To limit CPU/Memory for function with executor type \"poolmgr\", please specify resources limits when creating environment") + } + strategy = &fv1.InvokeStrategy{ + StrategyType: fv1.StrategyTypeExecution, + ExecutionStrategy: fv1.ExecutionStrategy{ + ExecutorType: types.ExecutorTypePoolmgr, + }, + } + } else { + // set default value + targetCPU := DEFAULT_TARGET_CPU_PERCENTAGE + minScale := DEFAULT_MIN_SCALE + maxScale := minScale + specializationTimeout := fv1.DefaultSpecializationTimeOut + + if existingInvokeStrategy != nil && existingInvokeStrategy.ExecutionStrategy.ExecutorType == types.ExecutorTypeNewdeploy { + minScale = existingInvokeStrategy.ExecutionStrategy.MinScale + maxScale = existingInvokeStrategy.ExecutionStrategy.MaxScale + targetCPU = existingInvokeStrategy.ExecutionStrategy.TargetCPUPercent + specializationTimeout = existingInvokeStrategy.ExecutionStrategy.SpecializationTimeout + } + + if flags.IsSet("targetcpu") { + targetCPU, err = getTargetCPU(flags) + if err != nil { + return nil, err + } + } + + if flags.IsSet("minscale") { + minScale = flags.Int("minscale") + } + + if flags.IsSet("maxscale") { + maxScale = flags.Int("maxscale") + if maxScale <= 0 { + return nil, errors.New("maxscale must be greater than 0") + } + } + + if flags.IsSet("specializationtimeout") { + specializationTimeout = flags.Int("specializationtimeout") + if specializationTimeout < fv1.DefaultSpecializationTimeOut { + return nil, errors.New("specializationtimeout must be greater than or equal to 120 seconds") + } + } + + if minScale > maxScale { + return nil, fmt.Errorf("minscale provided: %v can not be greater than maxscale value %v", minScale, maxScale) + } + + // Right now a simple single case strategy implementation + // This will potentially get more sophisticated once we have more strategies in place + strategy = &fv1.InvokeStrategy{ + StrategyType: fv1.StrategyTypeExecution, + ExecutionStrategy: fv1.ExecutionStrategy{ + ExecutorType: fnExecutor, + MinScale: minScale, + MaxScale: maxScale, + TargetCPUPercent: targetCPU, + SpecializationTimeout: specializationTimeout, + }, + } + } + + return strategy, nil +} + +func getTargetCPU(flags cli.Input) (int, error) { + var targetCPU int + if flags.IsSet("targetcpu") { + targetCPU = flags.Int("targetcpu") + if targetCPU <= 0 || targetCPU > 100 { + return 0, errors.New("TargetCPU must be a value between 1 - 100") + } + } else { + targetCPU = DEFAULT_TARGET_CPU_PERCENTAGE + } + return targetCPU, nil +} diff --git a/pkg/fission-cli/cmd/function/delete.go b/pkg/fission-cli/cmd/function/delete.go new file mode 100644 index 00000000..ed8bf722 --- /dev/null +++ b/pkg/fission-cli/cmd/function/delete.go @@ -0,0 +1,53 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/fission/fission/pkg/controller/client" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" +) + +type DeleteSubCommand struct { + client *client.Client +} + +func Delete(flags cli.Input) error { + opts := DeleteSubCommand{ + client: cmd.GetServer(flags), + } + return opts.do(flags) +} + +func (opts *DeleteSubCommand) do(flags cli.Input) error { + m, err := cmd.GetMetadata("name", "fnNamespace", flags) + if err != nil { + return err + } + + err = opts.client.FunctionDelete(m) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("delete function '%v'", m.Name)) + } + + fmt.Printf("function '%v' deleted\n", m.Name) + return nil +} diff --git a/pkg/fission-cli/function_test.go b/pkg/fission-cli/cmd/function/function_test.go similarity index 82% rename from pkg/fission-cli/function_test.go rename to pkg/fission-cli/cmd/function/function_test.go index df87a9ac..20daf801 100644 --- a/pkg/fission-cli/function_test.go +++ b/pkg/fission-cli/cmd/function/function_test.go @@ -1,26 +1,41 @@ -package fission_cli +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function import ( - "flag" "fmt" "testing" "github.com/stretchr/testify/assert" - "github.com/urfave/cli" fv1 "github.com/fission/fission/pkg/apis/fission.io/v1" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/driver/dummy" ) func TestGetInvokeStrategy(t *testing.T) { cases := []struct { - testArgs map[string]string + testArgs map[string]interface{} existingInvokeStrategy *fv1.InvokeStrategy expectedResult *fv1.InvokeStrategy expectError bool }{ { // case: use default executor poolmgr - testArgs: map[string]string{}, + testArgs: map[string]interface{}{}, existingInvokeStrategy: nil, expectedResult: &fv1.InvokeStrategy{ StrategyType: fv1.StrategyTypeExecution, @@ -32,7 +47,7 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: executor type set to poolmgr - testArgs: map[string]string{"executortype": fv1.ExecutorTypePoolmgr}, + testArgs: map[string]interface{}{"executortype": fv1.ExecutorTypePoolmgr}, existingInvokeStrategy: nil, expectedResult: &fv1.InvokeStrategy{ StrategyType: fv1.StrategyTypeExecution, @@ -44,7 +59,7 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: executor type set to newdeploy - testArgs: map[string]string{"executortype": fv1.ExecutorTypeNewdeploy}, + testArgs: map[string]interface{}{"executortype": fv1.ExecutorTypeNewdeploy}, existingInvokeStrategy: nil, expectedResult: &fv1.InvokeStrategy{ StrategyType: fv1.StrategyTypeExecution, @@ -60,7 +75,7 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: executor type change from poolmgr to newdeploy - testArgs: map[string]string{"executortype": fv1.ExecutorTypeNewdeploy}, + testArgs: map[string]interface{}{"executortype": fv1.ExecutorTypeNewdeploy}, existingInvokeStrategy: &fv1.InvokeStrategy{ StrategyType: fv1.StrategyTypeExecution, ExecutionStrategy: fv1.ExecutionStrategy{ @@ -81,7 +96,7 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: executor type change from newdeploy to poolmgr - testArgs: map[string]string{"executortype": fv1.ExecutorTypePoolmgr}, + testArgs: map[string]interface{}{"executortype": fv1.ExecutorTypePoolmgr}, existingInvokeStrategy: &fv1.InvokeStrategy{ StrategyType: fv1.StrategyTypeExecution, ExecutionStrategy: fv1.ExecutionStrategy{ @@ -102,10 +117,10 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: minscale < maxscale - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "minscale": "2", - "maxscale": "3", + "minscale": 2, + "maxscale": 3, }, existingInvokeStrategy: nil, expectedResult: &fv1.InvokeStrategy{ @@ -122,10 +137,10 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: minscale > maxscale - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "minscale": "5", - "maxscale": "3", + "minscale": 5, + "maxscale": 3, }, existingInvokeStrategy: nil, expectedResult: nil, @@ -133,9 +148,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: maxscale not specified - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "minscale": "5", + "minscale": 5, }, existingInvokeStrategy: nil, expectedResult: nil, @@ -143,9 +158,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: minscale not specified - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "maxscale": "3", + "maxscale": 3, }, existingInvokeStrategy: nil, expectedResult: &fv1.InvokeStrategy{ @@ -162,9 +177,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: maxscale set to 0 - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "maxscale": "0", + "maxscale": 0, }, existingInvokeStrategy: nil, expectedResult: nil, @@ -172,9 +187,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: maxscale set to 9 when existing is 5 - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "maxscale": "9", + "maxscale": 9, }, existingInvokeStrategy: &fv1.InvokeStrategy{ StrategyType: fv1.StrategyTypeExecution, @@ -200,7 +215,7 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: change nothing for existing strategy - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, }, existingInvokeStrategy: &fv1.InvokeStrategy{ @@ -227,9 +242,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: set target cpu percentage - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "targetcpu": "50", + "targetcpu": 50, }, existingInvokeStrategy: nil, expectedResult: &fv1.InvokeStrategy{ @@ -246,9 +261,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: change target cpu percentage - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "targetcpu": "20", + "targetcpu": 20, }, existingInvokeStrategy: &fv1.InvokeStrategy{ StrategyType: fv1.StrategyTypeExecution, @@ -274,9 +289,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: change specializationtimeout - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "specializationtimeout": "200", + "specializationtimeout": 200, }, existingInvokeStrategy: &fv1.InvokeStrategy{ StrategyType: fv1.StrategyTypeExecution, @@ -301,9 +316,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: specializationtimeout should not work for poolmgr - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypePoolmgr, - "specializationtimeout": "10", + "specializationtimeout": 10, }, existingInvokeStrategy: nil, expectedResult: nil, @@ -311,9 +326,9 @@ func TestGetInvokeStrategy(t *testing.T) { }, { // case: specializationtimeout should not be less than 120 - testArgs: map[string]string{ + testArgs: map[string]interface{}{ "executortype": fv1.ExecutorTypeNewdeploy, - "specializationtimeout": "90", + "specializationtimeout": 90, }, existingInvokeStrategy: nil, expectedResult: nil, @@ -324,16 +339,13 @@ func TestGetInvokeStrategy(t *testing.T) { for i, c := range cases { fmt.Printf("=== Test Case %v ===\n", i) - app := NewCliApp() - set := flag.NewFlagSet("test-cmd", 0) - ctx := cli.NewContext(app, set, nil) + flags := dummy.TestFlagSet() for k, v := range c.testArgs { - set.String(k, v, "") - ctx.Set(k, v) + flags.Set(k, v) } - strategy, err := getInvokeStrategy(ctx, c.existingInvokeStrategy) + strategy, err := getInvokeStrategy(flags, c.existingInvokeStrategy) if c.expectError { assert.NotNil(t, err) if err != nil { diff --git a/pkg/fission-cli/cmd/function/get.go b/pkg/fission-cli/cmd/function/get.go new file mode 100644 index 00000000..bdaa53d4 --- /dev/null +++ b/pkg/fission-cli/cmd/function/get.go @@ -0,0 +1,63 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import ( + "os" + + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/fission/fission/pkg/controller/client" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" +) + +type GetSubCommand struct { + client *client.Client +} + +func Get(flags cli.Input) error { + opts := GetSubCommand{ + client: cmd.GetServer(flags), + } + return opts.do(flags) +} + +func (opts *GetSubCommand) do(flags cli.Input) error { + m, err := cmd.GetMetadata("name", "fnNamespace", flags) + if err != nil { + return err + } + + fn, err := opts.client.FunctionGet(m) + if err != nil { + return errors.Wrap(err, "error getting function") + } + + pkg, err := opts.client.PackageGet(&metav1.ObjectMeta{ + Name: fn.Spec.Package.PackageRef.Name, + Namespace: fn.Spec.Package.PackageRef.Namespace, + }) + if err != nil { + return errors.Wrap(err, "error getting package") + } + + os.Stdout.Write(pkg.Spec.Deployment.Literal) + + return nil +} diff --git a/pkg/fission-cli/cmd/function/getmeta.go b/pkg/fission-cli/cmd/function/getmeta.go new file mode 100644 index 00000000..a3084aba --- /dev/null +++ b/pkg/fission-cli/cmd/function/getmeta.go @@ -0,0 +1,59 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/pkg/errors" + + "github.com/fission/fission/pkg/controller/client" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" +) + +type GetMetaSubCommand struct { + client *client.Client +} + +func GetMeta(flags cli.Input) error { + opts := GetMetaSubCommand{ + client: cmd.GetServer(flags), + } + return opts.do(flags) +} + +func (opts *GetMetaSubCommand) do(flags cli.Input) error { + m, err := cmd.GetMetadata("name", "fnNamespace", flags) + if err != nil { + return err + } + + fn, err := opts.client.FunctionGet(m) + if err != nil { + return errors.Wrap(err, "error getting function") + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + fmt.Fprintf(w, "%v\t%v\n", "NAME", "ENV") + fmt.Fprintf(w, "%v\t%v\n", fn.Metadata.Name, fn.Spec.Environment.Name) + w.Flush() + + return nil +} diff --git a/pkg/fission-cli/cmd/function/list.go b/pkg/fission-cli/cmd/function/list.go new file mode 100644 index 00000000..7e8c2fb4 --- /dev/null +++ b/pkg/fission-cli/cmd/function/list.go @@ -0,0 +1,81 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import ( + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/pkg/errors" + + "github.com/fission/fission/pkg/controller/client" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" +) + +type ListSubCommand struct { + client *client.Client +} + +func List(flags cli.Input) error { + opts := ListSubCommand{ + client: cmd.GetServer(flags), + } + return opts.do(flags) +} + +func (opts *ListSubCommand) do(flags cli.Input) error { + ns := flags.String("fnNamespace") + + fns, err := opts.client.FunctionList(ns) + if err != nil { + return errors.Wrap(err, "error listing functions") + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + + fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", "NAME", "ENV", "EXECUTORTYPE", "MINSCALE", "MAXSCALE", "MINCPU", "MAXCPU", "MINMEMORY", "MAXMEMORY", "TARGETCPU", "SECRETS", "CONFIGMAPS") + for _, f := range fns { + secrets := f.Spec.Secrets + configMaps := f.Spec.ConfigMaps + var secretsList, configMapList []string + for _, secret := range secrets { + secretsList = append(secretsList, secret.Name) + } + for _, configMap := range configMaps { + configMapList = append(configMapList, configMap.Name) + } + + fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", + f.Metadata.Name, f.Spec.Environment.Name, + f.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType, + f.Spec.InvokeStrategy.ExecutionStrategy.MinScale, + f.Spec.InvokeStrategy.ExecutionStrategy.MaxScale, + f.Spec.Resources.Requests.Cpu().String(), + f.Spec.Resources.Limits.Cpu().String(), + f.Spec.Resources.Requests.Memory().String(), + f.Spec.Resources.Limits.Memory().String(), + f.Spec.InvokeStrategy.ExecutionStrategy.TargetCPUPercent, + strings.Join(secretsList, ","), + strings.Join(configMapList, ",")) + } + w.Flush() + + return nil +} diff --git a/pkg/fission-cli/cmd/function/log.go b/pkg/fission-cli/cmd/function/log.go new file mode 100644 index 00000000..2e7801b2 --- /dev/null +++ b/pkg/fission-cli/cmd/function/log.go @@ -0,0 +1,126 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + + "github.com/fission/fission/pkg/controller/client" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" + "github.com/fission/fission/pkg/fission-cli/logdb" + "github.com/fission/fission/pkg/fission-cli/util" +) + +type LogSubCommand struct { + client *client.Client +} + +func Log(flags cli.Input) error { + opts := LogSubCommand{ + client: cmd.GetServer(flags), + } + return opts.do(flags) +} + +func (opts *LogSubCommand) do(flags cli.Input) error { + m, err := cmd.GetMetadata("name", "fnNamespace", flags) + if err != nil { + return err + } + + dbType := flags.String("dbtype") + if len(dbType) == 0 { + dbType = logdb.INFLUXDB + } + + fnPod := flags.String("pod") + + logReverseQuery := !flags.Bool("f") && flags.Bool("r") + + recordLimit := flags.Int("recordcount") + if recordLimit <= 0 { + recordLimit = 1000 + } + + f, err := opts.client.FunctionGet(m) + if err != nil { + return errors.Wrap(err, "error getting function") + } + + // request the controller to establish a proxy server to the database. + logDB, err := logdb.GetLogDB(dbType, util.GetServerUrl()) + if err != nil { + return errors.New("failed to connect log database") + } + + requestChan := make(chan struct{}) + responseChan := make(chan struct{}) + ctx := context.Background() + + go func(ctx context.Context, requestChan, responseChan chan struct{}) { + t := time.Unix(0, 0*int64(time.Millisecond)) + for { + select { + case <-requestChan: + logFilter := logdb.LogFilter{ + Pod: fnPod, + Function: f.Metadata.Name, + FuncUid: string(f.Metadata.UID), + Since: t, + Reverse: logReverseQuery, + RecordLimit: recordLimit, + } + logEntries, err := logDB.GetLogs(logFilter) + if err != nil { + fmt.Printf("Error querying logs: %v", err) + responseChan <- struct{}{} + return + } + for _, logEntry := range logEntries { + if flags.Bool("d") { + fmt.Printf("Timestamp: %s\nNamespace: %s\nFunction Name: %s\nFunction ID: %s\nPod: %s\nContainer: %s\nStream: %s\nLog: %s\n---\n", + logEntry.Timestamp, logEntry.Namespace, logEntry.FuncName, logEntry.FuncUid, logEntry.Pod, logEntry.Container, logEntry.Stream, logEntry.Message) + } else { + fmt.Printf("[%s] %s\n", logEntry.Timestamp, logEntry.Message) + } + t = logEntry.Timestamp + } + responseChan <- struct{}{} + case <-ctx.Done(): + return + } + } + }(ctx, requestChan, responseChan) + + for { + requestChan <- struct{}{} + time.Sleep(1 * time.Second) + + <-responseChan + if !flags.Bool("f") { + ctx.Done() + break + } + } + + return nil +} diff --git a/pkg/fission-cli/cmd/function/test.go b/pkg/fission-cli/cmd/function/test.go new file mode 100644 index 00000000..6e41fabd --- /dev/null +++ b/pkg/fission-cli/cmd/function/test.go @@ -0,0 +1,190 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/fission/fission/pkg/controller/client" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" + "github.com/fission/fission/pkg/fission-cli/cmd/httptrigger" + "github.com/fission/fission/pkg/fission-cli/util" +) + +type TestSubCommand struct { + client *client.Client +} + +func Test(flags cli.Input) error { + opts := TestSubCommand{ + client: cmd.GetServer(flags), + } + return opts.do(flags) +} + +func (opts *TestSubCommand) do(flags cli.Input) error { + m, err := cmd.GetMetadata("name", "fnNamespace", flags) + if err != nil { + return err + } + + routerURL := os.Getenv("FISSION_ROUTER") + if len(routerURL) == 0 { + // Portforward to the fission router + localRouterPort := util.SetupPortForward(util.GetFissionNamespace(), + "application=fission-router") + routerURL = "127.0.0.1:" + localRouterPort + } else { + routerURL = strings.TrimPrefix(routerURL, "http://") + } + + fnUri := m.Name + if m.Namespace != metav1.NamespaceDefault { + fnUri = fmt.Sprintf("%v/%v", m.Namespace, m.Name) + } + + functionUrl, err := url.Parse(fmt.Sprintf("http://%s/fission-function/%s", routerURL, fnUri)) + if err != nil { + return err + } + queryParams := flags.StringSlice("query") + if len(queryParams) > 0 { + query := url.Values{} + for _, q := range queryParams { + queryParts := strings.SplitN(q, "=", 2) + var key, value string + if len(queryParts) == 0 { + continue + } + if len(queryParts) > 0 { + key = queryParts[0] + } + if len(queryParts) > 1 { + value = queryParts[1] + } + query.Set(key, value) + } + functionUrl.RawQuery = query.Encode() + } + + ctx := context.Background() + if deadline := flags.Duration("timeout"); deadline > 0 { + var closeCtx func() + ctx, closeCtx = context.WithTimeout(ctx, deadline) + defer closeCtx() + } + + headers := flags.StringSlice("header") + + resp, err := doHTTPRequest(ctx, flags.String("method"), functionUrl.String(), flags.String("body"), headers) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "error reading response from function") + } + + if resp.StatusCode < 400 { + fmt.Print(string(body)) + + return nil + } + + fmt.Printf("Error calling function %s: %d; Please try again or fix the error: %s", m.Name, resp.StatusCode, string(body)) + err = printPodLogs(flags) + if err != nil { + return Log(flags) + } + + return nil +} + +func doHTTPRequest(ctx context.Context, method, url, body string, headers []string) (*http.Response, error) { + method, err := httptrigger.GetMethod(method) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, url, strings.NewReader(body)) + if err != nil { + return nil, errors.Wrap(err, "error creating HTTP request") + } + + for _, header := range headers { + headerKeyValue := strings.SplitN(header, ":", 2) + if len(headerKeyValue) != 2 { + return nil, errors.New("failed to create request without appropriate headers") + } + req.Header.Set(headerKeyValue[0], headerKeyValue[1]) + } + resp, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return nil, errors.Wrap(err, "error executing HTTP request") + } + + return resp, nil +} + +func printPodLogs(flags cli.Input) error { + fnName := flags.String("name") + if len(fnName) == 0 { + return errors.New("need --name argument.") + } + + queryURL, err := url.Parse(util.GetServerUrl()) + if err != nil { + return errors.Wrap(err, "error parsing the base URL") + } + queryURL.Path = fmt.Sprintf("/proxy/logs/%s", fnName) + + req, err := http.NewRequest("POST", queryURL.String(), nil) + if err != nil { + return errors.Wrap(err, "error creating logs request") + } + + httpClient := http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return errors.Wrap(err, "execute get logs request") + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return errors.New("get logs from pod directly") + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "read the response body") + } + + fmt.Println(string(body)) + return nil +} diff --git a/pkg/fission-cli/cmd/function/update.go b/pkg/fission-cli/cmd/function/update.go new file mode 100644 index 00000000..bfe927f4 --- /dev/null +++ b/pkg/fission-cli/cmd/function/update.go @@ -0,0 +1,288 @@ +/* +Copyright 2019 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package function + +import ( + "fmt" + + "github.com/pkg/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + fv1 "github.com/fission/fission/pkg/apis/fission.io/v1" + "github.com/fission/fission/pkg/controller/client" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" + _package "github.com/fission/fission/pkg/fission-cli/cmd/package" + "github.com/fission/fission/pkg/fission-cli/log" + "github.com/fission/fission/pkg/types" +) + +type UpdateSubCommand struct { + client *client.Client + function *fv1.Function +} + +func Update(flags cli.Input) error { + opts := UpdateSubCommand{ + client: cmd.GetServer(flags), + } + return opts.do(flags) +} + +func (opts *UpdateSubCommand) do(flags cli.Input) error { + err := opts.complete(flags) + if err != nil { + return err + } + return opts.run(flags) +} + +func (opts *UpdateSubCommand) complete(flags cli.Input) error { + if len(flags.String("package")) > 0 { + return errors.New("--package is deprecated, please use --deploy instead") + } + + if len(flags.String("srcpkg")) > 0 { + return errors.New("--srcpkg is deprecated, please use --src instead.") + } + + fnName := flags.String("name") + if len(fnName) == 0 { + return errors.New("Need name of function, use --name") + } + fnNamespace := flags.String("fnNamespace") + + m, err := cmd.GetMetadata("name", "fnNamespace", flags) + if err != nil { + return err + } + + function, err := opts.client.FunctionGet(m) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("read function '%v'", fnName)) + } + + envName := flags.String("env") + envNamespace := flags.String("envNamespace") + // if the new env specified is the same as the old one, no need to update package + // same is true for all update parameters, but, for now, we dont check all of them - because, its ok to + // re-write the object with same old values, we just end up getting a new resource version for the object. + if len(envName) > 0 && envName == function.Spec.Environment.Name { + envName = "" + } + + if envNamespace == function.Spec.Environment.Namespace { + envNamespace = "" + } + + var deployArchiveFiles []string + codeFlag := false + code := flags.String("code") + if len(code) == 0 { + deployArchiveFiles = flags.StringSlice("deploy") + } else { + deployArchiveFiles = append(deployArchiveFiles, flags.String("code")) + codeFlag = true + } + + srcArchiveFiles := flags.StringSlice("src") + pkgName := flags.String("pkg") + entrypoint := flags.String("entrypoint") + buildcmd := flags.String("buildcmd") + force := flags.Bool("force") + + secretNames := flags.StringSlice("secret") + cfgMapNames := flags.StringSlice("configmap") + + specializationTimeout := flags.Int("specializationtimeout") + + if len(srcArchiveFiles) > 0 && len(deployArchiveFiles) > 0 { + return errors.New("Need either of --src or --deploy and not both arguments.") + } + + var secrets []fv1.SecretReference + var configMaps []fv1.ConfigMapReference + + if len(secretNames) > 0 { + + // check that the referenced secret is in the same ns as the function, if not give a warning. + for _, secretName := range secretNames { + _, err := opts.client.SecretGet(&metav1.ObjectMeta{ + Namespace: fnNamespace, + Name: secretName, + }) + if k8serrors.IsNotFound(err) { + log.Warn(fmt.Sprintf("secret %s not found in Namespace: %s. Secret needs to be present in the same namespace as function", secretName, fnNamespace)) + } + } + + for _, secretName := range secretNames { + newSecret := fv1.SecretReference{ + Name: secretName, + Namespace: fnNamespace, + } + secrets = append(secrets, newSecret) + } + + function.Spec.Secrets = secrets + } + + if len(cfgMapNames) > 0 { + + // check that the referenced cfgmap is in the same ns as the function, if not give a warning. + for _, cfgMapName := range cfgMapNames { + _, err := opts.client.ConfigMapGet(&metav1.ObjectMeta{ + Namespace: fnNamespace, + Name: cfgMapName, + }) + if k8serrors.IsNotFound(err) { + log.Warn(fmt.Sprintf("ConfigMap %s not found in Namespace: %s. ConfigMap needs to be present in the same namespace as the function", cfgMapName, fnNamespace)) + } + } + + for _, cfgMapName := range cfgMapNames { + newCfgMap := fv1.ConfigMapReference{ + Name: cfgMapName, + Namespace: fnNamespace, + } + configMaps = append(configMaps, newCfgMap) + } + function.Spec.ConfigMaps = configMaps + } + + if len(envName) > 0 { + function.Spec.Environment.Name = envName + } + + if len(envNamespace) > 0 { + function.Spec.Environment.Namespace = envNamespace + } + + if len(entrypoint) > 0 { + function.Spec.Package.FunctionName = entrypoint + } + + if flags.IsSet("fntimeout") { + fnTimeout := flags.Int("fntimeout") + if fnTimeout <= 0 { + return errors.New("fntimeout must be greater than 0") + } + function.Spec.FunctionTimeout = fnTimeout + } + + if len(pkgName) == 0 { + pkgName = function.Spec.Package.PackageRef.Name + } + + strategy, err := getInvokeStrategy(flags, &function.Spec.InvokeStrategy) + if err != nil { + return err + } + function.Spec.InvokeStrategy = *strategy + + if flags.IsSet("specializationtimeout") { + if strategy.ExecutionStrategy.ExecutorType != types.ExecutorTypeNewdeploy { + return errors.New("specializationtimeout flag is only applicable for newdeploy type of executor") + } + + if specializationTimeout < fv1.DefaultSpecializationTimeOut { + return errors.New("specializationtimeout must be greater than or equal to 120 seconds") + } else { + function.Spec.InvokeStrategy.ExecutionStrategy.SpecializationTimeout = specializationTimeout + } + } + + resReqs, err := cmd.GetResourceReqs(flags, &function.Spec.Resources) + if err != nil { + return err + } + + function.Spec.Resources = *resReqs + + pkg, err := opts.client.PackageGet(&metav1.ObjectMeta{ + Namespace: fnNamespace, + Name: pkgName, + }) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("read package '%v.%v'. Pkg should be present in the same ns as the function", pkgName, fnNamespace)) + } + + pkgMetadata := &pkg.Metadata + + if len(deployArchiveFiles) != 0 || len(srcArchiveFiles) != 0 || len(buildcmd) != 0 || len(envName) != 0 || len(envNamespace) != 0 { + fnList, err := _package.GetFunctionsByPackage(opts.client, pkg.Metadata.Name, pkg.Metadata.Namespace) + if err != nil { + return errors.Wrap(err, "error getting function list") + } + + if !force && len(fnList) > 1 { + return errors.New("package is used by multiple functions, use --force to force update") + } + + keepURL := flags.Bool("keepurl") + + pkgMetadata, err = _package.UpdatePackage(opts.client, pkg, envName, envNamespace, srcArchiveFiles, deployArchiveFiles, buildcmd, false, codeFlag, keepURL) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("error updating package '%v'", pkgName)) + } + + fmt.Printf("package '%v' updated\n", pkgMetadata.GetName()) + + // update resource version of package reference of functions that shared the same package + for _, fn := range fnList { + // ignore the update for current function here, it will be updated later. + if fn.Metadata.Name != fnName { + fn.Spec.Package.PackageRef.ResourceVersion = pkgMetadata.ResourceVersion + _, err := opts.client.FunctionUpdate(&fn) + if err != nil { + return errors.Wrap(err, "error updating function") + } + } + } + } + + // TODO : One corner case where user just updates the pkg reference with fnUpdate, but internally this new pkg reference + // references a diff env than the spec + + // update function spec with new package metadata + function.Spec.Package.PackageRef = fv1.PackageRef{ + Namespace: pkgMetadata.Namespace, + Name: pkgMetadata.Name, + ResourceVersion: pkgMetadata.ResourceVersion, + } + + if function.Spec.Environment.Name != pkg.Spec.Environment.Name { + log.Warn("Function's environment is different than package's environment, package's environment will be used for updating function") + function.Spec.Environment.Name = pkg.Spec.Environment.Name + function.Spec.Environment.Namespace = pkg.Spec.Environment.Namespace + } + + opts.function = function + + return nil +} + +func (opts *UpdateSubCommand) run(flags cli.Input) error { + _, err := opts.client.FunctionUpdate(opts.function) + if err != nil { + return errors.Wrap(err, "error updating function") + } + + fmt.Printf("function '%v' updated\n", opts.function.Metadata.Name) + return nil +} diff --git a/pkg/fission-cli/function.go b/pkg/fission-cli/function.go deleted file mode 100644 index adb90c06..00000000 --- a/pkg/fission-cli/function.go +++ /dev/null @@ -1,944 +0,0 @@ -/* -Copyright 2016 The Fission Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package fission_cli - -import ( - "context" - "errors" - "fmt" - "github.com/fission/fission/pkg/fission-cli/cmd/httptrigger" - "io/ioutil" - "net/http" - "net/url" - "os" - "strings" - "text/tabwriter" - "time" - - uuid "github.com/satori/go.uuid" - "github.com/urfave/cli" - apiv1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - fv1 "github.com/fission/fission/pkg/apis/fission.io/v1" - ferror "github.com/fission/fission/pkg/error" - "github.com/fission/fission/pkg/fission-cli/cliwrapper/driver/urfavecli" - "github.com/fission/fission/pkg/fission-cli/cmd" - cmdutils "github.com/fission/fission/pkg/fission-cli/cmd" - _package "github.com/fission/fission/pkg/fission-cli/cmd/package" - "github.com/fission/fission/pkg/fission-cli/cmd/spec" - "github.com/fission/fission/pkg/fission-cli/log" - "github.com/fission/fission/pkg/fission-cli/logdb" - "github.com/fission/fission/pkg/fission-cli/util" - "github.com/fission/fission/pkg/types" -) - -const ( - DEFAULT_MIN_SCALE = 1 - DEFAULT_TARGET_CPU_PERCENTAGE = 80 -) - -func printPodLogs(c *cli.Context) error { - fnName := c.String("name") - if len(fnName) == 0 { - log.Fatal("Need --name argument.") - } - - queryURL, err := url.Parse(util.GetServerUrl()) - util.CheckErr(err, "parse the base URL") - queryURL.Path = fmt.Sprintf("/proxy/logs/%s", fnName) - - req, err := http.NewRequest("POST", queryURL.String(), nil) - util.CheckErr(err, "create logs request") - - httpClient := http.Client{} - resp, err := httpClient.Do(req) - util.CheckErr(err, "execute get logs request") - - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return errors.New("get logs from pod directly") - } - - body, err := ioutil.ReadAll(resp.Body) - util.CheckErr(err, "read the response body") - fmt.Println(string(body)) - return nil -} - -func getInvokeStrategy(c *cli.Context, existingInvokeStrategy *fv1.InvokeStrategy) (strategy *fv1.InvokeStrategy, err error) { - - var fnExecutor, newFnExecutor fv1.ExecutorType - - switch c.String("executortype") { - case "": - fallthrough - case types.ExecutorTypePoolmgr: - newFnExecutor = types.ExecutorTypePoolmgr - case types.ExecutorTypeNewdeploy: - newFnExecutor = types.ExecutorTypeNewdeploy - default: - return nil, errors.New("executor type must be one of 'poolmgr' or 'newdeploy', defaults to 'poolmgr'") - } - - if existingInvokeStrategy != nil { - fnExecutor = existingInvokeStrategy.ExecutionStrategy.ExecutorType - - // override the executor type if user specified a new executor type - if c.IsSet("executortype") { - fnExecutor = newFnExecutor - } - } else { - fnExecutor = newFnExecutor - } - - if c.IsSet("specializationtimeout") && fnExecutor != types.ExecutorTypeNewdeploy { - return nil, errors.New("specializationtimeout flag is only applicable for newdeploy type of executor") - } - - if fnExecutor == types.ExecutorTypePoolmgr { - if c.IsSet("targetcpu") || c.IsSet("minscale") || c.IsSet("maxscale") { - log.Fatal("To set target CPU or min/max scale for function, please specify \"--executortype newdeploy\"") - } - - if c.IsSet("mincpu") || c.IsSet("maxcpu") || c.IsSet("minmemory") || c.IsSet("maxmemory") { - log.Warn("To limit CPU/Memory for function with executor type \"poolmgr\", please specify resources limits when creating environment") - } - strategy = &fv1.InvokeStrategy{ - StrategyType: fv1.StrategyTypeExecution, - ExecutionStrategy: fv1.ExecutionStrategy{ - ExecutorType: types.ExecutorTypePoolmgr, - }, - } - } else { - // set default value - targetCPU := DEFAULT_TARGET_CPU_PERCENTAGE - minScale := DEFAULT_MIN_SCALE - maxScale := minScale - specializationTimeout := fv1.DefaultSpecializationTimeOut - - if existingInvokeStrategy != nil && existingInvokeStrategy.ExecutionStrategy.ExecutorType == types.ExecutorTypeNewdeploy { - minScale = existingInvokeStrategy.ExecutionStrategy.MinScale - maxScale = existingInvokeStrategy.ExecutionStrategy.MaxScale - targetCPU = existingInvokeStrategy.ExecutionStrategy.TargetCPUPercent - specializationTimeout = existingInvokeStrategy.ExecutionStrategy.SpecializationTimeout - } - - if c.IsSet("targetcpu") { - targetCPU = getTargetCPU(c) - } - - if c.IsSet("minscale") { - minScale = c.Int("minscale") - } - - if c.IsSet("maxscale") { - maxScale = c.Int("maxscale") - if maxScale <= 0 { - return nil, errors.New("maxscale must be greater than 0") - } - } - - if c.IsSet("specializationtimeout") { - specializationTimeout = c.Int("specializationtimeout") - if specializationTimeout < fv1.DefaultSpecializationTimeOut { - return nil, errors.New("specializationtimeout must be greater than or equal to 120 seconds") - } - } - - if minScale > maxScale { - return nil, fmt.Errorf("minscale provided: %v can not be greater than maxscale value %v", minScale, maxScale) - } - - // Right now a simple single case strategy implementation - // This will potentially get more sophisticated once we have more strategies in place - strategy = &fv1.InvokeStrategy{ - StrategyType: fv1.StrategyTypeExecution, - ExecutionStrategy: fv1.ExecutionStrategy{ - ExecutorType: fnExecutor, - MinScale: minScale, - MaxScale: maxScale, - TargetCPUPercent: targetCPU, - SpecializationTimeout: specializationTimeout, - }, - } - } - - return strategy, nil -} - -func getTargetCPU(c *cli.Context) int { - var targetCPU int - if c.IsSet("targetcpu") { - targetCPU = c.Int("targetcpu") - if targetCPU <= 0 || targetCPU > 100 { - log.Fatal("TargetCPU must be a value between 1 - 100") - } - } else { - targetCPU = DEFAULT_TARGET_CPU_PERCENTAGE - } - return targetCPU -} - -// From this change onwards, we mandate that a function should reference a secret, config map and package in its own ns -func fnCreate(c *cli.Context) error { - client := util.GetApiClient(c.GlobalString("server")) - - fnNamespace := c.String("fnNamespace") - envNamespace := c.String("envNamespace") - - fnName := c.String("name") - if len(fnName) == 0 { - log.Fatal("Need --name argument.") - } - - // user wants a spec, create a yaml file with package and function - toSpec := false - specFile := "" - if c.Bool("spec") { - toSpec = true - specFile = fmt.Sprintf("function-%v.yaml", fnName) - } - specDir := cmdutils.GetSpecDir(urfavecli.Parse(c)) - - // check for unique function names within a namespace - fnList, err := client.FunctionList(fnNamespace) - util.CheckErr(err, "get function list") - // check function existence before creating package - for _, fn := range fnList { - if fn.Metadata.Name == fnName { - log.Fatal("A function with the same name already exists.") - } - } - entrypoint := c.String("entrypoint") - - fnTimeout := c.Int("fntimeout") - if fnTimeout <= 0 { - log.Fatal("fntimeout must be greater than 0") - } - - pkgName := c.String("pkg") - - secretNames := c.StringSlice("secret") - cfgMapNames := c.StringSlice("configmap") - - invokeStrategy, err := getInvokeStrategy(c, nil) - if err != nil { - log.Fatal(err) - } - resourceReq, err := cmd.GetResourceReqs(urfavecli.Parse(c), &apiv1.ResourceRequirements{}) - if err != nil { - log.Fatal(err) - } - - var pkgMetadata *metav1.ObjectMeta - var envName string - if len(pkgName) > 0 { - // use existing package - pkg, err := client.PackageGet(&metav1.ObjectMeta{ - Namespace: fnNamespace, - Name: pkgName, - }) - util.CheckErr(err, fmt.Sprintf("read package in '%v' in Namespace: %s. Package needs to be present in the same namespace as function", pkgName, fnNamespace)) - pkgMetadata = &pkg.Metadata - envName = pkg.Spec.Environment.Name - if envName != c.String("env") { - log.Warn("Function's environment is different than package's environment, package's environment will be used for creating function") - } - envNamespace = pkg.Spec.Environment.Namespace - } else { - // need to specify environment for creating new package - envName = c.String("env") - if len(envName) == 0 { - log.Fatal("Need --env argument.") - } - - // examine existence of given environment. If specs - then spec validate will do it, don't check here. - if !toSpec { - _, err := client.EnvironmentGet(&metav1.ObjectMeta{ - Namespace: envNamespace, - Name: envName, - }) - if err != nil { - if e, ok := err.(ferror.Error); ok && e.Code == ferror.ErrorNotFound { - log.Warn(fmt.Sprintf("Environment \"%v\" does not exist. Please create the environment before executing the function. \nFor example: `fission env create --name %v --envns %v --image `\n", envName, envName, envNamespace)) - } else { - util.CheckErr(err, "retrieve environment information") - } - } - } - - srcArchiveFiles := c.StringSlice("src") - var deployArchiveFiles []string - noZip := false - code := c.String("code") - if len(code) == 0 { - deployArchiveFiles = c.StringSlice("deploy") - } else { - deployArchiveFiles = append(deployArchiveFiles, c.String("code")) - noZip = true - } - // fatal when both src & deploy archive are empty - if len(srcArchiveFiles) == 0 && len(deployArchiveFiles) == 0 { - log.Fatal("Need --deploy or --src argument.") - } - - buildcmd := c.String("buildcmd") - keepURL := c.Bool("keepurl") - - // create new package in the same namespace as the function. - pkgMetadata, err = _package.CreatePackage(c, client, fnNamespace, envName, envNamespace, - srcArchiveFiles, deployArchiveFiles, buildcmd, specDir, specFile, noZip, keepURL) - util.CheckErr(err, "create package") - } - - var secrets []fv1.SecretReference - var cfgmaps []fv1.ConfigMapReference - - if len(secretNames) > 0 { - // check the referenced secret is in the same ns as the function, if not give a warning. - for _, secretName := range secretNames { - _, err := client.SecretGet(&metav1.ObjectMeta{ - Namespace: fnNamespace, - Name: secretName, - }) - if k8serrors.IsNotFound(err) { - log.Warn(fmt.Sprintf("Secret %s not found in Namespace: %s. Secret needs to be present in the same namespace as function", secretName, fnNamespace)) - } - } - for _, secretName := range secretNames { - newSecret := fv1.SecretReference{ - Name: secretName, - Namespace: fnNamespace, - } - secrets = append(secrets, newSecret) - } - } - - if len(cfgMapNames) > 0 { - // check the referenced cfgmap is in the same ns as the function, if not give a warning. - for _, cfgMapName := range cfgMapNames { - _, err := client.ConfigMapGet(&metav1.ObjectMeta{ - Namespace: fnNamespace, - Name: cfgMapName, - }) - if k8serrors.IsNotFound(err) { - log.Warn(fmt.Sprintf("ConfigMap %s not found in Namespace: %s. ConfigMap needs to be present in the same namespace as function", cfgMapName, fnNamespace)) - } - } - for _, cfgMapName := range cfgMapNames { - newCfgMap := fv1.ConfigMapReference{ - Name: cfgMapName, - Namespace: fnNamespace, - } - cfgmaps = append(cfgmaps, newCfgMap) - } - } - - function := &fv1.Function{ - Metadata: metav1.ObjectMeta{ - Name: fnName, - Namespace: fnNamespace, - }, - Spec: fv1.FunctionSpec{ - Environment: fv1.EnvironmentReference{ - Name: envName, - Namespace: envNamespace, - }, - Package: fv1.FunctionPackageRef{ - FunctionName: entrypoint, - PackageRef: fv1.PackageRef{ - Namespace: pkgMetadata.Namespace, - Name: pkgMetadata.Name, - ResourceVersion: pkgMetadata.ResourceVersion, - }, - }, - Secrets: secrets, - ConfigMaps: cfgmaps, - Resources: *resourceReq, - InvokeStrategy: *invokeStrategy, - FunctionTimeout: fnTimeout, - }, - } - - // if we're writing a spec, don't create the function - if toSpec { - err = spec.SpecSave(*function, specFile) - util.CheckErr(err, "create function spec") - return nil - - } - - _, err = client.FunctionCreate(function) - util.CheckErr(err, "create function") - - fmt.Printf("function '%v' created\n", fnName) - - // Allow the user to specify an HTTP trigger while creating a function. - triggerUrl := c.String("url") - if len(triggerUrl) == 0 { - return nil - } - if !strings.HasPrefix(triggerUrl, "/") { - triggerUrl = fmt.Sprintf("/%s", triggerUrl) - } - - method, err := httptrigger.GetMethod(c.String("method")) - if err != nil { - util.CheckErr(err, "get HTTP trigger method") - } - - triggerName := uuid.NewV4().String() - ht := &fv1.HTTPTrigger{ - Metadata: metav1.ObjectMeta{ - Name: triggerName, - Namespace: fnNamespace, - }, - Spec: fv1.HTTPTriggerSpec{ - RelativeURL: triggerUrl, - Method: method, - FunctionReference: fv1.FunctionReference{ - Type: fv1.FunctionReferenceTypeFunctionName, - Name: fnName, - }, - }, - } - _, err = client.HTTPTriggerCreate(ht) - util.CheckErr(err, "create HTTP trigger") - fmt.Printf("route created: %v %v -> %v\n", method, triggerUrl, fnName) - - return err -} - -func fnGet(c *cli.Context) error { - client := util.GetApiClient(c.GlobalString("server")) - - fnName := c.String("name") - if len(fnName) == 0 { - log.Fatal("Need name of function, use --name") - } - fnNamespace := c.String("fnNamespace") - m := &metav1.ObjectMeta{ - Name: fnName, - Namespace: fnNamespace, - } - fn, err := client.FunctionGet(m) - util.CheckErr(err, "get function") - - pkg, err := client.PackageGet(&metav1.ObjectMeta{ - Name: fn.Spec.Package.PackageRef.Name, - Namespace: fn.Spec.Package.PackageRef.Namespace, - }) - util.CheckErr(err, "get package") - - os.Stdout.Write(pkg.Spec.Deployment.Literal) - return err -} - -func fnGetMeta(c *cli.Context) error { - client := util.GetApiClient(c.GlobalString("server")) - - fnName := c.String("name") - if len(fnName) == 0 { - log.Fatal("Need name of function, use --name") - } - fnNamespace := c.String("fnNamespace") - - m := &metav1.ObjectMeta{ - Name: fnName, - Namespace: fnNamespace, - } - - f, err := client.FunctionGet(m) - util.CheckErr(err, "get function") - - w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) - fmt.Fprintf(w, "%v\t%v\n", "NAME", "ENV") - fmt.Fprintf(w, "%v\t%v\n", f.Metadata.Name, f.Spec.Environment.Name) - w.Flush() - return err -} - -func fnUpdate(c *cli.Context) error { - client := util.GetApiClient(c.GlobalString("server")) - - if len(c.String("package")) > 0 { - log.Fatal("--package is deprecated, please use --deploy instead.") - } - - if len(c.String("srcpkg")) > 0 { - log.Fatal("--srcpkg is deprecated, please use --src instead.") - } - - fnName := c.String("name") - if len(fnName) == 0 { - log.Fatal("Need name of function, use --name") - } - fnNamespace := c.String("fnNamespace") - - function, err := client.FunctionGet(&metav1.ObjectMeta{ - Name: fnName, - Namespace: fnNamespace, - }) - util.CheckErr(err, fmt.Sprintf("read function '%v'", fnName)) - - envName := c.String("env") - envNamespace := c.String("envNamespace") - // if the new env specified is the same as the old one, no need to update package - // same is true for all update parameters, but, for now, we dont check all of them - because, its ok to - // re-write the object with same old values, we just end up getting a new resource version for the object. - if len(envName) > 0 && envName == function.Spec.Environment.Name { - envName = "" - } - - if envNamespace == function.Spec.Environment.Namespace { - envNamespace = "" - } - - var deployArchiveFiles []string - codeFlag := false - code := c.String("code") - if len(code) == 0 { - deployArchiveFiles = c.StringSlice("deploy") - } else { - deployArchiveFiles = append(deployArchiveFiles, c.String("code")) - codeFlag = true - } - - srcArchiveFiles := c.StringSlice("src") - pkgName := c.String("pkg") - entrypoint := c.String("entrypoint") - buildcmd := c.String("buildcmd") - force := c.Bool("force") - - secretNames := c.StringSlice("secret") - cfgMapNames := c.StringSlice("configmap") - - specializationTimeout := c.Int("specializationtimeout") - - if len(srcArchiveFiles) > 0 && len(deployArchiveFiles) > 0 { - log.Fatal("Need either of --src or --deploy and not both arguments.") - } - - var secrets []fv1.SecretReference - var configMaps []fv1.ConfigMapReference - - if len(secretNames) > 0 { - - // check that the referenced secret is in the same ns as the function, if not give a warning. - for _, secretName := range secretNames { - _, err := client.SecretGet(&metav1.ObjectMeta{ - Namespace: fnNamespace, - Name: secretName, - }) - if k8serrors.IsNotFound(err) { - log.Warn(fmt.Sprintf("secret %s not found in Namespace: %s. Secret needs to be present in the same namespace as function", secretName, fnNamespace)) - } - } - - for _, secretName := range secretNames { - newSecret := fv1.SecretReference{ - Name: secretName, - Namespace: fnNamespace, - } - secrets = append(secrets, newSecret) - } - - function.Spec.Secrets = secrets - } - - if len(cfgMapNames) > 0 { - - // check that the referenced cfgmap is in the same ns as the function, if not give a warning. - for _, cfgMapName := range cfgMapNames { - _, err := client.ConfigMapGet(&metav1.ObjectMeta{ - Namespace: fnNamespace, - Name: cfgMapName, - }) - if k8serrors.IsNotFound(err) { - log.Warn(fmt.Sprintf("ConfigMap %s not found in Namespace: %s. ConfigMap needs to be present in the same namespace as the function", cfgMapName, fnNamespace)) - } - } - - for _, cfgMapName := range cfgMapNames { - newCfgMap := fv1.ConfigMapReference{ - Name: cfgMapName, - Namespace: fnNamespace, - } - configMaps = append(configMaps, newCfgMap) - } - function.Spec.ConfigMaps = configMaps - } - - if len(envName) > 0 { - function.Spec.Environment.Name = envName - } - - if len(envNamespace) > 0 { - function.Spec.Environment.Namespace = envNamespace - } - - if len(entrypoint) > 0 { - function.Spec.Package.FunctionName = entrypoint - } - - if c.IsSet("fntimeout") { - fnTimeout := c.Int("fntimeout") - if fnTimeout <= 0 { - log.Fatal("fntimeout must be greater than 0") - } - function.Spec.FunctionTimeout = fnTimeout - } - - if len(pkgName) == 0 { - pkgName = function.Spec.Package.PackageRef.Name - } - - strategy, err := getInvokeStrategy(c, &function.Spec.InvokeStrategy) - if err != nil { - log.Fatal(err) - } - function.Spec.InvokeStrategy = *strategy - - if c.IsSet("specializationtimeout") { - if strategy.ExecutionStrategy.ExecutorType != types.ExecutorTypeNewdeploy { - log.Fatal("specializationtimeout flag is only applicable for newdeploy type of executor") - } - - if specializationTimeout < fv1.DefaultSpecializationTimeOut { - log.Fatal("specializationtimeout must be greater than or equal to 120 seconds") - } else { - function.Spec.InvokeStrategy.ExecutionStrategy.SpecializationTimeout = specializationTimeout - } - } - - resReqs, err := cmd.GetResourceReqs(urfavecli.Parse(c), &function.Spec.Resources) - if err != nil { - log.Fatal(err) - } - - function.Spec.Resources = *resReqs - - pkg, err := client.PackageGet(&metav1.ObjectMeta{ - Namespace: fnNamespace, - Name: pkgName, - }) - util.CheckErr(err, fmt.Sprintf("read package '%v.%v'. Pkg should be present in the same ns as the function", pkgName, fnNamespace)) - - pkgMetadata := &pkg.Metadata - - if len(deployArchiveFiles) != 0 || len(srcArchiveFiles) != 0 || len(buildcmd) != 0 || len(envName) != 0 || len(envNamespace) != 0 { - fnList, err := _package.GetFunctionsByPackage(client, pkg.Metadata.Name, pkg.Metadata.Namespace) - util.CheckErr(err, "get function list") - - if !force && len(fnList) > 1 { - log.Fatal("Package is used by multiple functions, use --force to force update") - } - - keepURL := c.Bool("keepurl") - - pkgMetadata, err = _package.UpdatePackage(client, pkg, envName, envNamespace, srcArchiveFiles, deployArchiveFiles, buildcmd, false, codeFlag, keepURL) - util.CheckErr(err, fmt.Sprintf("update package '%v'", pkgName)) - - fmt.Printf("package '%v' updated\n", pkgMetadata.GetName()) - - // update resource version of package reference of functions that shared the same package - for _, fn := range fnList { - // ignore the update for current function here, it will be updated later. - if fn.Metadata.Name != fnName { - fn.Spec.Package.PackageRef.ResourceVersion = pkgMetadata.ResourceVersion - _, err := client.FunctionUpdate(&fn) - util.CheckErr(err, "update function") - } - } - } - - // TODO : One corner case where user just updates the pkg reference with fnUpdate, but internally this new pkg reference - // references a diff env than the spec - - // update function spec with new package metadata - function.Spec.Package.PackageRef = fv1.PackageRef{ - Namespace: pkgMetadata.Namespace, - Name: pkgMetadata.Name, - ResourceVersion: pkgMetadata.ResourceVersion, - } - - if function.Spec.Environment.Name != pkg.Spec.Environment.Name { - log.Warn("Function's environment is different than package's environment, package's environment will be used for updating function") - function.Spec.Environment.Name = pkg.Spec.Environment.Name - function.Spec.Environment.Namespace = pkg.Spec.Environment.Namespace - } - - _, err = client.FunctionUpdate(function) - util.CheckErr(err, "update function") - - fmt.Printf("function '%v' updated\n", fnName) - return err -} - -func fnDelete(c *cli.Context) error { - client := util.GetApiClient(c.GlobalString("server")) - - fnName := c.String("name") - if len(fnName) == 0 { - log.Fatal("Need name of function, use --name") - } - fnNamespace := c.String("fnNamespace") - - m := &metav1.ObjectMeta{ - Name: fnName, - Namespace: fnNamespace, - } - - err := client.FunctionDelete(m) - util.CheckErr(err, fmt.Sprintf("delete function '%v'", fnName)) - - fmt.Printf("function '%v' deleted\n", fnName) - return err -} - -func fnList(c *cli.Context) error { - client := util.GetApiClient(c.GlobalString("server")) - ns := c.String("fnNamespace") - - fns, err := client.FunctionList(ns) - util.CheckErr(err, "list functions") - - w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) - - fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", "NAME", "ENV", "EXECUTORTYPE", "MINSCALE", "MAXSCALE", "MINCPU", "MAXCPU", "MINMEMORY", "MAXMEMORY", "TARGETCPU", "SECRETS", "CONFIGMAPS") - for _, f := range fns { - secrets := f.Spec.Secrets - configMaps := f.Spec.ConfigMaps - var secretsList, configMapList []string - for _, secret := range secrets { - secretsList = append(secretsList, secret.Name) - } - for _, configMap := range configMaps { - configMapList = append(configMapList, configMap.Name) - } - mincpu := f.Spec.Resources.Requests.Cpu - mincpu().Value() - - fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", - f.Metadata.Name, f.Spec.Environment.Name, - f.Spec.InvokeStrategy.ExecutionStrategy.ExecutorType, - f.Spec.InvokeStrategy.ExecutionStrategy.MinScale, - f.Spec.InvokeStrategy.ExecutionStrategy.MaxScale, - f.Spec.Resources.Requests.Cpu().String(), - f.Spec.Resources.Limits.Cpu().String(), - f.Spec.Resources.Requests.Memory().String(), - f.Spec.Resources.Limits.Memory().String(), - f.Spec.InvokeStrategy.ExecutionStrategy.TargetCPUPercent, - strings.Join(secretsList, ","), - strings.Join(configMapList, ",")) - } - w.Flush() - - return err -} - -func fnLogs(c *cli.Context) error { - - client := util.GetApiClient(c.GlobalString("server")) - - fnName := c.String("name") - if len(fnName) == 0 { - log.Fatal("Need name of function, use --name") - } - fnNamespace := c.String("fnNamespace") - - dbType := c.String("dbtype") - if len(dbType) == 0 { - dbType = logdb.INFLUXDB - } - - fnPod := c.String("pod") - m := &metav1.ObjectMeta{ - Name: fnName, - Namespace: fnNamespace, - } - - logReverseQuery := !c.Bool("f") && c.Bool("r") - - recordLimit := c.Int("recordcount") - if recordLimit <= 0 { - recordLimit = 1000 - } - - f, err := client.FunctionGet(m) - util.CheckErr(err, "get function") - - // request the controller to establish a proxy server to the database. - logDB, err := logdb.GetLogDB(dbType, util.GetServerUrl()) - if err != nil { - log.Fatal("failed to connect log database") - } - - requestChan := make(chan struct{}) - responseChan := make(chan struct{}) - ctx := context.Background() - - go func(ctx context.Context, requestChan, responseChan chan struct{}) { - t := time.Unix(0, 0*int64(time.Millisecond)) - for { - select { - case <-requestChan: - logFilter := logdb.LogFilter{ - Pod: fnPod, - Function: f.Metadata.Name, - FuncUid: string(f.Metadata.UID), - Since: t, - Reverse: logReverseQuery, - RecordLimit: recordLimit, - } - logEntries, err := logDB.GetLogs(logFilter) - if err != nil { - log.Fatal(fmt.Sprintf("Error querying logs: %v", err)) - } - for _, logEntry := range logEntries { - if c.Bool("d") { - fmt.Printf("Timestamp: %s\nNamespace: %s\nFunction Name: %s\nFunction ID: %s\nPod: %s\nContainer: %s\nStream: %s\nLog: %s\n---\n", - logEntry.Timestamp, logEntry.Namespace, logEntry.FuncName, logEntry.FuncUid, logEntry.Pod, logEntry.Container, logEntry.Stream, logEntry.Message) - } else { - fmt.Printf("[%s] %s\n", logEntry.Timestamp, logEntry.Message) - } - t = logEntry.Timestamp - } - responseChan <- struct{}{} - case <-ctx.Done(): - return - } - } - }(ctx, requestChan, responseChan) - - for { - requestChan <- struct{}{} - <-responseChan - if !c.Bool("f") { - ctx.Done() - return nil - } - time.Sleep(1 * time.Second) - } -} - -func fnTest(c *cli.Context) error { - fnName := c.String("name") - if len(fnName) == 0 { - log.Fatal("Need function name to be specified with --name") - } - ns := c.String("fnNamespace") - - routerURL := os.Getenv("FISSION_ROUTER") - if len(routerURL) == 0 { - // Portforward to the fission router - localRouterPort := util.SetupPortForward(util.GetFissionNamespace(), - "application=fission-router") - routerURL = "127.0.0.1:" + localRouterPort - } else { - routerURL = strings.TrimPrefix(routerURL, "http://") - } - - fnUri := fnName - if ns != metav1.NamespaceDefault { - fnUri = fmt.Sprintf("%v/%v", ns, fnName) - } - - functionUrl, err := url.Parse(fmt.Sprintf("http://%s/fission-function/%s", routerURL, fnUri)) - if err != nil { - log.Fatal(err) - } - queryParams := c.StringSlice("query") - if len(queryParams) > 0 { - query := url.Values{} - for _, q := range queryParams { - queryParts := strings.SplitN(q, "=", 2) - var key, value string - if len(queryParts) == 0 { - continue - } - if len(queryParts) > 0 { - key = queryParts[0] - } - if len(queryParts) > 1 { - value = queryParts[1] - } - query.Set(key, value) - } - functionUrl.RawQuery = query.Encode() - } - - ctx := context.Background() - if deadline := c.Duration("timeout"); deadline > 0 { - var closeCtx func() - ctx, closeCtx = context.WithTimeout(ctx, deadline) - defer closeCtx() - } - - headers := c.StringSlice("header") - - resp := doHTTPRequest(ctx, c.String("method"), functionUrl.String(), c.String("body"), headers) - if resp.StatusCode < 400 { - body, err := ioutil.ReadAll(resp.Body) - util.CheckErr(err, "Function test") - fmt.Print(string(body)) - defer resp.Body.Close() - return nil - } - - body, err := ioutil.ReadAll(resp.Body) - util.CheckErr(err, "read log response from pod") - fmt.Printf("Error calling function %s: %d; Please try again or fix the error: %s", fnName, resp.StatusCode, string(body)) - defer resp.Body.Close() - err = printPodLogs(c) - if err != nil { - fnLogs(c) - } - - return nil -} - -func doHTTPRequest(ctx context.Context, method, url, body string, headers []string) *http.Response { - if method == "" { - method = http.MethodGet - } - - if method != http.MethodGet && - method != http.MethodDelete && - method != http.MethodPost && - method != http.MethodPut && - method != http.MethodOptions { - log.Fatal(fmt.Sprintf("Invalid HTTP method '%s'.", method)) - } - - req, err := http.NewRequest(method, url, strings.NewReader(body)) - util.CheckErr(err, "create HTTP request") - - for _, header := range headers { - headerKeyValue := strings.SplitN(header, ":", 2) - if len(headerKeyValue) != 2 { - log.Fatal("Failed to create request without appropriate headers") - } - req.Header.Set(headerKeyValue[0], headerKeyValue[1]) - } - resp, err := http.DefaultClient.Do(req.WithContext(ctx)) - util.CheckErr(err, "execute HTTP request") - - return resp -}