Move packages to proejct/pkg to follow go project folder structure convention (#1190)
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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/util"
|
||||
"github.com/fission/fission/pkg/types"
|
||||
)
|
||||
|
||||
type (
|
||||
// packageBuildWatcher is used to watch a set of in-progress builds.
|
||||
packageBuildWatcher struct {
|
||||
// fission client
|
||||
fclient *client.Client
|
||||
|
||||
// set of packages already printed, ensures we don't duplicate the notifications
|
||||
finished map[string]bool
|
||||
|
||||
// set of metadata in the app spec. packages outside this set should be ignored.
|
||||
pkgMeta map[string]metav1.ObjectMeta
|
||||
}
|
||||
)
|
||||
|
||||
func makePackageBuildWatcher(fclient *client.Client) *packageBuildWatcher {
|
||||
return &packageBuildWatcher{
|
||||
fclient: fclient,
|
||||
finished: make(map[string]bool),
|
||||
pkgMeta: make(map[string]metav1.ObjectMeta),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *packageBuildWatcher) addPackages(pkgMeta map[string]metav1.ObjectMeta) {
|
||||
for k, v := range pkgMeta {
|
||||
w.pkgMeta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (w *packageBuildWatcher) watch(ctx context.Context) {
|
||||
for {
|
||||
// non-blocking check if we're cancelled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// poll list of packages (TODO: convert to watch)
|
||||
pkgs, err := w.fclient.PackageList(metav1.NamespaceAll)
|
||||
util.CheckErr(err, "Getting list of packages")
|
||||
|
||||
// find packages that (a) are in the app spec and (b) have an interesting
|
||||
// build status (either succeeded or failed; not "none")
|
||||
keepWaiting := false
|
||||
buildpkgs := make([]fv1.Package, 0)
|
||||
for _, pkg := range pkgs {
|
||||
_, ok := w.pkgMeta[mapKey(&pkg.Metadata)]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if pkg.Status.BuildStatus == types.BuildStatusNone {
|
||||
continue
|
||||
}
|
||||
if pkg.Status.BuildStatus == types.BuildStatusPending ||
|
||||
pkg.Status.BuildStatus == types.BuildStatusRunning {
|
||||
keepWaiting = true
|
||||
}
|
||||
buildpkgs = append(buildpkgs, pkg)
|
||||
}
|
||||
|
||||
// print package status, and error logs if any
|
||||
for _, pkg := range buildpkgs {
|
||||
k := pkgKey(&pkg)
|
||||
if _, printed := w.finished[k]; printed {
|
||||
continue
|
||||
}
|
||||
if pkg.Status.BuildStatus == types.BuildStatusFailed {
|
||||
w.finished[k] = true
|
||||
fmt.Printf("--- Build FAILED: ---\n%v\n------\n", pkg.Status.BuildLog)
|
||||
} else if pkg.Status.BuildStatus == types.BuildStatusSucceeded {
|
||||
w.finished[k] = true
|
||||
fmt.Printf("--- Build SUCCEEDED ---\n")
|
||||
if len(pkg.Status.BuildLog) > 0 {
|
||||
fmt.Printf("%v\n------\n", pkg.Status.BuildLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there are no builds running, we can stop polling
|
||||
if !keepWaiting {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func pkgKey(pkg *fv1.Package) string {
|
||||
// packages are mutable so we want to keep track of them by resource version
|
||||
return fmt.Sprintf("%v:%v:%v", pkg.Metadata.Name, pkg.Metadata.Namespace, pkg.Metadata.ResourceVersion)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
"github.com/fission/fission/pkg/types"
|
||||
)
|
||||
|
||||
func canaryConfigCreate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
canaryConfigName := c.String("name")
|
||||
// canary configs can be created for functions in the same namespace
|
||||
if len(canaryConfigName) == 0 {
|
||||
log.Fatal("Need a name, use --name.")
|
||||
}
|
||||
|
||||
trigger := c.String("httptrigger")
|
||||
newFunc := c.String("newfunction")
|
||||
oldFunc := c.String("oldfunction")
|
||||
ns := c.String("fnNamespace")
|
||||
incrementStep := c.Int("increment-step")
|
||||
failureThreshold := c.Int("failure-threshold")
|
||||
incrementInterval := c.String("increment-interval")
|
||||
|
||||
// check for time parsing
|
||||
_, err := time.ParseDuration(incrementInterval)
|
||||
util.CheckErr(err, "parsing time duration.")
|
||||
|
||||
// check that the trigger exists in the same namespace.
|
||||
m := &metav1.ObjectMeta{
|
||||
Name: trigger,
|
||||
Namespace: ns,
|
||||
}
|
||||
|
||||
htTrigger, err := client.HTTPTriggerGet(m)
|
||||
if err != nil {
|
||||
util.CheckErr(err, "find trigger referenced in the canary config")
|
||||
}
|
||||
|
||||
// check that the trigger has function reference type function weights
|
||||
if htTrigger.Spec.FunctionReference.Type != types.FunctionReferenceTypeFunctionWeights {
|
||||
log.Fatal("Canary config cannot be created for http triggers that do not reference functions by weights")
|
||||
}
|
||||
|
||||
// check that the trigger references same functions in the function weights
|
||||
_, ok := htTrigger.Spec.FunctionReference.FunctionWeights[newFunc]
|
||||
if !ok {
|
||||
log.Fatal(fmt.Sprintf("HTTP Trigger doesn't reference the function %s in Canary Config", newFunc))
|
||||
}
|
||||
|
||||
_, ok = htTrigger.Spec.FunctionReference.FunctionWeights[oldFunc]
|
||||
if !ok {
|
||||
log.Fatal(fmt.Sprintf("HTTP Trigger doesn't reference the function %s in Canary Config", oldFunc))
|
||||
}
|
||||
|
||||
// check that the functions exist in the same namespace
|
||||
fnList := []string{newFunc, oldFunc}
|
||||
err = util.CheckFunctionExistence(client, fnList, ns)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("checkFunctionExistence err : %v", err))
|
||||
}
|
||||
|
||||
// finally create canaryCfg in the same namespace as the functions referenced
|
||||
canaryCfg := &fv1.CanaryConfig{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: canaryConfigName,
|
||||
Namespace: ns,
|
||||
},
|
||||
Spec: fv1.CanaryConfigSpec{
|
||||
Trigger: trigger,
|
||||
NewFunction: newFunc,
|
||||
OldFunction: oldFunc,
|
||||
WeightIncrement: incrementStep,
|
||||
WeightIncrementDuration: incrementInterval,
|
||||
FailureThreshold: failureThreshold,
|
||||
FailureType: fv1.FailureTypeStatusCode,
|
||||
},
|
||||
Status: fv1.CanaryConfigStatus{
|
||||
Status: fv1.CanaryConfigStatusPending,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = client.CanaryConfigCreate(canaryCfg)
|
||||
util.CheckErr(err, "create canary config")
|
||||
|
||||
fmt.Printf("canary config '%v' created\n", canaryConfigName)
|
||||
return err
|
||||
}
|
||||
|
||||
func canaryConfigGet(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
name := c.String("name")
|
||||
if len(name) == 0 {
|
||||
log.Fatal("Need a name, use --name.")
|
||||
}
|
||||
ns := c.String("canaryNamespace")
|
||||
|
||||
m := &metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
}
|
||||
|
||||
canaryCfg, err := client.CanaryConfigGet(m)
|
||||
util.CheckErr(err, "get canary config")
|
||||
|
||||
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\n", "NAME", "TRIGGER", "FUNCTION-N", "FUNCTION-N-1", "WEIGHT-INCREMENT", "INTERVAL", "FAILURE-THRESHOLD", "FAILURE-TYPE", "STATUS")
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
canaryCfg.Metadata.Name, canaryCfg.Spec.Trigger, canaryCfg.Spec.NewFunction, canaryCfg.Spec.OldFunction, canaryCfg.Spec.WeightIncrement, canaryCfg.Spec.WeightIncrementDuration,
|
||||
canaryCfg.Spec.FailureThreshold, canaryCfg.Spec.FailureType, canaryCfg.Status.Status)
|
||||
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func canaryConfigUpdate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
canaryConfigName := c.String("name")
|
||||
ns := c.String("canaryNamespace")
|
||||
if len(canaryConfigName) == 0 {
|
||||
log.Fatal("Need a name, use --name.")
|
||||
}
|
||||
|
||||
incrementStep := c.Int("increment-step")
|
||||
failureThreshold := c.Int("failure-threshold")
|
||||
incrementInterval := c.String("increment-interval")
|
||||
|
||||
// check for time parsing
|
||||
_, err := time.ParseDuration(incrementInterval)
|
||||
util.CheckErr(err, "parsing time duration.")
|
||||
|
||||
// get the current config
|
||||
m := &metav1.ObjectMeta{
|
||||
Name: canaryConfigName,
|
||||
Namespace: ns,
|
||||
}
|
||||
|
||||
var updateNeeded bool
|
||||
canaryCfg, err := client.CanaryConfigGet(m)
|
||||
util.CheckErr(err, "get canary config")
|
||||
|
||||
if incrementStep != canaryCfg.Spec.WeightIncrement {
|
||||
canaryCfg.Spec.WeightIncrement = incrementStep
|
||||
updateNeeded = true
|
||||
}
|
||||
|
||||
if failureThreshold != canaryCfg.Spec.FailureThreshold {
|
||||
canaryCfg.Spec.FailureThreshold = failureThreshold
|
||||
updateNeeded = true
|
||||
}
|
||||
|
||||
if incrementInterval != canaryCfg.Spec.WeightIncrementDuration {
|
||||
canaryCfg.Spec.WeightIncrementDuration = incrementInterval
|
||||
updateNeeded = true
|
||||
}
|
||||
|
||||
if updateNeeded {
|
||||
canaryCfg.Status.Status = fv1.CanaryConfigStatusPending
|
||||
|
||||
_, err = client.CanaryConfigUpdate(canaryCfg)
|
||||
util.CheckErr(err, "update canary config")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func canaryConfigDelete(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
canaryConfigName := c.String("name")
|
||||
ns := c.String("canaryNamespace")
|
||||
if len(canaryConfigName) == 0 {
|
||||
log.Fatal("Need a name, use --name.")
|
||||
}
|
||||
|
||||
// get the current config
|
||||
m := &metav1.ObjectMeta{
|
||||
Name: canaryConfigName,
|
||||
Namespace: ns,
|
||||
}
|
||||
|
||||
err := client.CanaryConfigDelete(m)
|
||||
util.CheckErr(err, fmt.Sprintf("delete function '%v.%v'", canaryConfigName, ns))
|
||||
|
||||
fmt.Printf("canaryconfig '%v.%v' deleted\n", canaryConfigName, ns)
|
||||
return err
|
||||
}
|
||||
|
||||
func canaryConfigList(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
ns := c.String("canaryNamespace")
|
||||
|
||||
canaryCfgs, err := client.CanaryConfigList(ns)
|
||||
util.CheckErr(err, "list canary config")
|
||||
|
||||
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\n", "NAME", "TRIGGER", "FUNCTION-N", "FUNCTION-N-1", "WEIGHT-INCREMENT", "INTERVAL", "FAILURE-THRESHOLD", "FAILURE-TYPE", "STATUS")
|
||||
for _, canaryCfg := range canaryCfgs {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
canaryCfg.Metadata.Name, canaryCfg.Spec.Trigger, canaryCfg.Spec.NewFunction, canaryCfg.Spec.OldFunction, canaryCfg.Spec.WeightIncrement, canaryCfg.Spec.WeightIncrementDuration,
|
||||
canaryCfg.Spec.FailureThreshold, canaryCfg.Spec.FailureType, canaryCfg.Status.Status)
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
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/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
)
|
||||
|
||||
func getFunctionsByEnvironment(client *client.Client, envName, envNamespace string) ([]fv1.Function, error) {
|
||||
fnList, err := client.FunctionList(metav1.NamespaceAll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fns := []fv1.Function{}
|
||||
for _, fn := range fnList {
|
||||
if fn.Spec.Environment.Name == envName && fn.Spec.Environment.Namespace == envNamespace {
|
||||
fns = append(fns, fn)
|
||||
}
|
||||
}
|
||||
return fns, nil
|
||||
}
|
||||
|
||||
func envCreate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
envName := c.String("name")
|
||||
if len(envName) == 0 {
|
||||
log.Fatal("Need a name, use --name.")
|
||||
}
|
||||
envNamespace := c.String("envNamespace")
|
||||
|
||||
envList, err := client.EnvironmentList(envNamespace)
|
||||
if err == nil && len(envList) > 0 {
|
||||
log.Verbose(2, "%d environment(s) are present in the %s namespace. "+
|
||||
"These environments are not isolated from each other; use separate namespaces if you need isolation.",
|
||||
len(envList), envNamespace)
|
||||
}
|
||||
|
||||
var poolsize int
|
||||
if c.IsSet("poolsize") {
|
||||
poolsize = c.Int("poolsize")
|
||||
} else {
|
||||
poolsize = 3
|
||||
}
|
||||
|
||||
envImg := c.String("image")
|
||||
if len(envImg) == 0 {
|
||||
log.Fatal("Need an image, use --image.")
|
||||
}
|
||||
|
||||
envVersion := c.Int("version")
|
||||
envBuilderImg := c.String("builder")
|
||||
envBuildCmd := c.String("buildcmd")
|
||||
envExternalNetwork := c.Bool("externalnetwork")
|
||||
envGracePeriod := c.Int64("period")
|
||||
if envGracePeriod <= 0 {
|
||||
envGracePeriod = 360
|
||||
}
|
||||
|
||||
if len(envBuilderImg) > 0 {
|
||||
if !c.IsSet("version") {
|
||||
envVersion = 2
|
||||
}
|
||||
if len(envBuildCmd) == 0 {
|
||||
envBuildCmd = "build"
|
||||
}
|
||||
}
|
||||
if c.IsSet("poolsize") {
|
||||
envVersion = 3
|
||||
}
|
||||
|
||||
keepArchive := c.Bool("keeparchive")
|
||||
|
||||
// Environment API interface version is not specified and
|
||||
// builder image is empty, set default interface version
|
||||
if envVersion == 0 {
|
||||
envVersion = 1
|
||||
}
|
||||
|
||||
resourceReq := getResourceReq(c, v1.ResourceRequirements{})
|
||||
|
||||
env := &fv1.Environment{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: envName,
|
||||
Namespace: envNamespace,
|
||||
},
|
||||
Spec: fv1.EnvironmentSpec{
|
||||
Version: envVersion,
|
||||
Runtime: fv1.Runtime{
|
||||
Image: envImg,
|
||||
},
|
||||
Builder: fv1.Builder{
|
||||
Image: envBuilderImg,
|
||||
Command: envBuildCmd,
|
||||
},
|
||||
Poolsize: poolsize,
|
||||
Resources: resourceReq,
|
||||
AllowAccessToExternalNetwork: envExternalNetwork,
|
||||
TerminationGracePeriod: envGracePeriod,
|
||||
KeepArchive: keepArchive,
|
||||
},
|
||||
}
|
||||
|
||||
// if we're writing a spec, don't call the API
|
||||
if c.Bool("spec") {
|
||||
specFile := fmt.Sprintf("env-%v.yaml", envName)
|
||||
err := specSave(*env, specFile)
|
||||
util.CheckErr(err, "create environment spec")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = client.EnvironmentCreate(env)
|
||||
util.CheckErr(err, "create environment")
|
||||
|
||||
fmt.Printf("environment '%v' created\n", envName)
|
||||
return err
|
||||
}
|
||||
|
||||
func envGet(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
envName := c.String("name")
|
||||
if len(envName) == 0 {
|
||||
log.Fatal("Need a name, use --name.")
|
||||
}
|
||||
envNamespace := c.String("envNamespace")
|
||||
|
||||
m := &metav1.ObjectMeta{
|
||||
Name: envName,
|
||||
Namespace: envNamespace,
|
||||
}
|
||||
env, err := client.EnvironmentGet(m)
|
||||
util.CheckErr(err, "get environment")
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", "NAME", "UID", "IMAGE")
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n",
|
||||
env.Metadata.Name, env.Metadata.UID, env.Spec.Runtime.Image)
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func envUpdate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
envName := c.String("name")
|
||||
if len(envName) == 0 {
|
||||
log.Fatal("Need a name, use --name.")
|
||||
}
|
||||
envNamespace := c.String("envNamespace")
|
||||
|
||||
envImg := c.String("image")
|
||||
envBuilderImg := c.String("builder")
|
||||
envBuildCmd := c.String("buildcmd")
|
||||
envExternalNetwork := c.Bool("externalnetwork")
|
||||
|
||||
if len(envImg) == 0 && len(envBuilderImg) == 0 && len(envBuildCmd) == 0 {
|
||||
log.Fatal("Need --image to specify env image, or use --builder to specify env builder, or use --buildcmd to specify new build command.")
|
||||
}
|
||||
|
||||
env, err := client.EnvironmentGet(&metav1.ObjectMeta{
|
||||
Name: envName,
|
||||
Namespace: envNamespace,
|
||||
})
|
||||
util.CheckErr(err, "find environment")
|
||||
|
||||
if len(envImg) > 0 {
|
||||
env.Spec.Runtime.Image = envImg
|
||||
}
|
||||
|
||||
if env.Spec.Version == 1 && (len(envBuilderImg) > 0 || len(envBuildCmd) > 0) {
|
||||
log.Fatal("Version 1 Environments do not support builders. Must specify --version=2.")
|
||||
}
|
||||
|
||||
if len(envBuilderImg) > 0 {
|
||||
env.Spec.Builder.Image = envBuilderImg
|
||||
}
|
||||
if len(envBuildCmd) > 0 {
|
||||
env.Spec.Builder.Command = envBuildCmd
|
||||
}
|
||||
|
||||
if c.IsSet("poolsize") {
|
||||
env.Spec.Poolsize = c.Int("poolsize")
|
||||
}
|
||||
|
||||
if c.IsSet("period") {
|
||||
env.Spec.TerminationGracePeriod = c.Int64("period")
|
||||
}
|
||||
|
||||
if c.IsSet("keeparchive") {
|
||||
env.Spec.KeepArchive = c.Bool("keeparchive")
|
||||
}
|
||||
|
||||
env.Spec.AllowAccessToExternalNetwork = envExternalNetwork
|
||||
|
||||
if c.IsSet("mincpu") || c.IsSet("maxcpu") || c.IsSet("minmemory") || c.IsSet("maxmemory") || c.IsSet("minscale") || c.IsSet("maxscale") {
|
||||
log.Fatal("Updating resource limits/requests for existing environments is currently unsupported; re-create the environment instead.")
|
||||
}
|
||||
|
||||
_, err = client.EnvironmentUpdate(env)
|
||||
util.CheckErr(err, "update environment")
|
||||
|
||||
fmt.Printf("environment '%v' updated\n", envName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func envDelete(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
envName := c.String("name")
|
||||
if len(envName) == 0 {
|
||||
log.Fatal("Need a name , use --name.")
|
||||
}
|
||||
envNamespace := c.String("envNamespace")
|
||||
|
||||
m := &metav1.ObjectMeta{
|
||||
Name: envName,
|
||||
Namespace: envNamespace,
|
||||
}
|
||||
err := client.EnvironmentDelete(m)
|
||||
util.CheckErr(err, "delete environment")
|
||||
|
||||
fmt.Printf("environment '%v' deleted\n", envName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func envList(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
envNamespace := c.String("envNamespace")
|
||||
|
||||
envs, err := client.EnvironmentList(envNamespace)
|
||||
util.CheckErr(err, "list environments")
|
||||
|
||||
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\n", "NAME", "UID", "IMAGE", "BUILDER_IMAGE", "POOLSIZE", "MINCPU", "MAXCPU", "MINMEMORY", "MAXMEMORY", "EXTNET", "GRACETIME")
|
||||
for _, env := range envs {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
env.Metadata.Name, env.Metadata.UID, env.Spec.Runtime.Image, env.Spec.Builder.Image, env.Spec.Poolsize,
|
||||
env.Spec.Resources.Requests.Cpu(), env.Spec.Resources.Limits.Cpu(),
|
||||
env.Spec.Resources.Requests.Memory(), env.Spec.Resources.Limits.Memory(),
|
||||
env.Spec.AllowAccessToExternalNetwork, env.Spec.TerminationGracePeriod)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getResourceReq(c *cli.Context, resources v1.ResourceRequirements) v1.ResourceRequirements {
|
||||
|
||||
var requestResources map[v1.ResourceName]resource.Quantity
|
||||
|
||||
if len(resources.Requests) == 0 {
|
||||
requestResources = make(map[v1.ResourceName]resource.Quantity)
|
||||
} else {
|
||||
requestResources = resources.Requests
|
||||
}
|
||||
|
||||
if c.IsSet("mincpu") {
|
||||
mincpu := c.Int("mincpu")
|
||||
cpuRequest, err := resource.ParseQuantity(strconv.Itoa(mincpu) + "m")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse mincpu")
|
||||
}
|
||||
requestResources[v1.ResourceCPU] = cpuRequest
|
||||
}
|
||||
|
||||
if c.IsSet("minmemory") {
|
||||
minmem := c.Int("minmemory")
|
||||
memRequest, err := resource.ParseQuantity(strconv.Itoa(minmem) + "Mi")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse minmemory")
|
||||
}
|
||||
requestResources[v1.ResourceMemory] = memRequest
|
||||
}
|
||||
|
||||
var limitResources map[v1.ResourceName]resource.Quantity
|
||||
|
||||
if len(resources.Limits) == 0 {
|
||||
limitResources = make(map[v1.ResourceName]resource.Quantity)
|
||||
} else {
|
||||
limitResources = resources.Limits
|
||||
}
|
||||
|
||||
if c.IsSet("maxcpu") {
|
||||
maxcpu := c.Int("maxcpu")
|
||||
cpuLimit, err := resource.ParseQuantity(strconv.Itoa(maxcpu) + "m")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse maxcpu")
|
||||
}
|
||||
limitResources[v1.ResourceCPU] = cpuLimit
|
||||
}
|
||||
|
||||
if c.IsSet("maxmemory") {
|
||||
maxmem := c.Int("maxmemory")
|
||||
memLimit, err := resource.ParseQuantity(strconv.Itoa(maxmem) + "Mi")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to parse maxmemory")
|
||||
}
|
||||
limitResources[v1.ResourceMemory] = memLimit
|
||||
}
|
||||
|
||||
limitCPU := limitResources[v1.ResourceCPU]
|
||||
requestCPU := requestResources[v1.ResourceCPU]
|
||||
|
||||
if limitCPU.IsZero() && !requestCPU.IsZero() {
|
||||
limitResources[v1.ResourceCPU] = requestCPU
|
||||
} else if limitCPU.Cmp(requestCPU) < 0 {
|
||||
log.Fatal(fmt.Sprintf("MinCPU (%v) cannot be greater than MaxCPU (%v)", requestCPU.String(), limitCPU.String()))
|
||||
}
|
||||
|
||||
limitMem := limitResources[v1.ResourceMemory]
|
||||
requestMem := requestResources[v1.ResourceMemory]
|
||||
|
||||
if limitMem.IsZero() && !requestMem.IsZero() {
|
||||
limitResources[v1.ResourceMemory] = requestMem
|
||||
} else if limitMem.Cmp(requestMem) < 0 {
|
||||
log.Fatal(fmt.Sprintf("MinMemory (%v) cannot be greater than MaxMemory (%v)", requestMem.String(), limitMem.String()))
|
||||
}
|
||||
|
||||
resources = v1.ResourceRequirements{
|
||||
Requests: requestResources,
|
||||
Limits: limitResources,
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
@@ -0,0 +1,851 @@
|
||||
/*
|
||||
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"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/fission/fission/pkg/types"
|
||||
"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/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/logdb"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
)
|
||||
|
||||
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 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
|
||||
|
||||
if existingInvokeStrategy != nil && existingInvokeStrategy.ExecutionStrategy.ExecutorType == types.ExecutorTypeNewdeploy {
|
||||
minScale = existingInvokeStrategy.ExecutionStrategy.MinScale
|
||||
maxScale = existingInvokeStrategy.ExecutionStrategy.MaxScale
|
||||
targetCPU = existingInvokeStrategy.ExecutionStrategy.TargetCPUPercent
|
||||
}
|
||||
|
||||
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 minScale > maxScale {
|
||||
return nil, errors.New(fmt.Sprintf("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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
spec := false
|
||||
specFile := ""
|
||||
if c.Bool("spec") {
|
||||
spec = true
|
||||
specFile = fmt.Sprintf("function-%v.yaml", fnName)
|
||||
}
|
||||
specDir := getSpecDir(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")
|
||||
pkgName := c.String("pkg")
|
||||
|
||||
secretName := c.String("secret")
|
||||
cfgMapName := c.String("configmap")
|
||||
|
||||
invokeStrategy, err := getInvokeStrategy(c, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
resourceReq := getResourceReq(c, apiv1.ResourceRequirements{})
|
||||
|
||||
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 !spec {
|
||||
_, 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 <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")
|
||||
|
||||
// create new package in the same namespace as the function.
|
||||
pkgMetadata = createPackage(client, fnNamespace, envName, envNamespace, srcArchiveFiles, deployArchiveFiles, buildcmd, specDir, specFile, noZip)
|
||||
}
|
||||
|
||||
var secrets []fv1.SecretReference
|
||||
var cfgmaps []fv1.ConfigMapReference
|
||||
|
||||
if len(secretName) > 0 {
|
||||
// check the referenced secret is in the same ns as the function, if not give a warning.
|
||||
_, 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))
|
||||
}
|
||||
|
||||
newSecret := fv1.SecretReference{
|
||||
Name: secretName,
|
||||
Namespace: fnNamespace,
|
||||
}
|
||||
secrets = []fv1.SecretReference{newSecret}
|
||||
}
|
||||
|
||||
if len(cfgMapName) > 0 {
|
||||
// check the referenced cfgmap is in the same ns as the function, if not give a warning.
|
||||
_, 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))
|
||||
}
|
||||
|
||||
newCfgMap := fv1.ConfigMapReference{
|
||||
Name: cfgMapName,
|
||||
Namespace: fnNamespace,
|
||||
}
|
||||
cfgmaps = []fv1.ConfigMapReference{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,
|
||||
},
|
||||
}
|
||||
|
||||
// if we're writing a spec, don't create the function
|
||||
if spec {
|
||||
err = 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 := c.String("method")
|
||||
if len(method) == 0 {
|
||||
method = http.MethodGet
|
||||
}
|
||||
triggerName := uuid.NewV4().String()
|
||||
ht := &fv1.HTTPTrigger{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: triggerName,
|
||||
Namespace: fnNamespace,
|
||||
},
|
||||
Spec: fv1.HTTPTriggerSpec{
|
||||
RelativeURL: triggerUrl,
|
||||
Method: getMethod(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\t%v\n", "NAME", "UID", "ENV")
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n",
|
||||
f.Metadata.Name, f.Metadata.UID, 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")
|
||||
|
||||
secretName := c.String("secret")
|
||||
cfgMapName := c.String("configmap")
|
||||
|
||||
if len(srcArchiveFiles) > 0 && len(deployArchiveFiles) > 0 {
|
||||
log.Fatal("Need either of --src or --deploy and not both arguments.")
|
||||
}
|
||||
|
||||
if len(secretName) > 0 {
|
||||
if len(function.Spec.Secrets) > 1 {
|
||||
log.Fatal("Please use 'fission spec apply' to update list of secrets")
|
||||
}
|
||||
|
||||
// check that the referenced secret is in the same ns as the function, if not give a warning.
|
||||
_, 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))
|
||||
}
|
||||
|
||||
newSecret := fv1.SecretReference{
|
||||
Name: secretName,
|
||||
Namespace: fnNamespace,
|
||||
}
|
||||
function.Spec.Secrets = []fv1.SecretReference{newSecret}
|
||||
}
|
||||
|
||||
if len(cfgMapName) > 0 {
|
||||
if len(function.Spec.ConfigMaps) > 1 {
|
||||
log.Fatal("Please use 'fission spec apply' to update list of configmaps")
|
||||
}
|
||||
|
||||
// check that the referenced cfgmap is in the same ns as the function, if not give a warning.
|
||||
_, 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))
|
||||
}
|
||||
|
||||
newCfgMap := fv1.ConfigMapReference{
|
||||
Name: cfgMapName,
|
||||
Namespace: fnNamespace,
|
||||
}
|
||||
function.Spec.ConfigMaps = []fv1.ConfigMapReference{newCfgMap}
|
||||
}
|
||||
|
||||
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 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
|
||||
function.Spec.Resources = getResourceReq(c, function.Spec.Resources)
|
||||
|
||||
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 := 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")
|
||||
}
|
||||
|
||||
pkgMetadata, err = updatePackage(client, pkg, envName, envNamespace, srcArchiveFiles, deployArchiveFiles, buildcmd, false, codeFlag)
|
||||
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\n", "NAME", "UID", "ENV", "EXECUTORTYPE", "MINSCALE", "MAXSCALE", "MINCPU", "MAXCPU", "MINMEMORY", "MAXMEMORY", "TARGETCPU")
|
||||
for _, f := range fns {
|
||||
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\n",
|
||||
f.Metadata.Name, f.Metadata.UID, 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)
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
package fission_cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
|
||||
)
|
||||
|
||||
func TestGetInvokeStrategy(t *testing.T) {
|
||||
cases := []struct {
|
||||
testArgs map[string]string
|
||||
existingInvokeStrategy *fv1.InvokeStrategy
|
||||
expectedResult *fv1.InvokeStrategy
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
// case: use default executor poolmgr
|
||||
testArgs: map[string]string{},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypePoolmgr,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: executor type set to poolmgr
|
||||
testArgs: map[string]string{"executortype": fv1.ExecutorTypePoolmgr},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypePoolmgr,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: executor type set to newdeploy
|
||||
testArgs: map[string]string{"executortype": fv1.ExecutorTypeNewdeploy},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: DEFAULT_MIN_SCALE,
|
||||
MaxScale: DEFAULT_MIN_SCALE,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: executor type change from poolmgr to newdeploy
|
||||
testArgs: map[string]string{"executortype": fv1.ExecutorTypeNewdeploy},
|
||||
existingInvokeStrategy: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypePoolmgr,
|
||||
},
|
||||
},
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: DEFAULT_MIN_SCALE,
|
||||
MaxScale: DEFAULT_MIN_SCALE,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: executor type change from newdeploy to poolmgr
|
||||
testArgs: map[string]string{"executortype": fv1.ExecutorTypePoolmgr},
|
||||
existingInvokeStrategy: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: DEFAULT_MIN_SCALE,
|
||||
MaxScale: DEFAULT_MIN_SCALE,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypePoolmgr,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: minscale < maxscale
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
"minscale": "2",
|
||||
"maxscale": "3",
|
||||
},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: 2,
|
||||
MaxScale: 3,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: minscale > maxscale
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
"minscale": "5",
|
||||
"maxscale": "3",
|
||||
},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
// case: maxscale not specified
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
"minscale": "5",
|
||||
},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
// case: minscale not specified
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
"maxscale": "3",
|
||||
},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: DEFAULT_MIN_SCALE,
|
||||
MaxScale: 3,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: maxscale set to 0
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
"maxscale": "0",
|
||||
},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
// case: maxscale set to 9 when existing is 5
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
"maxscale": "9",
|
||||
},
|
||||
existingInvokeStrategy: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: 2,
|
||||
MaxScale: 5,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: 2,
|
||||
MaxScale: 9,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: change nothing for existing strategy
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
},
|
||||
existingInvokeStrategy: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: 2,
|
||||
MaxScale: 5,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: 2,
|
||||
MaxScale: 5,
|
||||
TargetCPUPercent: DEFAULT_TARGET_CPU_PERCENTAGE,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: set target cpu percentage
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
"targetcpu": "50",
|
||||
},
|
||||
existingInvokeStrategy: nil,
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: DEFAULT_MIN_SCALE,
|
||||
MaxScale: DEFAULT_MIN_SCALE,
|
||||
TargetCPUPercent: 50,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// case: change target cpu percentage
|
||||
testArgs: map[string]string{
|
||||
"executortype": fv1.ExecutorTypeNewdeploy,
|
||||
"targetcpu": "20",
|
||||
},
|
||||
existingInvokeStrategy: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: 2,
|
||||
MaxScale: 5,
|
||||
TargetCPUPercent: 88,
|
||||
},
|
||||
},
|
||||
expectedResult: &fv1.InvokeStrategy{
|
||||
StrategyType: fv1.StrategyTypeExecution,
|
||||
ExecutionStrategy: fv1.ExecutionStrategy{
|
||||
ExecutorType: fv1.ExecutorTypeNewdeploy,
|
||||
MinScale: 2,
|
||||
MaxScale: 5,
|
||||
TargetCPUPercent: 20,
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
for k, v := range c.testArgs {
|
||||
set.String(k, v, "")
|
||||
ctx.Set(k, v)
|
||||
}
|
||||
|
||||
strategy, err := getInvokeStrategy(ctx, c.existingInvokeStrategy)
|
||||
if c.expectError {
|
||||
assert.NotNil(t, err)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, strategy.Validate(), fmt.Sprintf("Failed at test case %v", i))
|
||||
assert.Equal(t, *c.expectedResult, *strategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/urfave/cli"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
)
|
||||
|
||||
// returns one of http.Method*
|
||||
func getMethod(method string) string {
|
||||
switch strings.ToUpper(method) {
|
||||
case "GET":
|
||||
return http.MethodGet
|
||||
case "HEAD":
|
||||
return http.MethodHead
|
||||
case "POST":
|
||||
return http.MethodPost
|
||||
case "PUT":
|
||||
return http.MethodPut
|
||||
case "PATCH":
|
||||
return http.MethodPatch
|
||||
case "DELETE":
|
||||
return http.MethodDelete
|
||||
case "CONNECT":
|
||||
return http.MethodConnect
|
||||
case "OPTIONS":
|
||||
return http.MethodOptions
|
||||
case "TRACE":
|
||||
return http.MethodTrace
|
||||
}
|
||||
log.Fatal(fmt.Sprintf("Invalid HTTP Method %v", method))
|
||||
return ""
|
||||
}
|
||||
|
||||
func setHtFunctionRef(functionList []string, functionWeightsList []int) (*fv1.FunctionReference, error) {
|
||||
if len(functionList) == 1 {
|
||||
return &fv1.FunctionReference{
|
||||
Type: fv1.FunctionReferenceTypeFunctionName,
|
||||
Name: functionList[0],
|
||||
}, nil
|
||||
} else if len(functionList) == 2 {
|
||||
if len(functionWeightsList) != 2 {
|
||||
return nil, fmt.Errorf("weights of the function need to be specified when 2 functions are supplied")
|
||||
}
|
||||
|
||||
totalWeight := functionWeightsList[0] + functionWeightsList[1]
|
||||
if totalWeight != 100 {
|
||||
log.Fatal("The function weights should add up to 100")
|
||||
}
|
||||
|
||||
functionWeights := make(map[string]int, 0)
|
||||
for index := range functionList {
|
||||
functionWeights[functionList[index]] = functionWeightsList[index]
|
||||
}
|
||||
|
||||
return &fv1.FunctionReference{
|
||||
Type: fv1.FunctionReferenceTypeFunctionWeights,
|
||||
FunctionWeights: functionWeights,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("the number of functions in a trigger can be 1 or 2(for canary feature along with their weights)")
|
||||
}
|
||||
|
||||
func htCreate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
functionList := c.StringSlice("function")
|
||||
functionWeightsList := c.IntSlice("weight")
|
||||
|
||||
if len(functionList) == 0 {
|
||||
log.Fatal("Need a function name to create a trigger, use --function")
|
||||
}
|
||||
|
||||
functionRef, err := setHtFunctionRef(functionList, functionWeightsList)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
triggerName := c.String("name")
|
||||
fnNamespace := c.String("fnNamespace")
|
||||
spec := c.Bool("spec")
|
||||
|
||||
m := &metav1.ObjectMeta{
|
||||
Name: triggerName,
|
||||
Namespace: fnNamespace,
|
||||
}
|
||||
|
||||
htTrigger, err := client.HTTPTriggerGet(m)
|
||||
if htTrigger != nil {
|
||||
util.CheckErr(fmt.Errorf("duplicate trigger exists"), "choose a different name or leave it empty for fission to auto-generate it")
|
||||
}
|
||||
|
||||
triggerUrl := c.String("url")
|
||||
if len(triggerUrl) == 0 {
|
||||
log.Fatal("Need a trigger URL, use --url")
|
||||
}
|
||||
if !strings.HasPrefix(triggerUrl, "/") {
|
||||
triggerUrl = fmt.Sprintf("/%s", triggerUrl)
|
||||
}
|
||||
|
||||
method := c.String("method")
|
||||
if len(method) == 0 {
|
||||
method = "GET"
|
||||
}
|
||||
|
||||
// For Specs, the spec validate checks for function reference
|
||||
if !spec {
|
||||
err = util.CheckFunctionExistence(client, functionList, fnNamespace)
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
createIngress := false
|
||||
if c.IsSet("createingress") {
|
||||
createIngress = c.Bool("createingress")
|
||||
}
|
||||
|
||||
host := c.String("host")
|
||||
|
||||
// just name triggers by uuid.
|
||||
if triggerName == "" {
|
||||
triggerName = uuid.NewV4().String()
|
||||
}
|
||||
|
||||
ht := &fv1.HTTPTrigger{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: triggerName,
|
||||
Namespace: fnNamespace,
|
||||
},
|
||||
Spec: fv1.HTTPTriggerSpec{
|
||||
Host: host,
|
||||
RelativeURL: triggerUrl,
|
||||
Method: getMethod(method),
|
||||
FunctionReference: *functionRef,
|
||||
CreateIngress: createIngress,
|
||||
},
|
||||
}
|
||||
|
||||
// if we're writing a spec, don't call the API
|
||||
if spec {
|
||||
specFile := fmt.Sprintf("route-%v.yaml", triggerName)
|
||||
err := specSave(*ht, specFile)
|
||||
util.CheckErr(err, "create HTTP trigger spec")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = client.HTTPTriggerCreate(ht)
|
||||
util.CheckErr(err, "create HTTP trigger")
|
||||
|
||||
fmt.Printf("trigger '%v' created\n", triggerName)
|
||||
return err
|
||||
}
|
||||
|
||||
func htGet(c *cli.Context) error {
|
||||
cliClient := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
name := c.String("name")
|
||||
ns := c.String("fnNamespace")
|
||||
|
||||
m := &metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
}
|
||||
|
||||
htTrigger, err := cliClient.HTTPTriggerGet(m)
|
||||
util.CheckErr(err, "get http trigger")
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 1, 1, ' ', 0)
|
||||
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n", "NAME", "UID", "METHOD", "RELATIVE-URL", "FUNCTION-REFERENCE-TYPE", "FUNCTION(s)")
|
||||
|
||||
function := ""
|
||||
if htTrigger.Spec.FunctionReference.Type == fv1.FunctionReferenceTypeFunctionName {
|
||||
function = htTrigger.Spec.FunctionReference.Name
|
||||
} else {
|
||||
for k, v := range htTrigger.Spec.FunctionReference.FunctionWeights {
|
||||
function += fmt.Sprintf("%s:%v ", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
htTrigger.Metadata.Name, htTrigger.Metadata.UID, htTrigger.Spec.Method, htTrigger.Spec.RelativeURL,
|
||||
htTrigger.Spec.FunctionReference.Type, function)
|
||||
|
||||
w.Flush()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func htUpdate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
htName := c.String("name")
|
||||
if len(htName) == 0 {
|
||||
log.Fatal("Need name of trigger, use --name")
|
||||
}
|
||||
triggerNamespace := c.String("triggerNamespace")
|
||||
|
||||
ht, err := client.HTTPTriggerGet(&metav1.ObjectMeta{
|
||||
Name: htName,
|
||||
Namespace: triggerNamespace,
|
||||
})
|
||||
util.CheckErr(err, "get HTTP trigger")
|
||||
|
||||
if c.IsSet("function") {
|
||||
// get the functions and their weights if specified
|
||||
functionList := c.StringSlice("function")
|
||||
err := util.CheckFunctionExistence(client, functionList, triggerNamespace)
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var functionWeightsList []int
|
||||
if c.IsSet("weight") {
|
||||
functionWeightsList = c.IntSlice("weight")
|
||||
}
|
||||
|
||||
// set function reference
|
||||
functionRef, err := setHtFunctionRef(functionList, functionWeightsList)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
ht.Spec.FunctionReference = *functionRef
|
||||
}
|
||||
|
||||
if c.IsSet("createingress") {
|
||||
ht.Spec.CreateIngress = c.Bool("createingress")
|
||||
}
|
||||
|
||||
if c.IsSet("host") {
|
||||
ht.Spec.Host = c.String("host")
|
||||
}
|
||||
|
||||
_, err = client.HTTPTriggerUpdate(ht)
|
||||
util.CheckErr(err, "update HTTP trigger")
|
||||
|
||||
fmt.Printf("trigger '%v' updated\n", htName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func htDelete(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
htName := c.String("name")
|
||||
if len(htName) == 0 {
|
||||
log.Fatal("Need name of trigger to delete, use --name")
|
||||
}
|
||||
triggerNamespace := c.String("triggerNamespace")
|
||||
|
||||
err := client.HTTPTriggerDelete(&metav1.ObjectMeta{
|
||||
Name: htName,
|
||||
Namespace: triggerNamespace,
|
||||
})
|
||||
util.CheckErr(err, "delete trigger")
|
||||
|
||||
fmt.Printf("trigger '%v' deleted\n", htName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func htList(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
triggerNamespace := c.String("triggerNamespace")
|
||||
|
||||
hts, err := client.HTTPTriggerList(triggerNamespace)
|
||||
util.CheckErr(err, "list HTTP triggers")
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n", "NAME", "METHOD", "HOST", "URL", "INGRESS", "FUNCTION_NAME")
|
||||
for _, ht := range hts {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
ht.Metadata.Name, ht.Spec.Method, ht.Spec.Host, ht.Spec.RelativeURL, ht.Spec.CreateIngress, ht.Spec.FunctionReference.Name)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
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 log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
// global Verbosity of our CLI
|
||||
Verbosity int
|
||||
)
|
||||
|
||||
func Fatal(msg interface{}) {
|
||||
os.Stderr.WriteString(fmt.Sprintf("Fatal error: %v\n", msg))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func Warn(msg interface{}) {
|
||||
os.Stderr.WriteString(fmt.Sprintf("Warning: %v\n", msg))
|
||||
}
|
||||
|
||||
func Info(msg interface{}) {
|
||||
os.Stderr.WriteString(fmt.Sprintf("%v\n", msg))
|
||||
}
|
||||
|
||||
func Verbose(verbosityLevel int, format string, args ...interface{}) {
|
||||
if Verbosity >= verbosityLevel {
|
||||
fmt.Printf(format+"\n", args...)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
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 logdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
influxdbClient "github.com/influxdata/influxdb/client/v2"
|
||||
|
||||
ferror "github.com/fission/fission/pkg/error"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
)
|
||||
|
||||
const (
|
||||
INFLUXDB_DATABASE = "fissionFunctionLog"
|
||||
INFLUXDB_URL = "http://influxdb:8086/query"
|
||||
)
|
||||
|
||||
func NewInfluxDB(serverURL string) (InfluxDB, error) {
|
||||
return InfluxDB{endpoint: serverURL}, nil
|
||||
}
|
||||
|
||||
type InfluxDB struct {
|
||||
endpoint string
|
||||
}
|
||||
|
||||
func makeIndexMap(cols []string) map[string]int {
|
||||
indexMap := make(map[string]int, len(cols))
|
||||
for i := range cols {
|
||||
indexMap[cols[i]] = i
|
||||
}
|
||||
|
||||
return indexMap
|
||||
}
|
||||
|
||||
func (influx InfluxDB) GetLogs(filter LogFilter) ([]LogEntry, error) {
|
||||
timestamp := filter.Since.UnixNano()
|
||||
var queryCmd string
|
||||
|
||||
// please check "Example 4: Bind a parameter in the WHERE clause to specific tag value"
|
||||
// at https://docs.influxdata.com/influxdb/v1.2/tools/api/
|
||||
parameters := make(map[string]interface{})
|
||||
parameters["funcuid"] = filter.FuncUid
|
||||
parameters["time"] = timestamp
|
||||
//the parameters above are only for the where clause and do not work with LIMIT
|
||||
|
||||
if filter.Pod != "" {
|
||||
// wait for bug fix for fluent-bit influxdb plugin
|
||||
queryCmd = "select * from /^log*/ where (\"funcuid\" = $funcuid OR \"kubernetes_labels_functionUid\" = $funcuid) AND \"pod\" = $pod AND \"time\" > $time LIMIT " + strconv.Itoa(filter.RecordLimit)
|
||||
parameters["pod"] = filter.Pod
|
||||
} else {
|
||||
// wait for bug fix for fluent-bit influxdb plugin
|
||||
queryCmd = "select * from /^log*/ where (\"funcuid\" = $funcuid OR \"kubernetes_labels_functionUid\" = $funcuid) AND \"time\" > $time LIMIT " + strconv.Itoa(filter.RecordLimit)
|
||||
}
|
||||
|
||||
query := influxdbClient.NewQueryWithParameters(queryCmd, INFLUXDB_DATABASE, "", parameters)
|
||||
logEntries := []LogEntry{}
|
||||
response, err := influx.query(query)
|
||||
if err != nil {
|
||||
return logEntries, err
|
||||
}
|
||||
for _, r := range response.Results {
|
||||
for _, series := range r.Series {
|
||||
|
||||
//create map of columns to row indeces
|
||||
indexMap := makeIndexMap(series.Columns)
|
||||
|
||||
// TODO: Remove fallback indexes. Some of index's name changed in fluent-bit, here we add extra fallbackIndexes to address compatibility problem.
|
||||
container := indexMap["kubernetes_docker_id"]
|
||||
container_1 := indexMap["docker_container_id"] // for backward compatibility
|
||||
functionName := indexMap["kubernetes_labels_functionName"]
|
||||
funcuid := indexMap["kubernetes_labels_functionUid"]
|
||||
funcuid_1 := indexMap["funcuid"] // for backward compatibility
|
||||
funcuid_2 := indexMap["kubernetes_labels_functionUid_1"] // for backward compatibility
|
||||
logMessage := indexMap["log"]
|
||||
nameSpace := indexMap["kubernetes_namespace_name"]
|
||||
podName := indexMap["kubernetes_pod_name"]
|
||||
stream := indexMap["stream"]
|
||||
seq := indexMap["_seq"]
|
||||
|
||||
for _, row := range series.Values {
|
||||
t, err := time.Parse(time.RFC3339, row[0].(string))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
seqNum, err := strconv.Atoi(row[seq].(string))
|
||||
if err != nil {
|
||||
return logEntries, err
|
||||
}
|
||||
entry := LogEntry{
|
||||
//The attributes of the LogEntry are selected as relative to their position in InfluxDB's line protocol response
|
||||
Timestamp: t,
|
||||
Container: getEntryValue(row, container, container_1),
|
||||
FuncName: getEntryValue(row, functionName, -1),
|
||||
FuncUid: getEntryValue(row, funcuid, funcuid_1, funcuid_2),
|
||||
Message: strings.TrimSuffix(getEntryValue(row, logMessage, -1), "\n"), //log field
|
||||
Namespace: getEntryValue(row, nameSpace, -1),
|
||||
Pod: getEntryValue(row, podName, -1),
|
||||
Stream: getEntryValue(row, stream, -1),
|
||||
Sequence: seqNum, //sequence tag
|
||||
}
|
||||
logEntries = append(logEntries, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(logEntries, func(i, j int) bool {
|
||||
|
||||
if logEntries[i].Timestamp.Before(logEntries[j].Timestamp) {
|
||||
return true
|
||||
}
|
||||
if logEntries[j].Timestamp.Before(logEntries[i].Timestamp) {
|
||||
return false
|
||||
}
|
||||
return logEntries[i].Sequence < logEntries[j].Sequence
|
||||
})
|
||||
return logEntries, nil
|
||||
}
|
||||
|
||||
func (influx InfluxDB) query(query influxdbClient.Query) (*influxdbClient.Response, error) {
|
||||
queryURL, err := url.Parse(influx.endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// connect to controller first, then controller will redirect our query command
|
||||
// to influxdb and proxy back the db response.
|
||||
queryURL.Path = fmt.Sprintf("/proxy/%s", INFLUXDB)
|
||||
req, err := http.NewRequest("POST", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parametersBytes, err := json.Marshal(query.Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set up http URL query string
|
||||
params := req.URL.Query()
|
||||
params.Set("q", query.Command)
|
||||
params.Set("db", query.Database)
|
||||
params.Set("params", string(parametersBytes))
|
||||
req.URL.RawQuery = params.Encode()
|
||||
|
||||
httpClient := http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, ferror.MakeErrorFromHTTP(resp)
|
||||
}
|
||||
|
||||
// decode influxdb response
|
||||
response := influxdbClient.Response{}
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
decoder.UseNumber()
|
||||
if decoder.Decode(&response) != nil {
|
||||
return nil, fmt.Errorf("Failed to decode influxdb response: %v", err)
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// getEntryValue returns a field value in string type of log entry by providing index of log entry.
|
||||
// Since we switch from fluentd to fluent-bit, there are some field names' changed which will break
|
||||
// CLI due to empty value. For backward compatibility, getEntryValue also supports to get value from
|
||||
// fallbackIndex if exists, otherwise an empty string returned instead.
|
||||
func getEntryValue(list []interface{}, index int, fallbackIndex ...int) string {
|
||||
if index < len(list) && list[index] != nil {
|
||||
return list[index].(string)
|
||||
}
|
||||
|
||||
for _, i := range fallbackIndex {
|
||||
if i >= 0 && i < len(list) && list[i] != nil {
|
||||
return list[i].(string)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 logdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
INFLUXDB = "influxdb"
|
||||
)
|
||||
|
||||
type LogDatabase interface {
|
||||
GetLogs(LogFilter) ([]LogEntry, error)
|
||||
}
|
||||
|
||||
type LogFilter struct {
|
||||
Pod string
|
||||
Function string
|
||||
FuncUid string
|
||||
Since time.Time
|
||||
RecordLimit int
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Timestamp time.Time
|
||||
Message string
|
||||
Stream string
|
||||
Sequence int
|
||||
Container string
|
||||
Namespace string
|
||||
FuncName string
|
||||
FuncUid string
|
||||
Pod string
|
||||
}
|
||||
|
||||
func GetLogDB(dbType string, serverURL string) (LogDatabase, error) {
|
||||
switch dbType {
|
||||
case INFLUXDB:
|
||||
return NewInfluxDB(serverURL)
|
||||
}
|
||||
return nil, fmt.Errorf("log database type is incorrect, now only support %s", INFLUXDB)
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/plugin"
|
||||
"github.com/fission/fission/pkg/fission-cli/support"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
"github.com/fission/fission/pkg/info"
|
||||
"github.com/fission/fission/pkg/types"
|
||||
)
|
||||
|
||||
func cliHook(c *cli.Context) error {
|
||||
log.Verbosity = c.Int("verbosity")
|
||||
log.Verbose(2, "Verbosity = 2")
|
||||
|
||||
err := flagValueParser(c.Args())
|
||||
if err != nil {
|
||||
// The cli package wont't print out error, as a workaround we need to
|
||||
// fatal here instead of return it.
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCliApp() *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = "fission"
|
||||
app.Usage = "Serverless functions for Kubernetes"
|
||||
app.Version = info.Version
|
||||
cli.VersionPrinter = versionPrinter
|
||||
app.CustomAppHelpTemplate = helpTemplate
|
||||
app.ExtraInfo = func() map[string]string {
|
||||
info := map[string]string{}
|
||||
for _, pmd := range plugin.FindAll() {
|
||||
names := strings.Join(append([]string{pmd.Name}, pmd.Aliases...), ", ")
|
||||
info[names] = pmd.Usage
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{Name: "server", Value: "", Usage: "Fission server URL"},
|
||||
cli.IntFlag{Name: "verbosity", Value: 1, Usage: "CLI verbosity (0 is quiet, 1 is the default, 2 is verbose.)"},
|
||||
cli.BoolFlag{Name: "plugin", Hidden: true},
|
||||
}
|
||||
|
||||
// all resource create commands accept --spec
|
||||
specSaveFlag := cli.BoolFlag{Name: "spec", Usage: "Save to the spec directory instead of creating on cluster"}
|
||||
|
||||
// namespace reference for all objects
|
||||
fnNamespaceFlag := cli.StringFlag{Name: "fnNamespace, fns", Value: metav1.NamespaceDefault, Usage: "Namespace for function object"}
|
||||
envNamespaceFlag := cli.StringFlag{Name: "envNamespace, envns", Value: metav1.NamespaceDefault, Usage: "Namespace for environment object"}
|
||||
pkgNamespaceFlag := cli.StringFlag{Name: "pkgNamespace, pkgns", Value: metav1.NamespaceDefault, Usage: "Namespace for package object"}
|
||||
triggerNamespaceFlag := cli.StringFlag{Name: "triggerNamespace, triggerns", Value: metav1.NamespaceDefault, Usage: "Namespace for trigger object"}
|
||||
recorderNamespaceFlag := cli.StringFlag{Name: "recorderNamespace, recorderns", Value: metav1.NamespaceDefault, Usage: "Namespace for recorder object"}
|
||||
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"}
|
||||
htUrlFlag := cli.StringFlag{Name: "url", Usage: "URL pattern (See gorilla/mux supported patterns)"}
|
||||
|
||||
// Resource & scale related flags (Used in env and function)
|
||||
minCpu := cli.IntFlag{Name: "mincpu", Usage: "Minimum CPU to be assigned to pod (In millicore, minimum 1)"}
|
||||
maxCpu := cli.IntFlag{Name: "maxcpu", Usage: "Maximum CPU to be assigned to pod (In millicore, minimum 1)"}
|
||||
minMem := cli.IntFlag{Name: "minmemory", Usage: "Minimum memory to be assigned to pod (In megabyte)"}
|
||||
maxMem := cli.IntFlag{Name: "maxmemory", Usage: "Maximum memory to be assigned to pod (In megabyte)"}
|
||||
minScale := cli.IntFlag{Name: "minscale", Usage: "Minimum number of pods (Uses resource inputs to configure HPA)"}
|
||||
maxScale := cli.IntFlag{Name: "maxscale", Usage: "Maximum number of pods (Uses resource inputs to configure HPA)"}
|
||||
targetcpu := cli.IntFlag{Name: "targetcpu", Usage: "Target average CPU usage percentage across pods for scaling"}
|
||||
|
||||
// functions
|
||||
fnNameFlag := cli.StringFlag{Name: "name", Usage: "function name"}
|
||||
fnEnvNameFlag := cli.StringFlag{Name: "env", Usage: "environment name for function"}
|
||||
fnCodeFlag := cli.StringFlag{Name: "code", Usage: "local path or URL for source code"}
|
||||
fnDeployArchiveFlag := cli.StringSliceFlag{Name: "deployarchive, deploy", Usage: "local path or URL for deployment archive"}
|
||||
fnSrcArchiveFlag := cli.StringSliceFlag{Name: "sourcearchive, src, source", Usage: "local path or URL for source archive"}
|
||||
fnPkgNameFlag := cli.StringFlag{Name: "pkgname, pkg", Usage: "Name of the existing package (--deploy and --src and --env will be ignored), should be in the same namespace as the function"}
|
||||
fnPodFlag := cli.StringFlag{Name: "pod", Usage: "function pod name, optional (use latest if unspecified)"}
|
||||
fnFollowFlag := cli.BoolFlag{Name: "follow, f", Usage: "specify if the logs should be streamed"}
|
||||
fnDetailFlag := cli.BoolFlag{Name: "detail, d", Usage: "display detailed information"}
|
||||
fnLogDBTypeFlag := cli.StringFlag{Name: "dbtype", Usage: "log database type, e.g. influxdb (currently only influxdb is supported)"}
|
||||
fnBodyFlag := cli.StringFlag{Name: "body, b", Usage: "request body"}
|
||||
fnHeaderFlag := cli.StringSliceFlag{Name: "header, H", Usage: "request headers"}
|
||||
fnQueryFlag := cli.StringSliceFlag{Name: "query, q", Usage: "request query parameters: -q key1=value1 -q key2=value2"}
|
||||
fnEntryPointFlag := cli.StringFlag{Name: "entrypoint", Usage: "entry point for environment v2 to load with"}
|
||||
fnBuildCmdFlag := cli.StringFlag{Name: "buildcmd", Usage: "build command for builder to run with"}
|
||||
fnSecretFlag := cli.StringFlag{Name: "secret", Usage: "function access to secret, should be present in the same namespace as the function"}
|
||||
fnCfgMapFlag := cli.StringFlag{Name: "configmap", Usage: "function access to configmap, should be present in the same namespace as the function"}
|
||||
fnLogCountFlag := cli.StringFlag{Name: "recordcount", Usage: "the n most recent log records"}
|
||||
fnForceFlag := cli.BoolFlag{Name: "force", Usage: "Force update a package even if it is used by one or more functions"}
|
||||
fnExecutorTypeFlag := cli.StringFlag{Name: "executortype", Value: types.ExecutorTypePoolmgr, Usage: "Executor type for execution; one of 'poolmgr', 'newdeploy' defaults to 'poolmgr'"}
|
||||
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, fnEntryPointFlag, fnBuildCmdFlag, fnPkgNameFlag, htUrlFlag, htMethodFlag, minCpu, maxCpu, minMem, maxMem, minScale, maxScale, fnExecutorTypeFlag, targetcpu, fnCfgMapFlag, fnSecretFlag}, 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, fnEntryPointFlag, fnPkgNameFlag, pkgNamespaceFlag, fnBuildCmdFlag, fnForceFlag, minCpu, maxCpu, minMem, maxMem, minScale, maxScale, fnExecutorTypeFlag, targetcpu}, Action: fnUpdate},
|
||||
{Name: "delete", Usage: "Delete function", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag}, Action: fnDelete},
|
||||
// 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, fnLogCountFlag}, Action: fnLogs},
|
||||
{Name: "test", Usage: "Test a function", Flags: []cli.Flag{fnNameFlag, fnNamespaceFlag, fnEnvNameFlag,
|
||||
fnCodeFlag, fnSrcArchiveFlag, htMethodFlag, fnBodyFlag, fnHeaderFlag, fnQueryFlag, fnTimeoutFlag},
|
||||
Action: fnTest},
|
||||
}
|
||||
|
||||
// httptriggers
|
||||
htNameFlag := cli.StringFlag{Name: "name", Usage: "HTTP Trigger name"}
|
||||
htHostFlag := cli.StringFlag{Name: "host", Usage: "FQDN of the network host for route"}
|
||||
htIngressFlag := cli.BoolFlag{Name: "createingress", Usage: "Creates ingress with same URL, defaults to false"}
|
||||
htFnNameFlag := cli.StringSliceFlag{Name: "function", Usage: "Name(s) of the function for this trigger. If 2 functions are supplied with this flag, traffic gets routed to them based on weights supplied with --weight flag."}
|
||||
htFnWeightFlag := cli.IntSliceFlag{Name: "weight", Usage: "Weight for each function supplied with --function flag, in the same order. Used for canary deployment"}
|
||||
|
||||
htSubcommands := []cli.Command{
|
||||
|
||||
{Name: "create", Aliases: []string{"add"}, Usage: "Create HTTP trigger", Flags: []cli.Flag{htNameFlag, htMethodFlag, htUrlFlag, htFnNameFlag, htHostFlag, htIngressFlag, fnNamespaceFlag, specSaveFlag, htFnWeightFlag}, Action: htCreate},
|
||||
{Name: "get", Usage: "Get HTTP trigger", Flags: []cli.Flag{htNameFlag}, Action: htGet},
|
||||
{Name: "update", Usage: "Update HTTP trigger", Flags: []cli.Flag{htNameFlag, triggerNamespaceFlag, htFnNameFlag, htHostFlag, htIngressFlag, htFnWeightFlag}, Action: htUpdate},
|
||||
{Name: "delete", Usage: "Delete HTTP trigger", Flags: []cli.Flag{htNameFlag, triggerNamespaceFlag}, Action: htDelete},
|
||||
{Name: "list", Usage: "List HTTP triggers", Flags: []cli.Flag{triggerNamespaceFlag}, Action: htList},
|
||||
}
|
||||
|
||||
// timetriggers
|
||||
ttNameFlag := cli.StringFlag{Name: "name", Usage: "Time Trigger name"}
|
||||
ttCronFlag := cli.StringFlag{Name: "cron", Usage: "Time trigger cron spec with each asterisk representing respectively second, minute, hour, the day of the month, month and day of the week. Also supports readable formats like '@every 5m', '@hourly'"}
|
||||
ttFnNameFlag := cli.StringFlag{Name: "function", Usage: "Function name"}
|
||||
ttRoundFlag := cli.IntFlag{Name: "round", Value: 1, Usage: "Get next N rounds of invocation time"}
|
||||
ttSubcommands := []cli.Command{
|
||||
{Name: "create", Aliases: []string{"add"}, Usage: "Create time trigger", Flags: []cli.Flag{ttNameFlag, ttFnNameFlag, fnNamespaceFlag, ttCronFlag, specSaveFlag}, Action: ttCreate},
|
||||
{Name: "get", Usage: "Get time trigger", Flags: []cli.Flag{triggerNamespaceFlag}, Action: ttGet},
|
||||
{Name: "update", Usage: "Update time trigger", Flags: []cli.Flag{ttNameFlag, triggerNamespaceFlag, ttCronFlag, ttFnNameFlag}, Action: ttUpdate},
|
||||
{Name: "delete", Usage: "Delete time trigger", Flags: []cli.Flag{ttNameFlag, triggerNamespaceFlag}, Action: ttDelete},
|
||||
{Name: "list", Usage: "List time triggers", Flags: []cli.Flag{triggerNamespaceFlag}, Action: ttList},
|
||||
{Name: "showschedule", Aliases: []string{"show"}, Usage: "Show schedule for cron spec", Flags: []cli.Flag{ttCronFlag, ttRoundFlag}, Action: ttTest},
|
||||
}
|
||||
|
||||
// Message queue trigger
|
||||
mqtNameFlag := cli.StringFlag{Name: "name", Usage: "Message queue Trigger name"}
|
||||
mqtFnNameFlag := cli.StringFlag{Name: "function", Usage: "Function name"}
|
||||
mqtMQTypeFlag := cli.StringFlag{Name: "mqtype", Value: "nats-streaming", Usage: "Message queue type, e.g. nats-streaming, azure-storage-queue (optional)"}
|
||||
mqtTopicFlag := cli.StringFlag{Name: "topic", Usage: "Message queue Topic the trigger listens on"}
|
||||
mqtRespTopicFlag := cli.StringFlag{Name: "resptopic", Usage: "Topic that the function response is sent on (optional; response discarded if unspecified)"}
|
||||
mqtErrorTopicFlag := cli.StringFlag{Name: "errortopic", Usage: "Topic that the function error messages are sent to (optional; errors discarded if unspecified"}
|
||||
mqtMaxRetries := cli.IntFlag{Name: "maxretries", Value: 0, Usage: "Maximum number of times the function will be retried upon failure (optional; default is 0)"}
|
||||
mqtMsgContentType := cli.StringFlag{Name: "contenttype, c", Value: "application/json", Usage: "Content type of messages that publish to the topic (optional)"}
|
||||
mqtSubcommands := []cli.Command{
|
||||
{Name: "create", Aliases: []string{"add"}, Usage: "Create Message queue trigger", Flags: []cli.Flag{mqtNameFlag, mqtFnNameFlag, fnNamespaceFlag, mqtMQTypeFlag, mqtTopicFlag, mqtRespTopicFlag, mqtErrorTopicFlag, mqtMaxRetries, mqtMsgContentType, specSaveFlag}, Action: mqtCreate},
|
||||
{Name: "get", Usage: "Get message queue trigger", Flags: []cli.Flag{triggerNamespaceFlag}, Action: mqtGet},
|
||||
{Name: "update", Usage: "Update message queue trigger", Flags: []cli.Flag{mqtNameFlag, triggerNamespaceFlag, mqtTopicFlag, mqtRespTopicFlag, mqtErrorTopicFlag, mqtMaxRetries, mqtFnNameFlag, mqtMsgContentType}, Action: mqtUpdate},
|
||||
{Name: "delete", Usage: "Delete message queue trigger", Flags: []cli.Flag{mqtNameFlag, triggerNamespaceFlag}, Action: mqtDelete},
|
||||
{Name: "list", Usage: "List message queue triggers", Flags: []cli.Flag{mqtMQTypeFlag, triggerNamespaceFlag}, Action: mqtList},
|
||||
}
|
||||
|
||||
// Recorders
|
||||
recNameFlag := cli.StringFlag{Name: "name", Usage: "Recorder name"}
|
||||
recFnFlag := cli.StringFlag{Name: "function", Usage: "Record Function name(s): --function=fnA"}
|
||||
recTriggersFlag := cli.StringSliceFlag{Name: "trigger", Usage: "Record Trigger name(s): --trigger=trigger1,trigger2,trigger3"}
|
||||
//recRetentionPolFlag := cli.StringFlag{Name: "retention", Usage: "Retention policy (number of days)"}
|
||||
//recEvictionPolFlag := cli.StringFlag{Name: "eviction", Usage: "Eviction policy (default LRU)"}
|
||||
recEnabled := cli.BoolFlag{Name: "enable", Usage: "Enable recorder"}
|
||||
recDisabled := cli.BoolFlag{Name: "disable", Usage: "Disable recorder"}
|
||||
recSubcommands := []cli.Command{
|
||||
{Name: "create", Aliases: []string{"add"}, Usage: "Create recorder", Flags: []cli.Flag{recNameFlag, recFnFlag, recTriggersFlag, specSaveFlag}, Action: recorderCreate},
|
||||
{Name: "get", Usage: "Get recorder", Flags: []cli.Flag{recNameFlag}, Action: recorderGet},
|
||||
{Name: "update", Usage: "Update recorder", Flags: []cli.Flag{recNameFlag, recFnFlag, recTriggersFlag, recEnabled, recDisabled}, Action: recorderUpdate},
|
||||
{Name: "delete", Usage: "Delete recorder", Flags: []cli.Flag{recNameFlag, recorderNamespaceFlag}, Action: recorderDelete},
|
||||
{Name: "list", Usage: "List recorders", Flags: []cli.Flag{}, Action: recorderList},
|
||||
}
|
||||
|
||||
// View records
|
||||
filterTimeFrom := cli.StringFlag{Name: "from", Usage: "Filter records by time interval; specify start of interval"}
|
||||
filterTimeTo := cli.StringFlag{Name: "to", Usage: "Filter records by time interval; specify end of interval"}
|
||||
filterFunction := cli.StringFlag{Name: "function", Usage: "Filter records by function"}
|
||||
filterTrigger := cli.StringFlag{Name: "trigger", Usage: "Filter records by trigger"}
|
||||
verbosityFlag := cli.BoolFlag{Name: "v", Usage: "Toggle verbosity -- view more detailed requests/responses"}
|
||||
vvFlag := cli.BoolFlag{Name: "vv", Usage: "Toggle verbosity -- view raw requests/responses"}
|
||||
recViewSubcommands := []cli.Command{
|
||||
{Name: "view", Usage: "View existing records", Flags: []cli.Flag{filterTimeTo, filterTimeFrom, filterFunction, filterTrigger, verbosityFlag, vvFlag}, Action: recordsView},
|
||||
}
|
||||
|
||||
// Replay records
|
||||
reqIDFlag := cli.StringFlag{Name: "reqUID", Usage: "Replay a particular request by providing the reqUID (to view reqUIDs, do 'fission records view')"}
|
||||
|
||||
// environments
|
||||
envNameFlag := cli.StringFlag{Name: "name", Usage: "Environment name"}
|
||||
envPoolsizeFlag := cli.IntFlag{Name: "poolsize", Value: 3, Usage: "Size of the pool"}
|
||||
envImageFlag := cli.StringFlag{Name: "image", Usage: "Environment image URL"}
|
||||
envBuilderImageFlag := cli.StringFlag{Name: "builder", Usage: "Environment builder image URL (optional)"}
|
||||
envBuildCmdFlag := cli.StringFlag{Name: "buildcmd", Usage: "Build command for environment builder to build source package (optional)"}
|
||||
envKeepArchiveFlag := cli.BoolFlag{Name: "keeparchive", Usage: "Keep the archive instead of extracting it into a directory (optional, defaults to false)"}
|
||||
envExternalNetworkFlag := cli.BoolFlag{Name: "externalnetwork", Usage: "Allow environment access external network when istio feature enabled (optional, defaults to false)"}
|
||||
envTerminationGracePeriodFlag := cli.Int64Flag{Name: "graceperiod, period", Value: 360, Usage: "The grace time (in seconds) for pod to perform connection draining before termination (optional)"}
|
||||
envVersionFlag := cli.IntFlag{Name: "version", Value: 1, Usage: "Environment API version (1 means v1 interface)"}
|
||||
envSubcommands := []cli.Command{
|
||||
{Name: "create", Aliases: []string{"add"}, Usage: "Add an environment", Flags: []cli.Flag{envNameFlag, envNamespaceFlag, envPoolsizeFlag, envImageFlag, envBuilderImageFlag, envBuildCmdFlag, envKeepArchiveFlag, minCpu, maxCpu, minMem, maxMem, envVersionFlag, envExternalNetworkFlag, envTerminationGracePeriodFlag, specSaveFlag}, Action: envCreate},
|
||||
{Name: "get", Usage: "Get environment details", Flags: []cli.Flag{envNameFlag, envNamespaceFlag}, Action: envGet},
|
||||
{Name: "update", Usage: "Update environment", Flags: []cli.Flag{envNameFlag, envNamespaceFlag, envPoolsizeFlag, envImageFlag, envBuilderImageFlag, envBuildCmdFlag, envKeepArchiveFlag, minCpu, maxCpu, minMem, maxMem, envExternalNetworkFlag, envTerminationGracePeriodFlag}, Action: envUpdate},
|
||||
{Name: "delete", Usage: "Delete environment", Flags: []cli.Flag{envNameFlag, envNamespaceFlag}, Action: envDelete},
|
||||
{Name: "list", Usage: "List all environments", Flags: []cli.Flag{envNamespaceFlag}, Action: envList},
|
||||
}
|
||||
|
||||
// watches
|
||||
wNameFlag := cli.StringFlag{Name: "name", Usage: "Watch name"}
|
||||
wFnNameFlag := cli.StringFlag{Name: "function", Usage: "Function name"}
|
||||
wNamespaceFlag := cli.StringFlag{Name: "ns", Usage: "Namespace of resource to watch"}
|
||||
wObjTypeFlag := cli.StringFlag{Name: "type", Usage: "Type of resource to watch (Pod, Service, etc.)"}
|
||||
wLabelsFlag := cli.StringFlag{Name: "labels", Usage: "Label selector of the form a=b,c=d"}
|
||||
wSubCommands := []cli.Command{
|
||||
{Name: "create", Aliases: []string{"add"}, Usage: "Create a watch", Flags: []cli.Flag{wFnNameFlag, fnNamespaceFlag, wNamespaceFlag, wObjTypeFlag, wLabelsFlag, specSaveFlag}, Action: wCreate},
|
||||
{Name: "get", Usage: "Get details about a watch", Flags: []cli.Flag{wNameFlag, triggerNamespaceFlag}, Action: wGet},
|
||||
// TODO add update flag when supported
|
||||
{Name: "delete", Usage: "Delete watch", Flags: []cli.Flag{wNameFlag, triggerNamespaceFlag}, Action: wDelete},
|
||||
{Name: "list", Usage: "List all watches", Flags: []cli.Flag{triggerNamespaceFlag}, Action: wList},
|
||||
}
|
||||
|
||||
// packages
|
||||
pkgNameFlag := cli.StringFlag{Name: "name", Usage: "Package name"}
|
||||
pkgForceFlag := cli.BoolFlag{Name: "force, f", Usage: "Force update a package even if it is used by one or more functions"}
|
||||
pkgEnvironmentFlag := cli.StringFlag{Name: "env", Usage: "Environment name"}
|
||||
pkgSrcArchiveFlag := cli.StringSliceFlag{Name: "sourcearchive, src", Usage: "Local path or URL for source archive"}
|
||||
pkgDeployArchiveFlag := cli.StringSliceFlag{Name: "deployarchive, deploy", Usage: "Local path or URL for binary archive"}
|
||||
pkgBuildCmdFlag := cli.StringFlag{Name: "buildcmd", Usage: "Build command for builder to run with"}
|
||||
pkgOutputFlag := cli.StringFlag{Name: "output, o", Usage: "Output filename to save archive content"}
|
||||
pkgOrphanFlag := cli.BoolFlag{Name: "orphan", Usage: "orphan packages that are not referenced by any function"}
|
||||
pkgSubCommands := []cli.Command{
|
||||
{Name: "create", Usage: "Create new package", Flags: []cli.Flag{pkgNamespaceFlag, pkgEnvironmentFlag, envNamespaceFlag, pkgSrcArchiveFlag, pkgDeployArchiveFlag, pkgBuildCmdFlag}, Action: pkgCreate},
|
||||
{Name: "update", Usage: "Update package", Flags: []cli.Flag{pkgNameFlag, pkgNamespaceFlag, pkgEnvironmentFlag, envNamespaceFlag, pkgSrcArchiveFlag, pkgDeployArchiveFlag, pkgBuildCmdFlag, pkgForceFlag}, Action: pkgUpdate},
|
||||
{Name: "rebuild", Usage: "Rebuild a failed package", Flags: []cli.Flag{pkgNameFlag, pkgNamespaceFlag}, Action: pkgRebuild},
|
||||
{Name: "getsrc", Usage: "Get source archive content", Flags: []cli.Flag{pkgNameFlag, pkgNamespaceFlag, pkgOutputFlag}, Action: pkgSourceGet},
|
||||
{Name: "getdeploy", Usage: "Get deployment archive content", Flags: []cli.Flag{pkgNameFlag, pkgNamespaceFlag, pkgOutputFlag}, Action: pkgDeployGet},
|
||||
{Name: "info", Usage: "Show package information", Flags: []cli.Flag{pkgNameFlag, pkgNamespaceFlag}, Action: pkgInfo},
|
||||
{Name: "list", Usage: "List all packages", Flags: []cli.Flag{pkgOrphanFlag, pkgNamespaceFlag}, Action: pkgList},
|
||||
{Name: "delete", Usage: "Delete package", Flags: []cli.Flag{pkgNameFlag, pkgNamespaceFlag, pkgForceFlag, pkgOrphanFlag}, Action: pkgDelete},
|
||||
}
|
||||
|
||||
// upgrades, data migrations
|
||||
upgradeFileFlag := cli.StringFlag{Name: "file", Usage: "JSON file containing all fission state"}
|
||||
upgradeSubCommands := []cli.Command{
|
||||
{Name: "dump", Usage: "Dump all state from a v0.1 fission installation", Flags: []cli.Flag{upgradeFileFlag}, Action: upgradeDumpState},
|
||||
{Name: "restore", Usage: "Restore state dumped from a v0.1 install into a v0.2+ install", Flags: []cli.Flag{upgradeFileFlag}, Action: upgradeRestoreState},
|
||||
}
|
||||
|
||||
// specs
|
||||
specDirFlag := cli.StringFlag{Name: "specdir", Usage: "Directory to store specs, defaults to ./specs"}
|
||||
specNameFlag := cli.StringFlag{Name: "name", Usage: "(optional) Name for the app, applied to resources as a Kubernetes annotation"}
|
||||
specWaitFlag := cli.BoolFlag{Name: "wait", Usage: "Wait for package builds"}
|
||||
specWatchFlag := cli.BoolFlag{Name: "watch", Usage: "Watch local files for change, and re-apply specs as necessary"}
|
||||
specDeleteFlag := cli.BoolFlag{Name: "delete", Usage: "Allow apply to delete resources that no longer exist in the specification"}
|
||||
specSubCommands := []cli.Command{
|
||||
{Name: "init", Usage: "Create an initial declarative app specification", Flags: []cli.Flag{specDirFlag, specNameFlag}, Action: specInit},
|
||||
{Name: "validate", Usage: "Validate Fission app specification", Flags: []cli.Flag{specDirFlag}, Action: specValidate},
|
||||
{Name: "apply", Usage: "Create, update, or delete Fission resources from app specification", Flags: []cli.Flag{specDirFlag, specDeleteFlag, specWaitFlag, specWatchFlag}, Action: specApply},
|
||||
{Name: "destroy", Usage: "Delete all Fission resources in the app specification", Flags: []cli.Flag{specDirFlag}, Action: specDestroy},
|
||||
{Name: "helm", Usage: "Create a helm chart from the app specification", Flags: []cli.Flag{specDirFlag}, Action: specHelm, Hidden: true},
|
||||
}
|
||||
|
||||
// support
|
||||
supportOutputFlag := cli.StringFlag{Name: "output, o", Value: support.DEFAULT_OUTPUT_DIR, Usage: "Output directory to save dump archive/files"}
|
||||
supportNoZipFlag := cli.BoolFlag{Name: "nozip", Usage: "Save dump information into multiple files instead of single zip file"}
|
||||
supportSubCommands := []cli.Command{
|
||||
{Name: "dump", Usage: "Collect & dump all necessary for troubleshooting", Flags: []cli.Flag{supportOutputFlag, supportNoZipFlag}, Action: support.DumpInfo},
|
||||
}
|
||||
|
||||
// canary configs
|
||||
canaryConfigNameFlag := cli.StringFlag{Name: "name", Usage: "Name for the canary config"}
|
||||
triggerNameFlag := cli.StringFlag{Name: "httptrigger", Usage: "Http trigger that this config references"}
|
||||
newFunc := cli.StringFlag{Name: "newfunction", Usage: "New version of the function"}
|
||||
oldFunc := cli.StringFlag{Name: "oldfunction", Usage: "Old stable version of the function"}
|
||||
weightIncrementFlag := cli.IntFlag{Name: "increment-step", Value: 20, Usage: "Weight increment step for function"}
|
||||
incrementIntervalFlag := cli.StringFlag{Name: "increment-interval", Value: "2m", Usage: "Weight increment interval, string representation of time.Duration, ex : 1m, 2h, 2d"}
|
||||
failureThresholdFlag := cli.IntFlag{Name: "failure-threshold", Value: 10, Usage: "Threshold in percentage beyond which the new version of the function is considered unstable"}
|
||||
canarySubCommands := []cli.Command{
|
||||
{Name: "create", Usage: "Create a canary config", Flags: []cli.Flag{canaryConfigNameFlag, triggerNameFlag, newFunc, oldFunc, fnNamespaceFlag, weightIncrementFlag, incrementIntervalFlag, failureThresholdFlag}, Action: canaryConfigCreate},
|
||||
{Name: "get", Usage: "View parameters in a canary config", Flags: []cli.Flag{canaryConfigNameFlag, canaryNamespaceFlag}, Action: canaryConfigGet},
|
||||
{Name: "update", Usage: "Update parameters of a canary config", Flags: []cli.Flag{canaryConfigNameFlag, canaryNamespaceFlag, incrementIntervalFlag, weightIncrementFlag, failureThresholdFlag}, Action: canaryConfigUpdate},
|
||||
{Name: "delete", Usage: "Delete a canary config", Flags: []cli.Flag{canaryConfigNameFlag, canaryNamespaceFlag}, Action: canaryConfigDelete},
|
||||
{Name: "list", Usage: "List all canary configs in a namespace", Flags: []cli.Flag{canaryNamespaceFlag}, Action: canaryConfigList},
|
||||
}
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
{Name: "function", Aliases: []string{"fn"}, Usage: "Create, update and manage functions", Subcommands: fnSubcommands},
|
||||
{Name: "httptrigger", Aliases: []string{"ht", "route"}, Usage: "Manage HTTP triggers (routes) for functions", Subcommands: htSubcommands},
|
||||
{Name: "timetrigger", Aliases: []string{"tt", "timer"}, Usage: "Manage Time triggers (timers) for functions", Subcommands: ttSubcommands},
|
||||
{Name: "mqtrigger", Aliases: []string{"mqt", "messagequeue"}, Usage: "Manage message queue triggers for functions", Subcommands: mqtSubcommands},
|
||||
{Name: "recorder", Usage: "Manage recorders for functions", Subcommands: recSubcommands, Hidden: true},
|
||||
{Name: "records", Usage: "View records with optional filters", Subcommands: recViewSubcommands, Hidden: true},
|
||||
{Name: "replay", Usage: "Replay records", Flags: []cli.Flag{reqIDFlag}, Action: replay},
|
||||
{Name: "environment", Aliases: []string{"env"}, Usage: "Manage environments", Subcommands: envSubcommands},
|
||||
{Name: "watch", Aliases: []string{"w"}, Usage: "Manage watches", Subcommands: wSubCommands},
|
||||
{Name: "package", Aliases: []string{"pkg"}, Usage: "Manage packages", Subcommands: pkgSubCommands},
|
||||
{Name: "spec", Aliases: []string{"specs"}, Usage: "Manage a declarative app specification", Subcommands: specSubCommands},
|
||||
{Name: "upgrade", Aliases: []string{}, Usage: "Upgrade tool from fission v0.1", Subcommands: upgradeSubCommands},
|
||||
{Name: "support", Usage: "Collect an archive of diagnostic information for support", Subcommands: supportSubCommands},
|
||||
cmdPlugin,
|
||||
{Name: "canary-config", Aliases: []string{}, Usage: "Create, Update and manage Canary Configs", Subcommands: canarySubCommands},
|
||||
}
|
||||
|
||||
app.Before = cliHook
|
||||
app.Action = handleNoCommand
|
||||
return app
|
||||
}
|
||||
|
||||
func handleNoCommand(ctx *cli.Context) error {
|
||||
if ctx.GlobalBool("version") {
|
||||
versionPrinter(ctx)
|
||||
return nil
|
||||
}
|
||||
if ctx.GlobalBool("plugin") {
|
||||
bs, err := json.Marshal(plugin.Metadata{
|
||||
Version: info.Version,
|
||||
Usage: ctx.App.Usage,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed to marshal plugin metadata to JSON: %v", err))
|
||||
}
|
||||
fmt.Println(string(bs))
|
||||
return nil
|
||||
}
|
||||
if len(ctx.Args()) > 0 {
|
||||
handleCommandNotFound(ctx, ctx.Args().First())
|
||||
return nil
|
||||
}
|
||||
|
||||
return cli.ShowAppHelp(ctx)
|
||||
}
|
||||
|
||||
func handleCommandNotFound(ctx *cli.Context, subCommand string) {
|
||||
pmd, err := plugin.Find(subCommand)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case plugin.ErrPluginNotFound:
|
||||
url, ok := plugin.SearchRegistries(subCommand)
|
||||
if !ok {
|
||||
log.Fatal("No help topic for '" + subCommand + "'")
|
||||
}
|
||||
log.Fatal(fmt.Sprintf(`Command '%v' is not installed.
|
||||
It is available to download at '%v'.
|
||||
|
||||
To install it for your local Fission CLI:
|
||||
1. Download the plugin binary for your OS from the URL
|
||||
2. Ensure that the plugin binary is executable: chmod +x <binary>
|
||||
2. Add the plugin binary to your $PATH: mv <binary> /usr/local/bin/fission-%v`, subCommand, url, subCommand))
|
||||
default:
|
||||
log.Fatal("Error occurred when invoking " + subCommand + ": " + err.Error())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Rebuild global arguments string (urfave/cli does not have an option to get the raw input of the global flags)
|
||||
var globalArgs []string
|
||||
for _, globalFlagName := range ctx.GlobalFlagNames() {
|
||||
if globalFlagName == "plugin" {
|
||||
continue
|
||||
}
|
||||
val := fmt.Sprintf("%v", ctx.GlobalGeneric(globalFlagName))
|
||||
if len(val) > 0 {
|
||||
globalArgs = append(globalArgs, fmt.Sprintf("--%v", globalFlagName), val)
|
||||
}
|
||||
}
|
||||
args := append(globalArgs, ctx.Args().Tail()...)
|
||||
|
||||
err = plugin.Exec(pmd, args)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func versionPrinter(_ *cli.Context) {
|
||||
client := util.GetApiClient(util.GetServerUrl())
|
||||
ver := util.GetVersion(client)
|
||||
fmt.Print(string(ver))
|
||||
}
|
||||
|
||||
func flagValueParser(args []string) error {
|
||||
// all input value for flags are properly set
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var flagIndexes []int
|
||||
var errorFlags []string
|
||||
|
||||
// find out all flag indexes
|
||||
for i, v := range args {
|
||||
// support both flags with "--" and "-"
|
||||
if strings.HasPrefix(v, "-") {
|
||||
flagIndexes = append(flagIndexes, i)
|
||||
}
|
||||
}
|
||||
|
||||
// add total length of args to indicate the end of args
|
||||
flagIndexes = append(flagIndexes, len(args))
|
||||
|
||||
for i := 0; i < len(flagIndexes)-1; i++ {
|
||||
// if the difference between the flag index i and i+1
|
||||
// is bigger then 2 means that CLI receives extra arguments
|
||||
// for one flag. For example,
|
||||
// 1. fission fn create --name e1 --code examples/nodejs/* --env nodejs ...
|
||||
// The wildcard will be extracted to multiple files and cause the difference between `--code` and `--env` large than 2.
|
||||
// 2. fission fn create --spec --name e1 ...
|
||||
// The difference between --spec and --name is 1.
|
||||
if flagIndexes[i+1]-flagIndexes[i] > 2 {
|
||||
index := flagIndexes[i]
|
||||
errorFlags = append(errorFlags, args[index])
|
||||
}
|
||||
}
|
||||
|
||||
if len(errorFlags) > 0 {
|
||||
e := fmt.Sprintf("Unable to parse flags: %v\nThe argument should have only one input value. Please quote the input value if it contains wildcard characters(*).", strings.Join(errorFlags[:], ", "))
|
||||
return errors.New(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var helpTemplate = `NAME:
|
||||
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}
|
||||
|
||||
USAGE:
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
|
||||
|
||||
VERSION:
|
||||
{{.Version}}{{end}}{{end}}{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description}}{{end}}{{if .VisibleCommands}}
|
||||
|
||||
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
||||
{{.Name}}:{{end}}{{range .VisibleCommands}}
|
||||
{{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}}
|
||||
|
||||
PLUGIN COMMANDS:{{ range $name, $usage := ExtraInfo }}
|
||||
{{$name}}{{"\t"}}{{$usage}}{{end}}
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
{{range $index, $option := .VisibleFlags}}{{if $index}}
|
||||
{{end}}{{$option}}{{end}}{{end}}
|
||||
`
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
Copyrigtt 2017 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
|
||||
|
||||
tttp://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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/urfave/cli"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
"github.com/fission/fission/pkg/types"
|
||||
)
|
||||
|
||||
func mqtCreate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
mqtName := c.String("name")
|
||||
if len(mqtName) == 0 {
|
||||
mqtName = uuid.NewV4().String()
|
||||
}
|
||||
fnName := c.String("function")
|
||||
if len(fnName) == 0 {
|
||||
log.Fatal("Need a function name to create a trigger, use --function")
|
||||
}
|
||||
fnNamespace := c.String("fnNamespace")
|
||||
|
||||
var mqType fv1.MessageQueueType
|
||||
switch c.String("mqtype") {
|
||||
case "":
|
||||
mqType = types.MessageQueueTypeNats
|
||||
case types.MessageQueueTypeNats:
|
||||
mqType = types.MessageQueueTypeNats
|
||||
case types.MessageQueueTypeASQ:
|
||||
mqType = types.MessageQueueTypeASQ
|
||||
case types.MessageQueueTypeKafka:
|
||||
mqType = types.MessageQueueTypeKafka
|
||||
|
||||
default:
|
||||
log.Fatal("Unknown message queue type, currently only \"nats-streaming, azure-storage-queue, kafka \" is supported")
|
||||
|
||||
}
|
||||
|
||||
// TODO: check topic availability
|
||||
topic := c.String("topic")
|
||||
if len(topic) == 0 {
|
||||
log.Fatal("Topic cannot be empty")
|
||||
}
|
||||
respTopic := c.String("resptopic")
|
||||
|
||||
if topic == respTopic {
|
||||
// TODO maybe this should just be a warning, perhaps
|
||||
// allow it behind a --force flag
|
||||
log.Fatal("Listen topic should not equal to response topic")
|
||||
}
|
||||
|
||||
errorTopic := c.String("errortopic")
|
||||
|
||||
maxRetries := c.Int("maxretries")
|
||||
|
||||
if maxRetries < 0 {
|
||||
log.Fatal("Maximum number of retries must be a natural number, default is 0")
|
||||
}
|
||||
|
||||
contentType := c.String("contenttype")
|
||||
if len(contentType) == 0 {
|
||||
contentType = "application/json"
|
||||
}
|
||||
|
||||
checkMQTopicAvailability(mqType, topic, respTopic)
|
||||
|
||||
mqt := &fv1.MessageQueueTrigger{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: mqtName,
|
||||
Namespace: fnNamespace,
|
||||
},
|
||||
Spec: fv1.MessageQueueTriggerSpec{
|
||||
FunctionReference: fv1.FunctionReference{
|
||||
Type: types.FunctionReferenceTypeFunctionName,
|
||||
Name: fnName,
|
||||
},
|
||||
MessageQueueType: mqType,
|
||||
Topic: topic,
|
||||
ResponseTopic: respTopic,
|
||||
ErrorTopic: errorTopic,
|
||||
MaxRetries: maxRetries,
|
||||
ContentType: contentType,
|
||||
},
|
||||
}
|
||||
|
||||
// if we're writing a spec, don't call the API
|
||||
if c.Bool("spec") {
|
||||
specFile := fmt.Sprintf("mqtrigger-%v.yaml", mqtName)
|
||||
err := specSave(*mqt, specFile)
|
||||
util.CheckErr(err, "create message queue trigger spec")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := client.MessageQueueTriggerCreate(mqt)
|
||||
util.CheckErr(err, "create message queue trigger")
|
||||
|
||||
fmt.Printf("trigger '%s' created\n", mqtName)
|
||||
return err
|
||||
}
|
||||
|
||||
func mqtGet(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mqtUpdate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
mqtName := c.String("name")
|
||||
if len(mqtName) == 0 {
|
||||
log.Fatal("Need name of trigger, use --name")
|
||||
}
|
||||
mqtNs := c.String("triggerns")
|
||||
|
||||
topic := c.String("topic")
|
||||
respTopic := c.String("resptopic")
|
||||
errorTopic := c.String("errortopic")
|
||||
maxRetries := c.Int("maxretries")
|
||||
fnName := c.String("function")
|
||||
contentType := c.String("contenttype")
|
||||
|
||||
mqt, err := client.MessageQueueTriggerGet(&metav1.ObjectMeta{
|
||||
Name: mqtName,
|
||||
Namespace: mqtNs,
|
||||
})
|
||||
util.CheckErr(err, "get Time trigger")
|
||||
|
||||
// TODO : Find out if we can make a call to checkIfFunctionExists, in the same ns more importantly.
|
||||
|
||||
checkMQTopicAvailability(mqt.Spec.MessageQueueType, topic, respTopic)
|
||||
|
||||
updated := false
|
||||
if len(topic) > 0 {
|
||||
mqt.Spec.Topic = topic
|
||||
updated = true
|
||||
}
|
||||
if len(respTopic) > 0 {
|
||||
mqt.Spec.ResponseTopic = respTopic
|
||||
updated = true
|
||||
}
|
||||
if len(errorTopic) > 0 {
|
||||
mqt.Spec.ErrorTopic = errorTopic
|
||||
updated = true
|
||||
}
|
||||
if maxRetries > -1 {
|
||||
mqt.Spec.MaxRetries = maxRetries
|
||||
updated = true
|
||||
}
|
||||
if len(fnName) > 0 {
|
||||
mqt.Spec.FunctionReference.Name = fnName
|
||||
updated = true
|
||||
}
|
||||
if len(contentType) > 0 {
|
||||
mqt.Spec.ContentType = contentType
|
||||
updated = true
|
||||
}
|
||||
|
||||
if !updated {
|
||||
log.Fatal("Nothing to update. Use --topic, --resptopic, --errortopic, --maxretries or --function.")
|
||||
}
|
||||
|
||||
_, err = client.MessageQueueTriggerUpdate(mqt)
|
||||
util.CheckErr(err, "update Time trigger")
|
||||
|
||||
fmt.Printf("trigger '%v' updated\n", mqtName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mqtDelete(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
mqtName := c.String("name")
|
||||
if len(mqtName) == 0 {
|
||||
log.Fatal("Need name of trigger to delete, use --name")
|
||||
}
|
||||
mqtNs := c.String("triggerns")
|
||||
|
||||
err := client.MessageQueueTriggerDelete(&metav1.ObjectMeta{
|
||||
Name: mqtName,
|
||||
Namespace: mqtNs,
|
||||
})
|
||||
util.CheckErr(err, "delete trigger")
|
||||
|
||||
fmt.Printf("trigger '%v' deleted\n", mqtName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mqtList(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
mqtNs := c.String("triggerns")
|
||||
|
||||
mqts, err := client.MessageQueueTriggerList(c.String("mqtype"), mqtNs)
|
||||
util.CheckErr(err, "list message queue triggers")
|
||||
|
||||
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\n",
|
||||
"NAME", "FUNCTION_NAME", "MESSAGE_QUEUE_TYPE", "TOPIC", "RESPONSE_TOPIC", "ERROR_TOPIC", "MAX_RETRIES", "PUB_MSG_CONTENT_TYPE")
|
||||
for _, mqt := range mqts {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
mqt.Metadata.Name, mqt.Spec.FunctionReference.Name, mqt.Spec.MessageQueueType, mqt.Spec.Topic, mqt.Spec.ResponseTopic, mqt.Spec.ErrorTopic, mqt.Spec.MaxRetries, mqt.Spec.ContentType)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMQTopicAvailability(mqType fv1.MessageQueueType, topics ...string) {
|
||||
for _, t := range topics {
|
||||
if len(t) > 0 && !fv1.IsTopicValid(mqType, t) {
|
||||
log.Fatal(fmt.Sprintf("Invalid topic for %s: %s", mqType, t))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,771 @@
|
||||
/*
|
||||
Copyright 2017 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/fission/fission/pkg/utils"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mholt/archiver"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/urfave/cli"
|
||||
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/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
storageSvcClient "github.com/fission/fission/pkg/storagesvc/client"
|
||||
"github.com/fission/fission/pkg/types"
|
||||
)
|
||||
|
||||
func getFunctionsByPackage(client *client.Client, pkgName, pkgNamespace string) ([]fv1.Function, error) {
|
||||
fnList, err := client.FunctionList(pkgNamespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fns := []fv1.Function{}
|
||||
for _, fn := range fnList {
|
||||
if fn.Spec.Package.PackageRef.Name == pkgName {
|
||||
fns = append(fns, fn)
|
||||
}
|
||||
}
|
||||
return fns, nil
|
||||
}
|
||||
|
||||
// downloadStoragesvcURL downloads and return archive content with given storage service url
|
||||
func downloadStoragesvcURL(client *client.Client, fileUrl string) io.ReadCloser {
|
||||
u, err := url.ParseRequestURI(fileUrl)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// replace in-cluster storage service host with controller server url
|
||||
fileDownloadUrl := strings.TrimSuffix(client.Url, "/") + "/proxy/storage/" + u.RequestURI()
|
||||
reader, err := downloadURL(fileDownloadUrl)
|
||||
|
||||
util.CheckErr(err, fmt.Sprintf("download from storage service url: %v", fileUrl))
|
||||
return reader
|
||||
}
|
||||
|
||||
func pkgCreate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
pkgNamespace := c.String("pkgNamespace")
|
||||
envName := c.String("env")
|
||||
if len(envName) == 0 {
|
||||
log.Fatal("Need --env argument.")
|
||||
}
|
||||
envNamespace := c.String("envNamespace")
|
||||
srcArchiveFiles := c.StringSlice("src")
|
||||
deployArchiveFiles := c.StringSlice("deploy")
|
||||
buildcmd := c.String("buildcmd")
|
||||
|
||||
if len(srcArchiveFiles) == 0 && len(deployArchiveFiles) == 0 {
|
||||
log.Fatal("Need --src to specify source archive, or use --deploy to specify deployment archive.")
|
||||
}
|
||||
|
||||
createPackage(client, pkgNamespace, envName, envNamespace, srcArchiveFiles, deployArchiveFiles, buildcmd, "", "", false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pkgUpdate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
pkgName := c.String("name")
|
||||
if len(pkgName) == 0 {
|
||||
log.Fatal("Need --name argument.")
|
||||
}
|
||||
pkgNamespace := c.String("pkgNamespace")
|
||||
|
||||
force := c.Bool("f")
|
||||
envName := c.String("env")
|
||||
envNamespace := c.String("envNamespace")
|
||||
srcArchiveFiles := c.StringSlice("src")
|
||||
deployArchiveFiles := c.StringSlice("deploy")
|
||||
buildcmd := c.String("buildcmd")
|
||||
|
||||
if len(srcArchiveFiles) > 0 && len(deployArchiveFiles) > 0 {
|
||||
log.Fatal("Need either of --src or --deploy and not both arguments.")
|
||||
}
|
||||
|
||||
if len(srcArchiveFiles) == 0 && len(deployArchiveFiles) == 0 &&
|
||||
len(envName) == 0 && len(buildcmd) == 0 {
|
||||
log.Fatal("Need --env or --src or --deploy or --buildcmd argument.")
|
||||
}
|
||||
|
||||
pkg, err := client.PackageGet(&metav1.ObjectMeta{
|
||||
Namespace: pkgNamespace,
|
||||
Name: pkgName,
|
||||
})
|
||||
util.CheckErr(err, "get package")
|
||||
|
||||
// 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 == pkg.Spec.Environment.Name {
|
||||
envName = ""
|
||||
}
|
||||
|
||||
if envNamespace == pkg.Spec.Environment.Namespace {
|
||||
envNamespace = ""
|
||||
}
|
||||
|
||||
fnList, err := 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")
|
||||
}
|
||||
|
||||
newPkgMeta, err := updatePackage(client, pkg,
|
||||
envName, envNamespace, srcArchiveFiles, deployArchiveFiles, buildcmd, false, false)
|
||||
if err != nil {
|
||||
util.CheckErr(err, "update package")
|
||||
}
|
||||
|
||||
// update resource version of package reference of functions that shared the same package
|
||||
for _, fn := range fnList {
|
||||
fn.Spec.Package.PackageRef.ResourceVersion = newPkgMeta.ResourceVersion
|
||||
_, err := client.FunctionUpdate(&fn)
|
||||
util.CheckErr(err, "update function")
|
||||
}
|
||||
|
||||
fmt.Printf("Package '%v' updated\n", newPkgMeta.GetName())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updatePackage(client *client.Client, pkg *fv1.Package, envName, envNamespace string,
|
||||
srcArchiveFiles []string, deployArchiveFiles []string, buildcmd string, forceRebuild bool, noZip bool) (*metav1.ObjectMeta, error) {
|
||||
|
||||
var srcArchiveMetadata, deployArchiveMetadata *fv1.Archive
|
||||
needToBuild := false
|
||||
|
||||
if len(envName) > 0 {
|
||||
pkg.Spec.Environment.Name = envName
|
||||
needToBuild = true
|
||||
}
|
||||
|
||||
if len(envNamespace) > 0 {
|
||||
pkg.Spec.Environment.Namespace = envNamespace
|
||||
needToBuild = true
|
||||
}
|
||||
|
||||
if len(buildcmd) > 0 {
|
||||
pkg.Spec.BuildCommand = buildcmd
|
||||
needToBuild = true
|
||||
}
|
||||
|
||||
if len(srcArchiveFiles) > 0 {
|
||||
srcArchiveMetadata = createArchive(client, srcArchiveFiles, false, "", "")
|
||||
pkg.Spec.Source = *srcArchiveMetadata
|
||||
needToBuild = true
|
||||
}
|
||||
|
||||
if len(deployArchiveFiles) > 0 {
|
||||
deployArchiveMetadata = createArchive(client, deployArchiveFiles, noZip, "", "")
|
||||
pkg.Spec.Deployment = *deployArchiveMetadata
|
||||
// Users may update the env, envNS and deploy archive at the same time,
|
||||
// but without the source archive. In this case, we should set needToBuild to false
|
||||
needToBuild = false
|
||||
}
|
||||
|
||||
// Set package as pending status when needToBuild is true
|
||||
if needToBuild || forceRebuild {
|
||||
// change into pending state to trigger package build
|
||||
pkg.Status = fv1.PackageStatus{
|
||||
BuildStatus: fv1.BuildStatusPending,
|
||||
}
|
||||
}
|
||||
|
||||
newPkgMeta, err := client.PackageUpdate(pkg)
|
||||
util.CheckErr(err, "update package")
|
||||
|
||||
return newPkgMeta, err
|
||||
}
|
||||
|
||||
func pkgSourceGet(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
pkgName := c.String("name")
|
||||
if len(pkgName) == 0 {
|
||||
log.Fatal("Need name of package, use --name")
|
||||
}
|
||||
pkgNamespace := c.String("pkgNamespace")
|
||||
|
||||
output := c.String("output")
|
||||
|
||||
pkg, err := client.PackageGet(&metav1.ObjectMeta{
|
||||
Namespace: pkgNamespace,
|
||||
Name: pkgName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
|
||||
if pkg.Spec.Source.Type == fv1.ArchiveTypeLiteral {
|
||||
reader = bytes.NewReader(pkg.Spec.Source.Literal)
|
||||
} else if pkg.Spec.Source.Type == fv1.ArchiveTypeUrl {
|
||||
readCloser := downloadStoragesvcURL(client, pkg.Spec.Source.URL)
|
||||
defer readCloser.Close()
|
||||
reader = readCloser
|
||||
}
|
||||
|
||||
if len(output) > 0 {
|
||||
return writeArchiveToFile(output, reader)
|
||||
} else {
|
||||
_, err := io.Copy(os.Stdout, reader)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func pkgDeployGet(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
pkgName := c.String("name")
|
||||
if len(pkgName) == 0 {
|
||||
log.Fatal("Need name of package, use --name")
|
||||
}
|
||||
pkgNamespace := c.String("pkgNamespace")
|
||||
|
||||
output := c.String("output")
|
||||
|
||||
pkg, err := client.PackageGet(&metav1.ObjectMeta{
|
||||
Namespace: pkgNamespace,
|
||||
Name: pkgName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
|
||||
if pkg.Spec.Deployment.Type == fv1.ArchiveTypeLiteral {
|
||||
reader = bytes.NewReader(pkg.Spec.Deployment.Literal)
|
||||
} else if pkg.Spec.Deployment.Type == fv1.ArchiveTypeUrl {
|
||||
readCloser := downloadStoragesvcURL(client, pkg.Spec.Deployment.URL)
|
||||
defer readCloser.Close()
|
||||
reader = readCloser
|
||||
}
|
||||
|
||||
if len(output) > 0 {
|
||||
return writeArchiveToFile(output, reader)
|
||||
} else {
|
||||
_, err := io.Copy(os.Stdout, reader)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func pkgInfo(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
pkgName := c.String("name")
|
||||
if len(pkgName) == 0 {
|
||||
log.Fatal("Need name of package, use --name")
|
||||
}
|
||||
pkgNamespace := c.String("pkgNamespace")
|
||||
|
||||
pkg, err := client.PackageGet(&metav1.ObjectMeta{
|
||||
Namespace: pkgNamespace,
|
||||
Name: pkgName,
|
||||
})
|
||||
if err != nil {
|
||||
util.CheckErr(err, fmt.Sprintf("find package %s", pkgName))
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
fmt.Fprintf(w, "%v\t%v\n", "Name:", pkg.Metadata.Name)
|
||||
fmt.Fprintf(w, "%v\t%v\n", "Environment:", pkg.Spec.Environment.Name)
|
||||
fmt.Fprintf(w, "%v\t%v\n", "Status:", pkg.Status.BuildStatus)
|
||||
fmt.Fprintf(w, "%v\n%v", "Build Logs:", pkg.Status.BuildLog)
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pkgList(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
// option for the user to list all orphan packages (not referenced by any function)
|
||||
listOrphans := c.Bool("orphan")
|
||||
pkgNamespace := c.String("pkgNamespace")
|
||||
|
||||
pkgList, err := client.PackageList(pkgNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", "NAME", "BUILD_STATUS", "ENV")
|
||||
if listOrphans {
|
||||
for _, pkg := range pkgList {
|
||||
fnList, err := getFunctionsByPackage(client, pkg.Metadata.Name, pkg.Metadata.Namespace)
|
||||
util.CheckErr(err, fmt.Sprintf("get functions sharing package %s", pkg.Metadata.Name))
|
||||
if len(fnList) == 0 {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", pkg.Metadata.Name, pkg.Status.BuildStatus, pkg.Spec.Environment.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, pkg := range pkgList {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", pkg.Metadata.Name,
|
||||
pkg.Status.BuildStatus, pkg.Spec.Environment.Name)
|
||||
}
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteOrphanPkgs(client *client.Client, pkgNamespace string) error {
|
||||
pkgList, err := client.PackageList(pkgNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// range through all packages and find out the ones not referenced by any function
|
||||
for _, pkg := range pkgList {
|
||||
fnList, err := getFunctionsByPackage(client, pkg.Metadata.Name, pkgNamespace)
|
||||
util.CheckErr(err, fmt.Sprintf("get functions sharing package %s", pkg.Metadata.Name))
|
||||
if len(fnList) == 0 {
|
||||
err = deletePackage(client, pkg.Metadata.Name, pkgNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deletePackage(client *client.Client, pkgName string, pkgNamespace string) error {
|
||||
return client.PackageDelete(&metav1.ObjectMeta{
|
||||
Namespace: pkgNamespace,
|
||||
Name: pkgName,
|
||||
})
|
||||
}
|
||||
|
||||
func pkgDelete(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
pkgName := c.String("name")
|
||||
pkgNamespace := c.String("pkgNamespace")
|
||||
deleteOrphans := c.Bool("orphan")
|
||||
|
||||
if len(pkgName) == 0 && !deleteOrphans {
|
||||
fmt.Println("Need --name argument or --orphan flag.")
|
||||
return nil
|
||||
}
|
||||
if len(pkgName) != 0 && deleteOrphans {
|
||||
fmt.Println("Need either --name argument or --orphan flag")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(pkgName) != 0 {
|
||||
force := c.Bool("f")
|
||||
|
||||
_, err := client.PackageGet(&metav1.ObjectMeta{
|
||||
Namespace: pkgNamespace,
|
||||
Name: pkgName,
|
||||
})
|
||||
util.CheckErr(err, "find package")
|
||||
|
||||
fnList, err := getFunctionsByPackage(client, pkgName, pkgNamespace)
|
||||
|
||||
if !force && len(fnList) > 0 {
|
||||
log.Fatal("Package is used by at least one function, use -f to force delete")
|
||||
}
|
||||
|
||||
err = deletePackage(client, pkgName, pkgNamespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Package '%v' deleted\n", pkgName)
|
||||
} else {
|
||||
err := deleteOrphanPkgs(client, pkgNamespace)
|
||||
util.CheckErr(err, "error deleting orphan packages")
|
||||
fmt.Println("Orphan packages deleted")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pkgRebuild(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
pkgName := c.String("name")
|
||||
if len(pkgName) == 0 {
|
||||
log.Fatal("Need name of package, use --name")
|
||||
}
|
||||
pkgNamespace := c.String("pkgNamespace")
|
||||
|
||||
pkg, err := client.PackageGet(&metav1.ObjectMeta{
|
||||
Name: pkgName,
|
||||
Namespace: pkgNamespace,
|
||||
})
|
||||
util.CheckErr(err, "find package")
|
||||
|
||||
if pkg.Status.BuildStatus != fv1.BuildStatusFailed {
|
||||
log.Fatal(fmt.Sprintf("Package %v is not in %v state.",
|
||||
pkg.Metadata.Name, fv1.BuildStatusFailed))
|
||||
}
|
||||
|
||||
_, err = updatePackage(client, pkg, "", "", nil, nil, "", true, false)
|
||||
util.CheckErr(err, "update package")
|
||||
|
||||
fmt.Printf("Retrying build for pkg %v. Use \"fission pkg info --name %v\" to view status.\n", pkg.Metadata.Name, pkg.Metadata.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileSize(filePath string) int64 {
|
||||
info, err := os.Stat(filePath)
|
||||
util.CheckErr(err, fmt.Sprintf("stat %v", filePath))
|
||||
return info.Size()
|
||||
}
|
||||
|
||||
func fileChecksum(fileName string) (*fv1.Checksum, error) {
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file %v: %v", fileName, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
_, err = io.Copy(h, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate checksum for %v", fileName)
|
||||
}
|
||||
|
||||
return &fv1.Checksum{
|
||||
Type: fv1.ChecksumTypeSHA256,
|
||||
Sum: hex.EncodeToString(h.Sum(nil)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Return a fv1.Archive made from an archive . If specFile, then
|
||||
// create an archive upload spec in the specs directory; otherwise
|
||||
// upload the archive using client. noZip avoids zipping the
|
||||
// includeFiles, but is ignored if there's more than one includeFile.
|
||||
func createArchive(client *client.Client, includeFiles []string, noZip bool, specDir string, specFile string) *fv1.Archive {
|
||||
|
||||
var errs *multierror.Error
|
||||
|
||||
// check files existence
|
||||
for _, path := range includeFiles {
|
||||
// ignore http files
|
||||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get files from inputs as number of files decide next steps
|
||||
files, err := utils.FindAllGlobs([]string{path})
|
||||
if err != nil {
|
||||
util.CheckErr(err, "finding all globs")
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
errs = multierror.Append(errs, errors.New(fmt.Sprintf("Error finding any files with path \"%v\"", path)))
|
||||
}
|
||||
}
|
||||
|
||||
if errs.ErrorOrNil() != nil {
|
||||
log.Fatal(errs.Error())
|
||||
}
|
||||
|
||||
if len(specFile) > 0 {
|
||||
// create an ArchiveUploadSpec and reference it from the archive
|
||||
aus := &ArchiveUploadSpec{
|
||||
Name: archiveName("", includeFiles),
|
||||
IncludeGlobs: includeFiles,
|
||||
}
|
||||
|
||||
// check if this AUS exists in the specs; if so, don't create a new one
|
||||
fr, err := readSpecs(specDir)
|
||||
util.CheckErr(err, "read specs")
|
||||
if m := fr.specExists(aus, false, true); m != nil {
|
||||
fmt.Printf("Re-using previously created archive %v\n", m.Name)
|
||||
aus.Name = m.Name
|
||||
} else {
|
||||
// save the uploadspec
|
||||
err := specSave(*aus, specFile)
|
||||
util.CheckErr(err, fmt.Sprintf("write spec file %v", specFile))
|
||||
}
|
||||
|
||||
// create the archive object
|
||||
ar := &fv1.Archive{
|
||||
Type: fv1.ArchiveTypeUrl,
|
||||
URL: fmt.Sprintf("%v%v", ARCHIVE_URL_PREFIX, aus.Name),
|
||||
}
|
||||
return ar
|
||||
}
|
||||
|
||||
archivePath := makeArchiveFileIfNeeded("", includeFiles, noZip)
|
||||
|
||||
ctx := context.Background()
|
||||
return uploadArchive(ctx, client, archivePath)
|
||||
}
|
||||
|
||||
func uploadArchive(ctx context.Context, client *client.Client, fileName string) *fv1.Archive {
|
||||
var archive fv1.Archive
|
||||
|
||||
// If filename is a URL, download it first
|
||||
if strings.HasPrefix(fileName, "http://") || strings.HasPrefix(fileName, "https://") {
|
||||
fileName = downloadToTempFile(fileName)
|
||||
}
|
||||
|
||||
if fileSize(fileName) < types.ArchiveLiteralSizeLimit {
|
||||
archive.Type = fv1.ArchiveTypeLiteral
|
||||
archive.Literal = getContents(fileName)
|
||||
} else {
|
||||
u := strings.TrimSuffix(client.Url, "/") + "/proxy/storage"
|
||||
ssClient := storageSvcClient.MakeClient(u)
|
||||
|
||||
// TODO add a progress bar
|
||||
id, err := ssClient.Upload(ctx, fileName, nil)
|
||||
util.CheckErr(err, fmt.Sprintf("upload file %v", fileName))
|
||||
|
||||
storageSvc, err := client.GetSvcURL("application=fission-storage")
|
||||
storageSvcURL := "http://" + storageSvc
|
||||
util.CheckErr(err, "get fission storage service name")
|
||||
|
||||
// We make a new client with actual URL of Storage service so that the URL is not
|
||||
// pointing to 127.0.0.1 i.e. proxy. DON'T reuse previous ssClient
|
||||
pkgClient := storageSvcClient.MakeClient(storageSvcURL)
|
||||
archiveURL := pkgClient.GetUrl(id)
|
||||
|
||||
archive.Type = fv1.ArchiveTypeUrl
|
||||
archive.URL = archiveURL
|
||||
|
||||
csum, err := fileChecksum(fileName)
|
||||
util.CheckErr(err, fmt.Sprintf("calculate checksum for file %v", fileName))
|
||||
|
||||
archive.Checksum = *csum
|
||||
}
|
||||
return &archive
|
||||
}
|
||||
|
||||
func createPackage(client *client.Client, pkgNamespace string, envName string, envNamespace string, srcArchiveFiles []string, deployArchiveFiles []string, buildcmd string, specDir string, specFile string, noZip bool) *metav1.ObjectMeta {
|
||||
pkgSpec := fv1.PackageSpec{
|
||||
Environment: fv1.EnvironmentReference{
|
||||
Namespace: envNamespace,
|
||||
Name: envName,
|
||||
},
|
||||
}
|
||||
var pkgStatus fv1.BuildStatus = fv1.BuildStatusSucceeded
|
||||
|
||||
var pkgName string
|
||||
if len(deployArchiveFiles) > 0 {
|
||||
if len(specFile) > 0 { // we should do this in all cases, i think
|
||||
pkgStatus = fv1.BuildStatusNone
|
||||
}
|
||||
pkgSpec.Deployment = *createArchive(client, deployArchiveFiles, noZip, specDir, specFile)
|
||||
pkgName = util.KubifyName(fmt.Sprintf("%v-%v", path.Base(deployArchiveFiles[0]), uniuri.NewLen(4)))
|
||||
}
|
||||
if len(srcArchiveFiles) > 0 {
|
||||
pkgSpec.Source = *createArchive(client, srcArchiveFiles, false, specDir, specFile)
|
||||
pkgStatus = fv1.BuildStatusPending // set package build status to pending
|
||||
pkgName = util.KubifyName(fmt.Sprintf("%v-%v", path.Base(srcArchiveFiles[0]), uniuri.NewLen(4)))
|
||||
}
|
||||
|
||||
if len(buildcmd) > 0 {
|
||||
pkgSpec.BuildCommand = buildcmd
|
||||
}
|
||||
|
||||
if len(pkgName) == 0 {
|
||||
pkgName = strings.ToLower(uuid.NewV4().String())
|
||||
}
|
||||
pkg := &fv1.Package{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: pkgName,
|
||||
Namespace: pkgNamespace,
|
||||
},
|
||||
Spec: pkgSpec,
|
||||
Status: fv1.PackageStatus{
|
||||
BuildStatus: pkgStatus,
|
||||
},
|
||||
}
|
||||
|
||||
if len(specFile) > 0 {
|
||||
// if a package sith the same spec exists, don't create a new spec file
|
||||
fr, err := readSpecs(getSpecDir(nil))
|
||||
util.CheckErr(err, "read specs")
|
||||
if m := fr.specExists(pkg, false, true); m != nil {
|
||||
fmt.Printf("Re-using previously created package %v\n", m.Name)
|
||||
return m
|
||||
}
|
||||
|
||||
err = specSave(*pkg, specFile)
|
||||
util.CheckErr(err, "save package spec")
|
||||
return &pkg.Metadata
|
||||
} else {
|
||||
pkgMetadata, err := client.PackageCreate(pkg)
|
||||
util.CheckErr(err, "create package")
|
||||
fmt.Printf("Package '%v' created\n", pkgMetadata.GetName())
|
||||
return pkgMetadata
|
||||
}
|
||||
}
|
||||
|
||||
func getContents(filePath string) []byte {
|
||||
var code []byte
|
||||
var err error
|
||||
|
||||
code, err = ioutil.ReadFile(filePath)
|
||||
util.CheckErr(err, fmt.Sprintf("read %v", filePath))
|
||||
return code
|
||||
}
|
||||
|
||||
func writeArchiveToFile(fileName string, reader io.Reader) error {
|
||||
tmpDir, err := utils.GetTempDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(tmpDir, fileName+".tmp")
|
||||
w, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Chmod(path, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Rename(path, fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadToTempFile fetches archive file from arbitrary url
|
||||
// and write it to temp file for further usage
|
||||
func downloadToTempFile(fileUrl string) string {
|
||||
reader, err := downloadURL(fileUrl)
|
||||
defer reader.Close()
|
||||
util.CheckErr(err, fmt.Sprintf("download from url: %v", fileUrl))
|
||||
|
||||
tmpDir, err := utils.GetTempDir()
|
||||
util.CheckErr(err, "create temp directory")
|
||||
|
||||
tmpFilename := uuid.NewV4().String()
|
||||
destination := filepath.Join(tmpDir, tmpFilename)
|
||||
err = os.Mkdir(tmpDir, 0744)
|
||||
util.CheckErr(err, "create temp directory")
|
||||
|
||||
err = writeArchiveToFile(destination, reader)
|
||||
util.CheckErr(err, "write archive to file")
|
||||
|
||||
return destination
|
||||
}
|
||||
|
||||
// downloadURL downloads file from given url
|
||||
func downloadURL(fileUrl string) (io.ReadCloser, error) {
|
||||
resp, err := http.Get(fileUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%v - HTTP response returned non 200 status", resp.StatusCode)
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// Create an archive from the given list of input files, unless that
|
||||
// list has only one item and that item is either a zip file or a URL.
|
||||
//
|
||||
// If the inputs have only one file and noZip is true, the file is
|
||||
// returned as-is with no zipping. (This is used for compatibility
|
||||
// with v1 envs.) noZip is IGNORED if there is more than one input
|
||||
// file.
|
||||
func makeArchiveFileIfNeeded(archiveNameHint string, archiveInput []string, noZip bool) string {
|
||||
|
||||
// Unique name for the archive
|
||||
archiveName := archiveName(archiveNameHint, archiveInput)
|
||||
|
||||
// Get files from inputs as number of files decide next steps
|
||||
files, err := utils.FindAllGlobs(archiveInput)
|
||||
if err != nil {
|
||||
util.CheckErr(err, "finding all globs")
|
||||
}
|
||||
|
||||
// We have one file; if it's a zip file or a URL, no need to archive it
|
||||
if len(files) == 1 {
|
||||
// make sure it exists
|
||||
if _, err := os.Stat(files[0]); err != nil {
|
||||
util.CheckErr(err, fmt.Sprintf("open input file %v", files[0]))
|
||||
}
|
||||
|
||||
// if it's an existing zip file OR we're not supposed to zip it, don't do anything
|
||||
if archiver.Zip.Match(files[0]) || noZip {
|
||||
return files[0]
|
||||
}
|
||||
|
||||
// if it's an HTTP URL, just use the URL.
|
||||
if strings.HasPrefix(files[0], "http://") || strings.HasPrefix(files[0], "https://") {
|
||||
return files[0]
|
||||
}
|
||||
}
|
||||
|
||||
// For anything else, create a new archive
|
||||
tmpDir, err := utils.GetTempDir()
|
||||
if err != nil {
|
||||
util.CheckErr(err, "create temporary archive directory")
|
||||
}
|
||||
|
||||
archivePath, err := utils.MakeArchive(filepath.Join(tmpDir, archiveName), archiveInput...)
|
||||
if err != nil {
|
||||
util.CheckErr(err, "create archive file")
|
||||
}
|
||||
|
||||
return archivePath
|
||||
}
|
||||
|
||||
// Name an archive
|
||||
func archiveName(givenNameHint string, includedFiles []string) string {
|
||||
if len(givenNameHint) > 0 {
|
||||
return fmt.Sprintf("%v-%v", givenNameHint, uniuri.NewLen(4))
|
||||
}
|
||||
if len(includedFiles) == 0 {
|
||||
return uniuri.NewLen(8)
|
||||
}
|
||||
return fmt.Sprintf("%v-%v", util.KubifyName(includedFiles[0]), uniuri.NewLen(4))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/fission/fission/pkg/fission-cli/plugin"
|
||||
)
|
||||
|
||||
var cmdPlugin = cli.Command{
|
||||
Name: "plugin",
|
||||
Aliases: []string{"plugins"},
|
||||
Usage: "Manage Fission CLI plugins",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "List installed client plugins",
|
||||
Action: pluginList,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func pluginList(_ *cli.Context) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
fmt.Fprintln(w, "NAME\tVERSION\tPATH")
|
||||
for _, p := range plugin.FindAll() {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", p.Name, p.Version, p.Path)
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
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 plugins provides support for creating extensible CLIs
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
cmdTimeout = 5 * time.Second
|
||||
cmdMetadataArgs = "--plugin"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPluginNotFound = errors.New("plugin not found")
|
||||
ErrPluginInvalid = errors.New("invalid plugin")
|
||||
|
||||
Prefix = "fission-"
|
||||
)
|
||||
|
||||
// Metadata contains the metadata of a plugin.
|
||||
// The only metadata that is guaranteed to be non-empty is the path and Name. All other fields are considered optional.
|
||||
type Metadata struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
Usage string `json:"usage,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
func (md *Metadata) AddAlias(alias string) {
|
||||
if alias != md.Name && !md.HasAlias(alias) {
|
||||
md.Aliases = append(md.Aliases, alias)
|
||||
}
|
||||
}
|
||||
|
||||
func (md *Metadata) HasAlias(needle string) bool {
|
||||
for _, alias := range md.Aliases {
|
||||
if alias == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Find searches the machine for the given plugin, returning the metadata of the plugin.
|
||||
// The only metadata that is guaranteed to be non-empty is the path and Name. All other fields are considered optional.
|
||||
// If found it returns the plugin, otherwise it returns ErrPluginNotFound if the plugin was not found.
|
||||
func Find(pluginName string) (*Metadata, error) {
|
||||
// Search PATH for plugin as command-name
|
||||
// To check if plugin is actually there still.
|
||||
pluginPath, err := findPluginOnPath(pluginName)
|
||||
if err != nil {
|
||||
// Fallback: Search for alias in each command
|
||||
mds := FindAll()
|
||||
for _, md := range mds {
|
||||
if md.HasAlias(pluginName) {
|
||||
return md, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrPluginNotFound
|
||||
}
|
||||
|
||||
md, err := fetchPluginMetadata(pluginPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
|
||||
// Exec executes the plugin using the provided args.
|
||||
// All input and output is redirected to stdin, stdout, and stderr.
|
||||
func Exec(md *Metadata, args []string) error {
|
||||
cmd := exec.Command(md.Path, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// FindAll searches the machine for all plugins currently present.
|
||||
func FindAll() map[string]*Metadata {
|
||||
plugins := map[string]*Metadata{}
|
||||
|
||||
dirs := strings.Split(os.Getenv("PATH"), ":")
|
||||
for _, dir := range dirs {
|
||||
fs, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range fs {
|
||||
if !strings.HasPrefix(f.Name(), Prefix) {
|
||||
continue
|
||||
}
|
||||
fp := path.Join(dir, f.Name())
|
||||
md, err := fetchPluginMetadata(fp)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if existing, ok := plugins[md.Name]; ok {
|
||||
for _, alias := range existing.Aliases {
|
||||
md.AddAlias(alias)
|
||||
}
|
||||
}
|
||||
plugins[md.Name] = md
|
||||
}
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
func findPluginOnPath(pluginName string) (path string, err error) {
|
||||
binaryName := Prefix + pluginName
|
||||
path, err = exec.LookPath(binaryName)
|
||||
|
||||
if err != nil || len(path) == 0 {
|
||||
return "", ErrPluginNotFound
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// fetchPluginMetadata attempts to fetch the plugin metadata given the plugin path.
|
||||
func fetchPluginMetadata(pluginPath string) (*Metadata, error) {
|
||||
d, err := os.Stat(pluginPath)
|
||||
if err != nil {
|
||||
return nil, ErrPluginNotFound
|
||||
}
|
||||
if m := d.Mode(); m.IsDir() || m&0111 == 0 {
|
||||
return nil, ErrPluginInvalid
|
||||
}
|
||||
|
||||
// Fetch the metadata from the plugin itself.
|
||||
buf := bytes.NewBuffer(nil)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cmdTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, pluginPath, cmdMetadataArgs) // Note: issue can occur with signal propagation
|
||||
cmd.Stdout = buf
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse metadata if possible
|
||||
pluginName := strings.TrimPrefix(path.Base(pluginPath), Prefix)
|
||||
md := &Metadata{}
|
||||
err = json.Unmarshal(buf.Bytes(), md)
|
||||
|
||||
// If metadata could not be retrieved, or if no name was provided, use the filename of the binary
|
||||
if err != nil || len(md.Name) == 0 {
|
||||
md.Name = pluginName
|
||||
}
|
||||
md.Path = pluginPath
|
||||
md.AddAlias(pluginName)
|
||||
return md, nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
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 plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
os.Clearenv()
|
||||
testDir := path.Join(os.TempDir(), fmt.Sprintf("fission-test-plugins-%v", time.Now().UnixNano()))
|
||||
err := os.MkdirAll(testDir, os.ModePerm)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
defer os.RemoveAll(testDir)
|
||||
testBinary := path.Join(testDir, "foo")
|
||||
md := &Metadata{
|
||||
Name: "foo",
|
||||
Version: "1.0.1",
|
||||
Usage: "Usage help",
|
||||
Aliases: []string{"bar"},
|
||||
}
|
||||
jsonMd, err := json.Marshal(md)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
err = ioutil.WriteFile(testBinary, []byte(fmt.Sprintf("#!/bin/sh\necho '%v'", string(jsonMd))), os.ModePerm)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
err = os.Setenv("PATH", testDir)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
Prefix = ""
|
||||
|
||||
found, err := Find(md.Name)
|
||||
os.RemoveAll(testDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, found)
|
||||
assert.Equal(t, md.Name, found.Name)
|
||||
assert.Equal(t, path.Join(testDir, md.Name), found.Path)
|
||||
assert.Equal(t, md.Aliases, found.Aliases)
|
||||
assert.Equal(t, md.Usage, found.Usage)
|
||||
assert.Equal(t, md.Version, found.Version)
|
||||
}
|
||||
|
||||
func TestExec(t *testing.T) {
|
||||
os.Clearenv()
|
||||
testDir := path.Join(os.TempDir(), fmt.Sprintf("fission-test-plugins-%v", time.Now().UnixNano()))
|
||||
err := os.MkdirAll(testDir, os.ModePerm)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
defer os.RemoveAll(testDir)
|
||||
testBinary := path.Join(testDir, "foo")
|
||||
md := &Metadata{
|
||||
Name: "foo",
|
||||
Version: "1.0.1",
|
||||
Usage: "Usage help",
|
||||
Aliases: []string{"bar"},
|
||||
Path: path.Join(testBinary),
|
||||
}
|
||||
jsonMd, err := json.Marshal(md)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
err = ioutil.WriteFile(testBinary, []byte(fmt.Sprintf("#!/bin/sh\necho '%v'", string(jsonMd))), os.ModePerm)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
err = os.Setenv("PATH", testDir)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
Prefix = ""
|
||||
err = Exec(md, nil)
|
||||
os.RemoveAll(testDir)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 plugin
|
||||
|
||||
// builtinRegistry consists of a map of plugin names along with the relevant url.
|
||||
var builtinRegistry = map[string]string{
|
||||
"workflows": "https://github.com/fission/fission-workflows/releases",
|
||||
}
|
||||
|
||||
// SearchRegistries will search (remote) registries for the presence of the command.
|
||||
// For now we only use the builtinRegistry
|
||||
func SearchRegistries(cmd string) (string, bool) {
|
||||
url, ok := builtinRegistry[cmd]
|
||||
return url, ok
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
Copyright 2018 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
|
||||
|
||||
tttp://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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/urfave/cli"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
)
|
||||
|
||||
func recorderCreate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
recName := c.String("name")
|
||||
if len(recName) == 0 {
|
||||
recName = uuid.NewV4().String()
|
||||
}
|
||||
fnName := c.String("function")
|
||||
triggersOriginal := c.StringSlice("trigger")
|
||||
|
||||
// Function XOR triggers can be given
|
||||
if len(fnName) == 0 && len(triggersOriginal) == 0 {
|
||||
log.Fatal("Need to specify at least one function or one trigger, use --function, --trigger")
|
||||
}
|
||||
if len(fnName) != 0 && len(triggersOriginal) != 0 {
|
||||
log.Fatal("Can specify either one function or one or more triggers, but not both")
|
||||
}
|
||||
|
||||
// TODO: Validate here or elsewhere that all triggers belong to the same namespace
|
||||
|
||||
var triggers []string
|
||||
if len(triggersOriginal) != 0 {
|
||||
ts := strings.Split(triggersOriginal[0], ",")
|
||||
for _, name := range ts {
|
||||
triggers = append(triggers, name)
|
||||
}
|
||||
}
|
||||
// TODO: Define appropriate set of policies and defaults
|
||||
//retPolicy := c.String("retention")
|
||||
//evictPolicy := c.String("eviction")
|
||||
|
||||
recorder := &fv1.Recorder{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: recName,
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: fv1.RecorderSpec{
|
||||
Name: recName,
|
||||
Function: fnName,
|
||||
Triggers: triggers,
|
||||
RetentionPolicy: "Permanent", // TODO: Implement customizable policies for expiration of records
|
||||
EvictionPolicy: "None",
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
// If we're writing a spec, don't call the API
|
||||
if c.Bool("spec") {
|
||||
specFile := fmt.Sprintf("recorder-%v.yaml", recName)
|
||||
err := specSave(*recorder, specFile)
|
||||
util.CheckErr(err, "create recorder spec")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := client.RecorderCreate(recorder)
|
||||
util.CheckErr(err, "create recorder")
|
||||
|
||||
fmt.Printf("recorder '%s' created\n", recName)
|
||||
return err
|
||||
}
|
||||
|
||||
func recorderGet(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
recName := c.String("name")
|
||||
|
||||
recorder, err := client.RecorderGet(&metav1.ObjectMeta{
|
||||
Name: recName,
|
||||
Namespace: "default",
|
||||
})
|
||||
|
||||
util.CheckErr(err, "get recorder")
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
"NAME", "ENABLED", "FUNCTION", "TRIGGERS", "RETENTION_POLICY", "EVICTION_POLICY")
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
recorder.Metadata.Name, recorder.Spec.Enabled, recorder.Spec.Function, recorder.Spec.Triggers, recorder.Spec.RetentionPolicy, recorder.Spec.EvictionPolicy)
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recorderUpdate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
recName := c.String("name")
|
||||
enable := c.Bool("enable")
|
||||
disable := c.Bool("disable")
|
||||
//retPolicy := c.String("retention")
|
||||
//evictPolicy := c.String("eviction")
|
||||
triggers := c.StringSlice("trigger")
|
||||
function := c.String("function")
|
||||
|
||||
if enable && disable {
|
||||
log.Fatal("Cannot enable and disable a recorder simultaneously.")
|
||||
}
|
||||
|
||||
// Prevent enable or disable while trying to update other fields. These flags must be standalone.
|
||||
if enable || disable {
|
||||
if len(triggers) > 0 || len(function) > 0 {
|
||||
log.Fatal("Enabling or disabling a recorder with other (non-name) flags set is not supported.")
|
||||
}
|
||||
} else if len(triggers) == 0 && len(function) == 0 {
|
||||
log.Fatal("Need to specify either a function or trigger(s) for this recorder")
|
||||
}
|
||||
|
||||
if len(recName) == 0 {
|
||||
log.Fatal("Need name of recorder, use --name")
|
||||
}
|
||||
|
||||
recorder, err := client.RecorderGet(&metav1.ObjectMeta{
|
||||
Name: recName,
|
||||
Namespace: "default",
|
||||
})
|
||||
|
||||
updated := false
|
||||
|
||||
// TODO: Additional validation on type of supported retention policy, eviction policy
|
||||
|
||||
//if len(retPolicy) > 0 {
|
||||
// recorder.Spec.RetentionPolicy = retPolicy
|
||||
// updated = true
|
||||
//}
|
||||
//if len(evictPolicy) > 0 {
|
||||
// recorder.Spec.EvictionPolicy = evictPolicy
|
||||
// updated = true
|
||||
//}
|
||||
if enable {
|
||||
recorder.Spec.Enabled = true
|
||||
updated = true
|
||||
}
|
||||
|
||||
if disable {
|
||||
recorder.Spec.Enabled = false
|
||||
updated = true
|
||||
}
|
||||
|
||||
if len(triggers) > 0 {
|
||||
var newTriggers []string
|
||||
triggs := strings.Split(triggers[0], ",")
|
||||
for _, name := range triggs {
|
||||
newTriggers = append(newTriggers, name)
|
||||
}
|
||||
recorder.Spec.Triggers = newTriggers
|
||||
updated = true
|
||||
}
|
||||
|
||||
if len(function) > 0 {
|
||||
recorder.Spec.Function = function
|
||||
updated = true
|
||||
}
|
||||
|
||||
if !updated {
|
||||
log.Fatal("Nothing to update. Use --function, --triggers, --enable or --disable")
|
||||
}
|
||||
|
||||
_, err = client.RecorderUpdate(recorder)
|
||||
util.CheckErr(err, "update recorder")
|
||||
|
||||
fmt.Printf("recorder '%v' updated\n", recName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func recorderDelete(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
recName := c.String("name")
|
||||
|
||||
if len(recName) == 0 {
|
||||
log.Fatal("Need name of recorder to delete, use --name")
|
||||
}
|
||||
|
||||
recNs := c.String("recorderns")
|
||||
|
||||
err := client.RecorderDelete(&metav1.ObjectMeta{
|
||||
Name: recName,
|
||||
Namespace: recNs,
|
||||
})
|
||||
|
||||
util.CheckErr(err, "delete recorder")
|
||||
|
||||
fmt.Printf("recorder '%v' deleted\n", recName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func recorderList(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
recorders, err := client.RecorderList("default")
|
||||
util.CheckErr(err, "list recorders")
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
"NAME", "ENABLED", "FUNCTIONS", "TRIGGERS", "RETENTION_POLICY", "EVICTION_POLICY")
|
||||
for _, r := range recorders {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n",
|
||||
r.Metadata.Name, r.Spec.Enabled, r.Spec.Function, r.Spec.Triggers, r.Spec.RetentionPolicy, r.Spec.EvictionPolicy)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
Copyright 2018 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
|
||||
|
||||
tttp://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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
"github.com/fission/fission/pkg/redis/build/gen"
|
||||
)
|
||||
|
||||
func recordsView(c *cli.Context) error {
|
||||
var verbosity int
|
||||
if c.Bool("v") && c.Bool("vv") {
|
||||
log.Fatal("conflicting verbosity levels, use either --v or --vv")
|
||||
}
|
||||
if c.Bool("v") {
|
||||
verbosity = 1
|
||||
}
|
||||
if c.Bool("vv") {
|
||||
verbosity = 2
|
||||
}
|
||||
|
||||
function := c.String("function")
|
||||
trigger := c.String("trigger")
|
||||
from := c.String("from")
|
||||
to := c.String("to")
|
||||
|
||||
//Refuse multiple filters for now
|
||||
if multipleFiltersSpecified(function, trigger, from+to) {
|
||||
log.Fatal("maximum of one filter is currently supported, either --function, --trigger, or --from,--to")
|
||||
}
|
||||
|
||||
if len(function) != 0 {
|
||||
return recordsByFunction(function, verbosity, c)
|
||||
}
|
||||
if len(trigger) != 0 {
|
||||
return recordsByTrigger(trigger, verbosity, c)
|
||||
}
|
||||
if len(from) != 0 && len(to) != 0 {
|
||||
return recordsByTime(from, to, verbosity, c)
|
||||
}
|
||||
err := recordsAll(verbosity, c)
|
||||
util.CheckErr(err, "view records")
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordsAll(verbosity int, c *cli.Context) error {
|
||||
fc := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
records, err := fc.RecordsAll()
|
||||
util.CheckErr(err, "view records")
|
||||
|
||||
showRecords(records, verbosity)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordsByTrigger(trigger string, verbosity int, c *cli.Context) error {
|
||||
fc := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
records, err := fc.RecordsByTrigger(trigger)
|
||||
util.CheckErr(err, "view records")
|
||||
|
||||
showRecords(records, verbosity)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: More accurate function name (function filter)
|
||||
func recordsByFunction(function string, verbosity int, c *cli.Context) error {
|
||||
fc := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
records, err := fc.RecordsByFunction(function)
|
||||
util.CheckErr(err, "view records")
|
||||
|
||||
showRecords(records, verbosity)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recordsByTime(from string, to string, verbosity int, c *cli.Context) error {
|
||||
fc := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
records, err := fc.RecordsByTime(from, to)
|
||||
util.CheckErr(err, "view records")
|
||||
|
||||
showRecords(records, verbosity)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func showRecords(records []*redisCache.RecordedEntry, verbosity int) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
|
||||
if verbosity == 1 {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\n",
|
||||
"REQUID", "REQUEST METHOD", "FUNCTION", "RESPONSE STATUS", "TRIGGER")
|
||||
for _, record := range records {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\n",
|
||||
record.ReqUID, record.Req.Method, record.Req.Header["X-Fission-Function-Name"], record.Resp.Status, record.Trigger)
|
||||
}
|
||||
} else if verbosity == 2 {
|
||||
for _, record := range records {
|
||||
fmt.Println(record)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, "%v\n",
|
||||
"REQUID")
|
||||
for _, record := range records {
|
||||
fmt.Fprintf(w, "%v\n",
|
||||
record.ReqUID)
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func multipleFiltersSpecified(entries ...string) bool {
|
||||
var specified int
|
||||
for _, entry := range entries {
|
||||
if len(entry) > 0 {
|
||||
specified += 1
|
||||
}
|
||||
}
|
||||
return specified > 1
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 2018 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func replay(c *cli.Context) error {
|
||||
fc := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
reqUID := c.String("reqUID")
|
||||
if len(reqUID) == 0 {
|
||||
log.Fatal("Need a reqUID, use --reqUID flag to specify")
|
||||
}
|
||||
|
||||
responses, err := fc.ReplayByReqUID(reqUID)
|
||||
util.CheckErr(err, "replay records")
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
|
||||
for _, resp := range responses {
|
||||
fmt.Fprintf(w, "%v",
|
||||
resp,
|
||||
)
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
Copyright 2018 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 support
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/fission/fission/pkg/fission-cli/support/resources"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
"github.com/fission/fission/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
DUMP_ARCHIVE_PREFIX = "fission-dump"
|
||||
DEFAULT_OUTPUT_DIR = "fission-dump"
|
||||
)
|
||||
|
||||
func DumpInfo(c *cli.Context) error {
|
||||
|
||||
fmt.Println("Start dumping process...")
|
||||
|
||||
nozip := c.Bool("nozip")
|
||||
outputDir := c.String("output")
|
||||
|
||||
// check whether the dump directory exists.
|
||||
_, err := os.Stat(outputDir)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err = os.Mkdir(outputDir, 0755)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else if err != nil {
|
||||
panic(errors.Wrap(err, "Error checking dump directory status"))
|
||||
}
|
||||
|
||||
outputDir, err = filepath.Abs(outputDir)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "Error creating dump directory for dumping files"))
|
||||
}
|
||||
|
||||
client := util.GetApiClient(util.GetServerUrl())
|
||||
_, k8sClient := util.GetKubernetesClient()
|
||||
|
||||
ress := map[string]resources.Resource{
|
||||
// kubernetes info
|
||||
"kubernetes-version": resources.NewKubernetesVersion(k8sClient),
|
||||
"kubernetes-nodes": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesNode, ""),
|
||||
|
||||
// fission info
|
||||
"fission-version": resources.NewFissionVersion(client),
|
||||
|
||||
// fission component logs & spec
|
||||
"fission-components-svc-sepc": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesService,
|
||||
"svc in (buildermgr, controller, executor, influxdb, kubewatcher, logger, mqtrigger, nats-streaming, redis, router, storagesvc, timer)"),
|
||||
"fission-components-deployment-sepc": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesDeployment,
|
||||
"svc in (buildermgr, controller, executor, influxdb, kubewatcher, logger, mqtrigger, nats-streaming, redis, router, storagesvc, timer)"),
|
||||
"fission-components-pod-sepc": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesPod,
|
||||
"svc in (buildermgr, controller, executor, influxdb, kubewatcher, logger, mqtrigger, nats-streaming, redis, router, storagesvc, timer)"),
|
||||
"fission-components-pod-log": resources.NewKubernetesPodLogDumper(k8sClient,
|
||||
"svc in (buildermgr, controller, executor, influxdb, kubewatcher, logger, mqtrigger, nats-streaming, redis, router, storagesvc, timer)"),
|
||||
|
||||
// fission builder logs & spec
|
||||
"fission-builder-svc-spec": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesService, "owner=buildermgr"),
|
||||
"fission-builder-deployment-spec": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesDeployment, "owner=buildermgr"),
|
||||
"fission-builder-pod-spec": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesPod, "owner=buildermgr"),
|
||||
"fission-builder-pod-log": resources.NewKubernetesPodLogDumper(k8sClient, "owner=buildermgr"),
|
||||
|
||||
// fission function logs & spec
|
||||
"fission-function-svc-sepc": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesService, "executorType=newdeploy"),
|
||||
"fission-function-deployment-spec": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesDeployment, "executorType in (poolmgr, newdeploy)"),
|
||||
"fission-function-pod-spec": resources.NewKubernetesObjectDumper(k8sClient, resources.KubernetesPod, "executorType in (poolmgr, newdeploy)"),
|
||||
"fission-function-pod-log": resources.NewKubernetesPodLogDumper(k8sClient, "executorType in (poolmgr, newdeploy)"),
|
||||
|
||||
// CRD resources
|
||||
"fission-crd-packages": resources.NewCrdDumper(client, resources.CrdPackage),
|
||||
"fission-crd-environments": resources.NewCrdDumper(client, resources.CrdEnvironment),
|
||||
"fission-crd-functions": resources.NewCrdDumper(client, resources.CrdFunction),
|
||||
"fission-crd-httptriggers": resources.NewCrdDumper(client, resources.CrdHttpTrigger),
|
||||
"fission-crd-kubewatchers": resources.NewCrdDumper(client, resources.CrdKubeWatcher),
|
||||
"fission-crd-mqtriggers": resources.NewCrdDumper(client, resources.CrdMessageQueueTrigger),
|
||||
"fission-crd-timetriggers": resources.NewCrdDumper(client, resources.CrdTimeTrigger),
|
||||
}
|
||||
|
||||
dumpName := fmt.Sprintf("%v_%v", DUMP_ARCHIVE_PREFIX, time.Now().Unix())
|
||||
dumpDir := filepath.Join(outputDir, dumpName)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
for key, res := range ress {
|
||||
dir := fmt.Sprintf("%v/%v/", dumpDir, key)
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
go func(res resources.Resource, dir string) {
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
res.Dump(dir)
|
||||
}(res, dir)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if !nozip {
|
||||
defer os.Remove(dumpDir)
|
||||
path := filepath.Join(outputDir, fmt.Sprintf("%v.zip", dumpName))
|
||||
_, err := utils.MakeArchive(path, dumpDir)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating archive for dump files: %v", err)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("The archive dump file is %v\n", path)
|
||||
} else {
|
||||
fmt.Printf("The dump files are placed at %v\n", dumpDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
Copyright 2018 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 resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
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/log"
|
||||
"github.com/fission/fission/pkg/types"
|
||||
)
|
||||
|
||||
const (
|
||||
CrdEnvironment = "Environment"
|
||||
CrdFunction = "Function"
|
||||
CrdPackage = "Packages"
|
||||
|
||||
CrdHttpTrigger = "HTTPTrigger"
|
||||
CrdKubeWatcher = "KubeWatcher"
|
||||
CrdMessageQueueTrigger = "MessageQueue"
|
||||
CrdTimeTrigger = "TimeTrigger"
|
||||
)
|
||||
|
||||
type CrdDumper struct {
|
||||
client *client.Client
|
||||
crdType string
|
||||
}
|
||||
|
||||
func NewCrdDumper(client *client.Client, crdType string) Resource {
|
||||
return CrdDumper{client: client, crdType: crdType}
|
||||
}
|
||||
|
||||
func (res CrdDumper) Dump(dumpDir string) {
|
||||
|
||||
switch res.crdType {
|
||||
case CrdEnvironment:
|
||||
items, err := res.client.EnvironmentList(metav1.NamespaceAll)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list: %v", res.crdType, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
f := getFileName(dumpDir, item.Metadata)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case CrdFunction:
|
||||
items, err := res.client.FunctionList(metav1.NamespaceAll)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list: %v", res.crdType, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
f := getFileName(dumpDir, item.Metadata)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case CrdPackage:
|
||||
items, err := res.client.PackageList(metav1.NamespaceAll)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list: %v", res.crdType, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
item = pkgClean(item)
|
||||
f := getFileName(dumpDir, item.Metadata)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case CrdHttpTrigger:
|
||||
items, err := res.client.HTTPTriggerList(metav1.NamespaceAll)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list: %v", res.crdType, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
f := getFileName(dumpDir, item.Metadata)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case CrdKubeWatcher:
|
||||
items, err := res.client.WatchList(metav1.NamespaceAll)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list: %v", res.crdType, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
f := getFileName(dumpDir, item.Metadata)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case CrdMessageQueueTrigger:
|
||||
var triggers []fv1.MessageQueueTrigger
|
||||
|
||||
for _, mqType := range []string{types.MessageQueueTypeNats, types.MessageQueueTypeASQ} {
|
||||
l, err := res.client.MessageQueueTriggerList(mqType, metav1.NamespaceAll)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list: %v", res.crdType, err))
|
||||
break
|
||||
}
|
||||
triggers = append(triggers, l...)
|
||||
}
|
||||
|
||||
for _, item := range triggers {
|
||||
f := getFileName(dumpDir, item.Metadata)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case CrdTimeTrigger:
|
||||
items, err := res.client.TimeTriggerList(metav1.NamespaceAll)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list: %v", res.crdType, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
f := getFileName(dumpDir, item.Metadata)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Info(fmt.Sprintf("Unknown type: %v", res.crdType))
|
||||
}
|
||||
}
|
||||
|
||||
func pkgClean(pkg fv1.Package) fv1.Package {
|
||||
// mask the sensitive information
|
||||
// use "-" as mask value to indicate the field wasn't empty
|
||||
if pkg.Spec.Source.Literal != nil {
|
||||
pkg.Spec.Source.Literal = []byte("-")
|
||||
}
|
||||
if pkg.Spec.Deployment.Literal != nil {
|
||||
pkg.Spec.Deployment.Literal = []byte("-")
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2018 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 resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fission/fission/pkg/controller/client"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
)
|
||||
|
||||
type FissionVersion struct {
|
||||
client *client.Client
|
||||
namespace string
|
||||
}
|
||||
|
||||
func NewFissionVersion(client *client.Client) Resource {
|
||||
return FissionVersion{client: client}
|
||||
}
|
||||
|
||||
func (res FissionVersion) Dump(dumpDir string) {
|
||||
ver := util.GetVersion(res.client)
|
||||
file := filepath.Clean(fmt.Sprintf("%v/%v", dumpDir, "fission-version.txt"))
|
||||
writeToFile(file, ver)
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
Copyright 2018 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 resources
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
KubernetesService = "Service"
|
||||
KubernetesDeployment = "Deployment"
|
||||
KubernetesPod = "Pod"
|
||||
KubernetesHPA = "HPA"
|
||||
KubernetesNode = "Node"
|
||||
)
|
||||
|
||||
// Kubernetes Version
|
||||
type KubernetesVersion struct {
|
||||
client *kubernetes.Clientset
|
||||
}
|
||||
|
||||
func NewKubernetesVersion(clientset *kubernetes.Clientset) Resource {
|
||||
return KubernetesVersion{client: clientset}
|
||||
}
|
||||
|
||||
func (res KubernetesVersion) Dump(dumpDir string) {
|
||||
serverVer, err := res.client.ServerVersion()
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error setting up kubernetes client: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
file := fmt.Sprintf("%v/%v", dumpDir, "kubernetes-version.txt")
|
||||
writeToFile(file, serverVer)
|
||||
}
|
||||
|
||||
// Kubernetes Object Dumper
|
||||
type KubernetesObjectDumper struct {
|
||||
client *kubernetes.Clientset
|
||||
objType string
|
||||
selector string
|
||||
}
|
||||
|
||||
func NewKubernetesObjectDumper(clientset *kubernetes.Clientset, objType string, selector string) Resource {
|
||||
return KubernetesObjectDumper{
|
||||
client: clientset,
|
||||
objType: objType,
|
||||
selector: selector,
|
||||
}
|
||||
}
|
||||
|
||||
func (res KubernetesObjectDumper) Dump(dumpDir string) {
|
||||
switch res.objType {
|
||||
case KubernetesService:
|
||||
objs, err := res.client.CoreV1().Services(metav1.NamespaceAll).List(metav1.ListOptions{LabelSelector: res.selector})
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list with selector %v: %v", res.objType, res.selector, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range objs.Items {
|
||||
item = serviceClean(item)
|
||||
f := getFileName(dumpDir, item.ObjectMeta)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case KubernetesDeployment:
|
||||
objs, err := res.client.AppsV1().Deployments(metav1.NamespaceAll).List(metav1.ListOptions{LabelSelector: res.selector})
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list with selector %v: %v", res.objType, res.selector, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range objs.Items {
|
||||
f := getFileName(dumpDir, item.ObjectMeta)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case KubernetesPod:
|
||||
objs, err := res.client.CoreV1().Pods(metav1.NamespaceAll).List(metav1.ListOptions{LabelSelector: res.selector})
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list with selector %v: %v", res.objType, res.selector, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range objs.Items {
|
||||
f := getFileName(dumpDir, item.ObjectMeta)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case KubernetesHPA:
|
||||
objs, err := res.client.AutoscalingV2beta1().HorizontalPodAutoscalers(metav1.NamespaceAll).List(metav1.ListOptions{LabelSelector: res.selector})
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list with selector %v: %v", res.objType, res.selector, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range objs.Items {
|
||||
f := getFileName(dumpDir, item.ObjectMeta)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
case KubernetesNode:
|
||||
objs, err := res.client.CoreV1().Nodes().List(metav1.ListOptions{LabelSelector: res.selector})
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting %v list with selector %v: %v", res.objType, res.selector, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range objs.Items {
|
||||
item = nodeClean(item)
|
||||
// Node doesn't have namespace value, use name here
|
||||
f := filepath.Clean(fmt.Sprintf("%v/%v", dumpDir, item.Name))
|
||||
getFileName(dumpDir, item.ObjectMeta)
|
||||
writeToFile(f, item)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Info(fmt.Sprintf("Unknown type: %v", res.objType))
|
||||
}
|
||||
}
|
||||
|
||||
// serviceClean remove sensitive data(e.g. public IP, external name) from service objects
|
||||
func serviceClean(svc corev1.Service) corev1.Service {
|
||||
svc.Spec.ExternalIPs = []string{}
|
||||
svc.Spec.LoadBalancerIP = ""
|
||||
svc.Spec.LoadBalancerSourceRanges = []string{}
|
||||
svc.Spec.ExternalName = ""
|
||||
svc.Status.LoadBalancer = corev1.LoadBalancerStatus{}
|
||||
return svc
|
||||
}
|
||||
|
||||
func nodeClean(node corev1.Node) corev1.Node {
|
||||
|
||||
var nodeAddresses []corev1.NodeAddress
|
||||
for _, address := range node.Status.Addresses {
|
||||
// use whitelist to filter the necessary information for debugging
|
||||
if address.Type == "InternalIP" || address.Type == "Hostname" {
|
||||
nodeAddresses = append(nodeAddresses, address)
|
||||
}
|
||||
}
|
||||
node.Status.Addresses = nodeAddresses
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
type KubernetesPodLogDumper struct {
|
||||
client *kubernetes.Clientset
|
||||
labelSelector string
|
||||
}
|
||||
|
||||
func NewKubernetesPodLogDumper(clientset *kubernetes.Clientset, selector string) Resource {
|
||||
return KubernetesPodLogDumper{
|
||||
client: clientset,
|
||||
labelSelector: selector,
|
||||
}
|
||||
}
|
||||
|
||||
func (res KubernetesPodLogDumper) Dump(dumpDir string) {
|
||||
l, err := res.client.CoreV1().
|
||||
Pods(metav1.NamespaceAll).
|
||||
List(metav1.ListOptions{LabelSelector: res.labelSelector})
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error getting controller list: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
for _, p := range l.Items {
|
||||
wg.Add(1)
|
||||
|
||||
go func(pod corev1.Pod) {
|
||||
defer wg.Done()
|
||||
|
||||
if !utils.IsReadyPod(&pod) {
|
||||
log.Info(fmt.Sprintf("Pod %v is not in ready state, ignore it\n", pod.Name))
|
||||
return
|
||||
}
|
||||
|
||||
// dump logs from each containers
|
||||
for _, container := range append(pod.Spec.Containers, pod.Spec.InitContainers...) {
|
||||
req := res.client.CoreV1().Pods(pod.Namespace).
|
||||
GetLogs(pod.Name, &corev1.PodLogOptions{Container: container.Name})
|
||||
|
||||
stream, err := req.Stream()
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error streaming logs for pod %v: %v", pod.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(stream)
|
||||
var buffer bytes.Buffer
|
||||
|
||||
for {
|
||||
line, _, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
stream.Close()
|
||||
break
|
||||
}
|
||||
log.Info(fmt.Sprintf("Error reading logs from buffer: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = buffer.WriteString(string(line) + "\n")
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error writing bytes to buffer: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
f := getFileName(dumpDir, pod.ObjectMeta)
|
||||
writeToFile(f, buffer.String())
|
||||
|
||||
stream.Close()
|
||||
}
|
||||
}(p)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2018 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 resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/utils"
|
||||
)
|
||||
|
||||
type Resource interface {
|
||||
Dump(string)
|
||||
}
|
||||
|
||||
func getFileName(dumpdir string, meta metav1.ObjectMeta) string {
|
||||
f := fmt.Sprintf("%v/%v_%v_%v.txt", dumpdir, meta.Namespace, meta.Name, meta.ResourceVersion)
|
||||
return filepath.Clean(f)
|
||||
}
|
||||
|
||||
func writeToFile(file string, obj interface{}) {
|
||||
bs, err := yaml.Marshal(obj)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error encoding object: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Due to unknown reason, the kubernetes objectMeta fields contain
|
||||
// empty byte and will fail os.Create/os.Openfile with error message
|
||||
// "open <file> invalid argument". To fix the problem, we need to
|
||||
// remove the empty byte from string.
|
||||
file = string(utils.RemoveZeroBytes([]byte(file)))
|
||||
|
||||
err = ioutil.WriteFile(file, bs, 0644)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("Error writing file %v: %v", file, err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
Copyright 2017 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
|
||||
|
||||
tttp://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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron"
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/urfave/cli"
|
||||
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/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
)
|
||||
|
||||
func getAPITimeInfo(client *client.Client) time.Time {
|
||||
serverInfo, err := client.ServerInfo()
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Error syncing server time information: %v", err))
|
||||
}
|
||||
return serverInfo.ServerTime.CurrentTime
|
||||
}
|
||||
|
||||
func getCronNextNActivationTime(cronSpec string, serverTime time.Time, round int) error {
|
||||
sched, err := cron.Parse(cronSpec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Current Server Time: \t%v\n", serverTime.Format(time.RFC3339))
|
||||
|
||||
for i := 0; i < round; i++ {
|
||||
serverTime = sched.Next(serverTime)
|
||||
fmt.Printf("Next %v invocation: \t%v\n", i+1, serverTime.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ttCreate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
name := c.String("name")
|
||||
if len(name) == 0 {
|
||||
name = uuid.NewV4().String()
|
||||
}
|
||||
fnName := c.String("function")
|
||||
if len(fnName) == 0 {
|
||||
log.Fatal("Need a function name to create a trigger, use --function")
|
||||
}
|
||||
|
||||
fnNamespace := c.String("fnNamespace")
|
||||
|
||||
cronSpec := c.String("cron")
|
||||
if len(cronSpec) == 0 {
|
||||
log.Fatal("Need a cron spec like '0 30 * * * *', '@every 1h30m', or '@hourly'; use --cron")
|
||||
}
|
||||
|
||||
tt := &fv1.TimeTrigger{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: fnNamespace,
|
||||
},
|
||||
Spec: fv1.TimeTriggerSpec{
|
||||
Cron: cronSpec,
|
||||
FunctionReference: fv1.FunctionReference{
|
||||
Type: fv1.FunctionReferenceTypeFunctionName,
|
||||
Name: fnName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// if we're writing a spec, don't call the API
|
||||
if c.Bool("spec") {
|
||||
specFile := fmt.Sprintf("timetrigger-%v.yaml", name)
|
||||
err := specSave(*tt, specFile)
|
||||
util.CheckErr(err, "create time trigger spec")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := client.TimeTriggerCreate(tt)
|
||||
util.CheckErr(err, "create Time trigger")
|
||||
|
||||
fmt.Printf("trigger '%v' created\n", name)
|
||||
|
||||
err = getCronNextNActivationTime(cronSpec, getAPITimeInfo(client), 1)
|
||||
util.CheckErr(err, "pass cron spec examination")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func ttGet(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ttUpdate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
ttName := c.String("name")
|
||||
if len(ttName) == 0 {
|
||||
log.Fatal("Need name of trigger, use --name")
|
||||
}
|
||||
ttNs := c.String("triggerns")
|
||||
|
||||
tt, err := client.TimeTriggerGet(&metav1.ObjectMeta{
|
||||
Name: ttName,
|
||||
Namespace: ttNs,
|
||||
})
|
||||
util.CheckErr(err, "get time trigger")
|
||||
|
||||
updated := false
|
||||
newCron := c.String("cron")
|
||||
if len(newCron) != 0 {
|
||||
tt.Spec.Cron = newCron
|
||||
updated = true
|
||||
}
|
||||
|
||||
// TODO : During update, function has to be in the same ns as the trigger object
|
||||
// but since we are not checking this for other triggers too, not sure if we need a check here.
|
||||
|
||||
fnName := c.String("function")
|
||||
if len(fnName) > 0 {
|
||||
tt.Spec.FunctionReference.Name = fnName
|
||||
updated = true
|
||||
}
|
||||
|
||||
if !updated {
|
||||
log.Fatal("Nothing to update. Use --cron or --function.")
|
||||
}
|
||||
|
||||
_, err = client.TimeTriggerUpdate(tt)
|
||||
util.CheckErr(err, "update Time trigger")
|
||||
|
||||
fmt.Printf("trigger '%v' updated\n", ttName)
|
||||
|
||||
err = getCronNextNActivationTime(newCron, getAPITimeInfo(client), 1)
|
||||
util.CheckErr(err, "pass cron spec examination")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ttDelete(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
ttName := c.String("name")
|
||||
if len(ttName) == 0 {
|
||||
log.Fatal("Need name of trigger to delete, use --name")
|
||||
}
|
||||
ttNs := c.String("triggerns")
|
||||
|
||||
err := client.TimeTriggerDelete(&metav1.ObjectMeta{
|
||||
Name: ttName,
|
||||
Namespace: ttNs,
|
||||
})
|
||||
util.CheckErr(err, "delete trigger")
|
||||
|
||||
fmt.Printf("trigger '%v' deleted\n", ttName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ttList(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
ttNs := c.String("triggerns")
|
||||
|
||||
tts, err := client.TimeTriggerList(ttNs)
|
||||
util.CheckErr(err, "list Time triggers")
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", "NAME", "CRON", "FUNCTION_NAME")
|
||||
for _, tt := range tts {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n",
|
||||
tt.Metadata.Name, tt.Spec.Cron, tt.Spec.FunctionReference.Name)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ttTest(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
round := c.Int("round")
|
||||
cronSpec := c.String("cron")
|
||||
if len(cronSpec) == 0 {
|
||||
log.Fatal("Need a cron spec like '0 30 * * * *', '@every 1h30m', or '@hourly'; use --cron")
|
||||
}
|
||||
|
||||
err := getCronNextNActivationTime(cronSpec, getAPITimeInfo(client), round)
|
||||
util.CheckErr(err, "pass cron spec examination")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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
|
||||
|
||||
const (
|
||||
FISSION_DEPLOYMENT_NAME_KEY = "fission-name"
|
||||
FISSION_DEPLOYMENT_UID_KEY = "fission-uid"
|
||||
)
|
||||
|
||||
// CLI spec types
|
||||
type (
|
||||
// DeploymentConfig is the global configuration for a set of Fission specs.
|
||||
DeploymentConfig struct {
|
||||
// TypeMeta describes the type of this object. It is inlined. The Kind
|
||||
// field should always be "DeploymentConfig".
|
||||
TypeMeta `json:",inline"`
|
||||
|
||||
// Name is a user-friendly name for the deployment. It is also stored in
|
||||
// all uploaded resources as an annotation.
|
||||
Name string `json:"name"`
|
||||
|
||||
// UID uniquely identifies the deployment. It is stored as a label and
|
||||
// used to find resources to clean up when local specs are changed.
|
||||
UID string `json:"uid"`
|
||||
}
|
||||
|
||||
// ArchiveUploadSpec specifies a set of files to be archived and uploaded.
|
||||
//
|
||||
// The resulting archive can be referenced as archive://<Name> in PackageSpecs,
|
||||
// using the name specified in the archive. The fission spec applier will
|
||||
// replace the archive:// URL with a real HTTP URL after uploading the file.
|
||||
ArchiveUploadSpec struct {
|
||||
// TypeMeta describes the type of this object. It is inlined. The Kind
|
||||
// field should always be "ArchiveUploadSpec".
|
||||
TypeMeta `json:",inline"`
|
||||
|
||||
// Name is a local name that can be used to reference this archive. It
|
||||
// must be unique; duplicate names will cause an error while handling
|
||||
// specs.
|
||||
Name string `json:"name"`
|
||||
|
||||
// RootDir specifies the root that the globs below are relative to. It
|
||||
// is optional and defaults to the parent directory of the spec
|
||||
// directory: for example, if the deployment config is at
|
||||
// /path/to/project/specs/config.yaml, the RootDir is /path/to/project.
|
||||
RootDir string `json:"rootdir,omitempty"`
|
||||
|
||||
// IncludeGlobs is a list of Unix shell globs to include
|
||||
IncludeGlobs []string `json:"include,omitempty"`
|
||||
|
||||
// ExcludeGlobs is a list of globs to exclude from the set specified by
|
||||
// IncludeGlobs.
|
||||
ExcludeGlobs []string `json:"exclude,omitempty"`
|
||||
}
|
||||
|
||||
// TypeMeta is the same as Kubernetes' TypeMeta, and allows us to version and
|
||||
// unmarshal local-only objects (like ArchiveUploadSpec) the same way that
|
||||
// Kubernetes does.
|
||||
TypeMeta struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,416 @@
|
||||
/*
|
||||
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"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/urfave/cli"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
v1 "github.com/fission/fission/pkg/v1"
|
||||
)
|
||||
|
||||
type (
|
||||
V1FissionState struct {
|
||||
Functions []v1.Function `json:"functions"`
|
||||
Environments []v1.Environment `json:"environments"`
|
||||
HTTPTriggers []v1.HTTPTrigger `json:"httptriggers"`
|
||||
Mqtriggers []v1.MessageQueueTrigger `json:"mqtriggers"`
|
||||
TimeTriggers []v1.TimeTrigger `json:"timetriggers"`
|
||||
Watches []v1.Watch `json:"watches"`
|
||||
NameChanges map[string]string `json:"namechanges"`
|
||||
}
|
||||
nameRemapper struct {
|
||||
oldToNew map[string]string
|
||||
newNames map[string]bool
|
||||
}
|
||||
)
|
||||
|
||||
func getV1URL(serverUrl string) string {
|
||||
if len(serverUrl) == 0 {
|
||||
log.Fatal("Need --server or FISSION_URL set to your fission server.")
|
||||
}
|
||||
isHTTPS := strings.Index(serverUrl, "https://") == 0
|
||||
isHTTP := strings.Index(serverUrl, "http://") == 0
|
||||
if !(isHTTP || isHTTPS) {
|
||||
serverUrl = "http://" + serverUrl
|
||||
}
|
||||
v1url := strings.TrimSuffix(serverUrl, "/") + "/v1"
|
||||
return v1url
|
||||
}
|
||||
|
||||
func get(url string) []byte {
|
||||
resp, err := http.Get(url)
|
||||
util.CheckErr(err, "get fission v0.1 state")
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
util.CheckErr(err, "reading server response")
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Fatal(fmt.Sprintf("Failed to fetch fission v0.1 state: %v", string(body)))
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// track a name in the remapper, creating a new name if needed
|
||||
func (nr *nameRemapper) trackName(old string) {
|
||||
// all kubernetes names must match this regex
|
||||
kubeNameRegex := "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
|
||||
maxLen := 63
|
||||
|
||||
ok, err := regexp.MatchString(kubeNameRegex, old)
|
||||
util.CheckErr(err, "match name regexp")
|
||||
if ok && len(old) < maxLen {
|
||||
// no rename
|
||||
nr.oldToNew[old] = old
|
||||
nr.newNames[old] = true
|
||||
return
|
||||
}
|
||||
|
||||
newName := strings.ToLower(old)
|
||||
|
||||
// remove disallowed
|
||||
inv, err := regexp.Compile("[^-a-z0-9]")
|
||||
util.CheckErr(err, "compile regexp")
|
||||
newName = string(inv.ReplaceAll([]byte(newName), []byte("-")))
|
||||
|
||||
// trim leading non-alphabetic
|
||||
leadingnonalpha, err := regexp.Compile("^[^a-z]+")
|
||||
util.CheckErr(err, "compile regexp")
|
||||
newName = string(leadingnonalpha.ReplaceAll([]byte(newName), []byte{}))
|
||||
|
||||
// trim trailing
|
||||
trailing, err := regexp.Compile("[^a-z0-9]+$")
|
||||
util.CheckErr(err, "compile regexp")
|
||||
newName = string(trailing.ReplaceAll([]byte(newName), []byte{}))
|
||||
|
||||
// truncate to length
|
||||
if len(newName) > maxLen-4 {
|
||||
newName = newName[0:(maxLen - 4)]
|
||||
}
|
||||
|
||||
// uniqueness
|
||||
n := newName
|
||||
i := 0
|
||||
for {
|
||||
_, exists := nr.newNames[n]
|
||||
if !exists {
|
||||
break
|
||||
} else {
|
||||
i++
|
||||
n = fmt.Sprintf("%v-%v", newName, i)
|
||||
}
|
||||
}
|
||||
newName = n
|
||||
|
||||
// track
|
||||
nr.oldToNew[old] = newName
|
||||
nr.newNames[newName] = true
|
||||
}
|
||||
|
||||
func upgradeDumpV1State(v1url string, filename string) {
|
||||
var v1state V1FissionState
|
||||
|
||||
fmt.Println("Getting environments")
|
||||
resp := get(v1url + "/environments")
|
||||
err := json.Unmarshal(resp, &v1state.Environments)
|
||||
util.CheckErr(err, "parse server response")
|
||||
|
||||
fmt.Println("Getting watches")
|
||||
resp = get(v1url + "/watches")
|
||||
err = json.Unmarshal(resp, &v1state.Watches)
|
||||
util.CheckErr(err, "parse server response")
|
||||
|
||||
fmt.Println("Getting routes")
|
||||
resp = get(v1url + "/triggers/http")
|
||||
err = json.Unmarshal(resp, &v1state.HTTPTriggers)
|
||||
util.CheckErr(err, "parse server response")
|
||||
|
||||
fmt.Println("Getting message queue triggers")
|
||||
resp = get(v1url + "/triggers/messagequeue")
|
||||
err = json.Unmarshal(resp, &v1state.Mqtriggers)
|
||||
util.CheckErr(err, "parse server response")
|
||||
|
||||
fmt.Println("Getting time triggers")
|
||||
resp = get(v1url + "/triggers/time")
|
||||
err = json.Unmarshal(resp, &v1state.TimeTriggers)
|
||||
util.CheckErr(err, "parse server response")
|
||||
|
||||
fmt.Println("Getting function list")
|
||||
resp = get(v1url + "/functions")
|
||||
err = json.Unmarshal(resp, &v1state.Functions)
|
||||
util.CheckErr(err, "parse server response")
|
||||
|
||||
// we have to change names that are disallowed in kubernetes
|
||||
nr := nameRemapper{
|
||||
oldToNew: make(map[string]string),
|
||||
newNames: make(map[string]bool),
|
||||
}
|
||||
|
||||
// get all referenced function metadata
|
||||
funcMetaSet := make(map[v1.Metadata]bool)
|
||||
for _, f := range v1state.Functions {
|
||||
funcMetaSet[f.Metadata] = true
|
||||
nr.trackName(f.Metadata.Name)
|
||||
}
|
||||
for _, t := range v1state.HTTPTriggers {
|
||||
funcMetaSet[t.Function] = true
|
||||
nr.trackName(t.Metadata.Name)
|
||||
}
|
||||
for _, t := range v1state.Mqtriggers {
|
||||
funcMetaSet[t.Function] = true
|
||||
nr.trackName(t.Metadata.Name)
|
||||
}
|
||||
for _, t := range v1state.Watches {
|
||||
funcMetaSet[t.Function] = true
|
||||
nr.trackName(t.Metadata.Name)
|
||||
}
|
||||
for _, t := range v1state.TimeTriggers {
|
||||
funcMetaSet[t.Function] = true
|
||||
nr.trackName(t.Metadata.Name)
|
||||
}
|
||||
|
||||
for _, e := range v1state.Environments {
|
||||
nr.trackName(e.Metadata.Name)
|
||||
}
|
||||
|
||||
fmt.Println("Getting functions")
|
||||
// get each function
|
||||
funcs := make(map[v1.Metadata]v1.Function)
|
||||
for m := range funcMetaSet {
|
||||
if len(m.Uid) != 0 {
|
||||
resp = get(fmt.Sprintf("%v/functions/%v?uid=%v", v1url, m.Name, m.Uid))
|
||||
} else {
|
||||
resp = get(fmt.Sprintf("%v/functions/%v", v1url, m.Name))
|
||||
}
|
||||
|
||||
var f v1.Function
|
||||
|
||||
// unmarshal
|
||||
err = json.Unmarshal(resp, &f)
|
||||
util.CheckErr(err, "parse server response")
|
||||
|
||||
// load into a map to remove duplicates
|
||||
funcs[f.Metadata] = f
|
||||
}
|
||||
|
||||
// add list of unique functions to v1state from map
|
||||
v1state.Functions = make([]v1.Function, 0)
|
||||
for _, f := range funcs {
|
||||
v1state.Functions = append(v1state.Functions, f)
|
||||
}
|
||||
|
||||
// dump name changes
|
||||
v1state.NameChanges = nr.oldToNew
|
||||
|
||||
// serialize v1state
|
||||
out, err := json.MarshalIndent(v1state, "", " ")
|
||||
util.CheckErr(err, "serialize v0.1 state")
|
||||
|
||||
// dump to file fission-v01-state.json
|
||||
if len(filename) == 0 {
|
||||
filename = "fission-v01-state.json"
|
||||
}
|
||||
err = ioutil.WriteFile(filename, out, 0644)
|
||||
util.CheckErr(err, "write file")
|
||||
|
||||
fmt.Printf("Done: Saved %v functions, %v HTTP triggers, %v watches, %v message queue triggers, %v time triggers.\n",
|
||||
len(v1state.Functions), len(v1state.HTTPTriggers), len(v1state.Watches), len(v1state.Mqtriggers), len(v1state.TimeTriggers))
|
||||
}
|
||||
|
||||
func functionRefFromV1Metadata(m *v1.Metadata, nameRemap map[string]string) *fv1.FunctionReference {
|
||||
return &fv1.FunctionReference{
|
||||
Type: fv1.FunctionReferenceTypeFunctionName,
|
||||
Name: nameRemap[m.Name],
|
||||
}
|
||||
}
|
||||
|
||||
func crdMetadataFromV1Metadata(m *v1.Metadata, nameRemap map[string]string) *metav1.ObjectMeta {
|
||||
return &metav1.ObjectMeta{
|
||||
Name: nameRemap[m.Name],
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
}
|
||||
}
|
||||
|
||||
func upgradeDumpState(c *cli.Context) error {
|
||||
u := getV1URL(c.GlobalString("server"))
|
||||
filename := c.String("file")
|
||||
|
||||
// check v1
|
||||
resp, err := http.Get(u + "/environments")
|
||||
util.CheckErr(err, "reach fission server")
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
msg := fmt.Sprintf("Server %v isn't a v1 Fission server. Use --server to point at a pre-0.2.x Fission server.", u)
|
||||
log.Fatal(msg)
|
||||
}
|
||||
|
||||
upgradeDumpV1State(u, filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeRestoreState(c *cli.Context) error {
|
||||
filename := c.String("file")
|
||||
if len(filename) == 0 {
|
||||
filename = "fission-v01-state.json"
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(filename)
|
||||
util.CheckErr(err, fmt.Sprintf("open file %v", filename))
|
||||
|
||||
var v1state V1FissionState
|
||||
err = json.Unmarshal(contents, &v1state)
|
||||
util.CheckErr(err, "parse dumped v1 state")
|
||||
|
||||
// create a regular v2 client
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
// create functions
|
||||
for _, f := range v1state.Functions {
|
||||
|
||||
// get post-rename function name, derive pkg name from it
|
||||
fnName := v1state.NameChanges[f.Metadata.Name]
|
||||
pkgName := fmt.Sprintf("%v-%v", fnName, strings.ToLower(uniuri.NewLen(6)))
|
||||
|
||||
// write function to file
|
||||
tmpfile, err := ioutil.TempFile("", pkgName)
|
||||
util.CheckErr(err, "create temporary file")
|
||||
code, err := base64.StdEncoding.DecodeString(f.Code)
|
||||
util.CheckErr(err, "decode base64 function contents")
|
||||
tmpfile.Write(code)
|
||||
tmpfile.Sync()
|
||||
tmpfile.Close()
|
||||
|
||||
// upload
|
||||
ctx := context.Background()
|
||||
archive := uploadArchive(ctx, client, tmpfile.Name())
|
||||
os.Remove(tmpfile.Name())
|
||||
|
||||
// create pkg
|
||||
pkgSpec := fv1.PackageSpec{
|
||||
Environment: fv1.EnvironmentReference{
|
||||
Name: v1state.NameChanges[f.Environment.Name],
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
},
|
||||
Deployment: *archive,
|
||||
}
|
||||
pkg, err := client.PackageCreate(&fv1.Package{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: pkgName,
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
},
|
||||
Spec: pkgSpec,
|
||||
})
|
||||
util.CheckErr(err, fmt.Sprintf("create package %v", pkgName))
|
||||
_, err = client.FunctionCreate(&fv1.Function{
|
||||
Metadata: *crdMetadataFromV1Metadata(&f.Metadata, v1state.NameChanges),
|
||||
Spec: fv1.FunctionSpec{
|
||||
Environment: pkgSpec.Environment,
|
||||
Package: fv1.FunctionPackageRef{
|
||||
PackageRef: fv1.PackageRef{
|
||||
Name: pkg.Name,
|
||||
Namespace: pkg.Namespace,
|
||||
ResourceVersion: pkg.ResourceVersion,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
util.CheckErr(err, fmt.Sprintf("create function %v", v1state.NameChanges[f.Metadata.Name]))
|
||||
|
||||
}
|
||||
|
||||
// create envs
|
||||
for _, e := range v1state.Environments {
|
||||
_, err = client.EnvironmentCreate(&fv1.Environment{
|
||||
Metadata: *crdMetadataFromV1Metadata(&e.Metadata, v1state.NameChanges),
|
||||
Spec: fv1.EnvironmentSpec{
|
||||
Version: 1,
|
||||
Runtime: fv1.Runtime{
|
||||
Image: e.RunContainerImageUrl,
|
||||
},
|
||||
},
|
||||
})
|
||||
util.CheckErr(err, fmt.Sprintf("create environment %v", e.Metadata.Name))
|
||||
}
|
||||
|
||||
// create httptriggers
|
||||
for _, t := range v1state.HTTPTriggers {
|
||||
_, err = client.HTTPTriggerCreate(&fv1.HTTPTrigger{
|
||||
Metadata: *crdMetadataFromV1Metadata(&t.Metadata, v1state.NameChanges),
|
||||
Spec: fv1.HTTPTriggerSpec{
|
||||
RelativeURL: t.UrlPattern,
|
||||
Method: t.Method,
|
||||
FunctionReference: *functionRefFromV1Metadata(&t.Function, v1state.NameChanges),
|
||||
},
|
||||
})
|
||||
util.CheckErr(err, fmt.Sprintf("create http trigger %v", t.Metadata.Name))
|
||||
}
|
||||
|
||||
// create mqtriggers
|
||||
for _, t := range v1state.Mqtriggers {
|
||||
_, err = client.MessageQueueTriggerCreate(&fv1.MessageQueueTrigger{
|
||||
Metadata: *crdMetadataFromV1Metadata(&t.Metadata, v1state.NameChanges),
|
||||
Spec: fv1.MessageQueueTriggerSpec{
|
||||
FunctionReference: *functionRefFromV1Metadata(&t.Function, v1state.NameChanges),
|
||||
MessageQueueType: fv1.MessageQueueTypeNats, // only NATS is supported at that time (v1 types)
|
||||
Topic: t.Topic,
|
||||
ResponseTopic: t.ResponseTopic,
|
||||
},
|
||||
})
|
||||
util.CheckErr(err, fmt.Sprintf("create http trigger %v", t.Metadata.Name))
|
||||
}
|
||||
|
||||
// create time triggers
|
||||
for _, t := range v1state.TimeTriggers {
|
||||
_, err = client.TimeTriggerCreate(&fv1.TimeTrigger{
|
||||
Metadata: *crdMetadataFromV1Metadata(&t.Metadata, v1state.NameChanges),
|
||||
Spec: fv1.TimeTriggerSpec{
|
||||
FunctionReference: *functionRefFromV1Metadata(&t.Function, v1state.NameChanges),
|
||||
Cron: t.Cron,
|
||||
},
|
||||
})
|
||||
util.CheckErr(err, fmt.Sprintf("create time trigger %v", t.Metadata.Name))
|
||||
}
|
||||
|
||||
// create watches
|
||||
for _, t := range v1state.Watches {
|
||||
_, err = client.WatchCreate(&fv1.KubernetesWatchTrigger{
|
||||
Metadata: *crdMetadataFromV1Metadata(&t.Metadata, v1state.NameChanges),
|
||||
Spec: fv1.KubernetesWatchTriggerSpec{
|
||||
Namespace: t.Namespace,
|
||||
Type: t.ObjType,
|
||||
FunctionReference: *functionRefFromV1Metadata(&t.Function, v1state.NameChanges),
|
||||
},
|
||||
})
|
||||
util.CheckErr(err, fmt.Sprintf("create kubernetes watch trigger %v", t.Metadata.Name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/portforward"
|
||||
"k8s.io/client-go/transport/spdy"
|
||||
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/utils"
|
||||
)
|
||||
|
||||
// Port forward a free local port to a pod on the cluster. The pod is
|
||||
// found in the specified namespace by labelSelector. The pod's port
|
||||
// is found by looking for a service in the same namespace and using
|
||||
// its targetPort. Once the port forward is started, wait for it to
|
||||
// start accepting connections before returning.
|
||||
func SetupPortForward(namespace, labelSelector string) string {
|
||||
log.Verbose(2, "Setting up port forward to %s in namespace %s",
|
||||
labelSelector, namespace)
|
||||
|
||||
localPort, err := findFreePort()
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Error finding unused port :%v", err.Error()))
|
||||
}
|
||||
|
||||
log.Verbose(2, "Waiting for local port %v", localPort)
|
||||
for {
|
||||
conn, _ := net.DialTimeout("tcp",
|
||||
net.JoinHostPort("", localPort), time.Millisecond)
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
}
|
||||
|
||||
log.Verbose(2, "Starting port forward from local port %v", localPort)
|
||||
go func() {
|
||||
err := runPortForward(labelSelector, localPort, namespace)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Error forwarding to port %v: %s", localPort, err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
log.Verbose(2, "Waiting for port forward %v to start...", localPort)
|
||||
for {
|
||||
conn, _ := net.DialTimeout("tcp",
|
||||
net.JoinHostPort("", localPort), time.Millisecond)
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
}
|
||||
|
||||
log.Verbose(2, "Port forward from local port %v started", localPort)
|
||||
|
||||
return localPort
|
||||
}
|
||||
|
||||
func findFreePort() (string, error) {
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
err = listener.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// runPortForward creates a local port forward to the specified pod
|
||||
func runPortForward(labelSelector string, localPort string, ns string) error {
|
||||
config, clientset := GetKubernetesClient()
|
||||
|
||||
log.Verbose(2, "Connected to Kubernetes API")
|
||||
|
||||
// if namespace is unset, try to find a pod in any namespace
|
||||
if len(ns) == 0 {
|
||||
ns = meta_v1.NamespaceAll
|
||||
}
|
||||
|
||||
// get the pod; if there is more than one, ask the user to disambiguate
|
||||
podList, err := clientset.CoreV1().Pods(ns).
|
||||
List(meta_v1.ListOptions{LabelSelector: labelSelector})
|
||||
if err != nil || len(podList.Items) == 0 {
|
||||
log.Fatal(fmt.Sprintf("Error getting pod for port-forwarding with label selector %v: %v", labelSelector, err))
|
||||
}
|
||||
|
||||
nsList := make([]string, 0)
|
||||
namespaces := make(map[string][]*v1.Pod)
|
||||
|
||||
// make a useful error message if there is more than one install
|
||||
if len(podList.Items) > 0 {
|
||||
for _, p := range podList.Items {
|
||||
if _, ok := namespaces[p.Namespace]; !ok {
|
||||
namespaces[p.Namespace] = []*v1.Pod{}
|
||||
nsList = append(nsList, p.Namespace)
|
||||
}
|
||||
namespaces[p.Namespace] = append(namespaces[p.Namespace], &p)
|
||||
}
|
||||
if len(nsList) > 1 {
|
||||
log.Fatal(fmt.Sprintf("Found %v fission installs, set FISSION_NAMESPACE to one of: %v",
|
||||
len(namespaces), strings.Join(nsList, " ")))
|
||||
}
|
||||
}
|
||||
|
||||
// there is at most one namespace in nsList,
|
||||
// use index 0 to get from it directly.
|
||||
ns = nsList[0]
|
||||
pods, ok := namespaces[ns]
|
||||
if !ok {
|
||||
log.Fatal(fmt.Sprintf("Error finding fission install within the given namespace %v, please check FISSION_NAMESPACE is set properly", ns))
|
||||
}
|
||||
|
||||
var podName, podNameSpace string
|
||||
|
||||
// make sure we establish the connection to a healthy pod
|
||||
for _, p := range pods {
|
||||
if utils.IsReadyPod(p) {
|
||||
podName = p.Name
|
||||
podNameSpace = p.Namespace
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// get the service and the target port
|
||||
svcs, err := clientset.CoreV1().Services(podNameSpace).
|
||||
List(meta_v1.ListOptions{LabelSelector: labelSelector})
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Error getting %v service :%v", labelSelector, err.Error()))
|
||||
}
|
||||
if len(svcs.Items) == 0 {
|
||||
log.Fatal(fmt.Sprintf("Service %v not found", labelSelector))
|
||||
}
|
||||
service := &svcs.Items[0]
|
||||
|
||||
var targetPort string
|
||||
for _, servicePort := range service.Spec.Ports {
|
||||
targetPort = servicePort.TargetPort.String()
|
||||
}
|
||||
log.Verbose(2, "Connecting to port %v on pod %v/%v", targetPort, podNameSpace, podNameSpace)
|
||||
|
||||
stopChannel := make(chan struct{}, 1)
|
||||
readyChannel := make(chan struct{})
|
||||
|
||||
// create request URL
|
||||
req := clientset.CoreV1().RESTClient().Post().Resource("pods").
|
||||
Namespace(podNameSpace).Name(podName).SubResource("portforward")
|
||||
url := req.URL()
|
||||
|
||||
// create ports slice
|
||||
portCombo := localPort + ":" + targetPort
|
||||
ports := []string{portCombo}
|
||||
|
||||
// actually start the port-forwarding process here
|
||||
transport, upgrader, err := spdy.RoundTripperFor(config)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Failed to connect to Fission service on Kubernetes: %v", err.Error())
|
||||
log.Fatal(msg)
|
||||
}
|
||||
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", url)
|
||||
|
||||
outStream := os.Stdout
|
||||
if log.Verbosity < 2 {
|
||||
outStream = nil
|
||||
}
|
||||
fw, err := portforward.New(dialer, ports, stopChannel, readyChannel, outStream, os.Stderr)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("portforward.new errored out :%v", err.Error())
|
||||
log.Fatal(msg)
|
||||
}
|
||||
|
||||
log.Verbose(2, "Starting port forwarder")
|
||||
return fw.ForwardPorts()
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright 2018 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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/fission/fission/pkg/controller/client"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
)
|
||||
|
||||
func GetApiClient(serverUrl string) *client.Client {
|
||||
if len(serverUrl) == 0 {
|
||||
// starts local portforwarder etc.
|
||||
serverUrl = GetServerUrl()
|
||||
}
|
||||
|
||||
isHTTPS := strings.Index(serverUrl, "https://") == 0
|
||||
isHTTP := strings.Index(serverUrl, "http://") == 0
|
||||
|
||||
if !(isHTTP || isHTTPS) {
|
||||
serverUrl = "http://" + serverUrl
|
||||
}
|
||||
|
||||
return client.MakeClient(serverUrl)
|
||||
}
|
||||
|
||||
func GetFissionNamespace() string {
|
||||
fissionNamespace := os.Getenv("FISSION_NAMESPACE")
|
||||
return fissionNamespace
|
||||
}
|
||||
|
||||
func GetServerUrl() string {
|
||||
return GetApplicationUrl("application=fission-api")
|
||||
}
|
||||
|
||||
func GetApplicationUrl(selector string) string {
|
||||
var serverUrl string
|
||||
// Use FISSION_URL env variable if set; otherwise, port-forward to controller.
|
||||
fissionUrl := os.Getenv("FISSION_URL")
|
||||
if len(fissionUrl) == 0 {
|
||||
fissionNamespace := GetFissionNamespace()
|
||||
localPort := SetupPortForward(fissionNamespace, "application=fission-api")
|
||||
serverUrl = "http://127.0.0.1:" + localPort
|
||||
} else {
|
||||
serverUrl = fissionUrl
|
||||
}
|
||||
return serverUrl
|
||||
}
|
||||
|
||||
func CheckErr(err error, msg string) {
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed to %v: %v", msg, err))
|
||||
}
|
||||
}
|
||||
|
||||
// KubifyName make a kubernetes compliant name out of an arbitrary string
|
||||
func KubifyName(old string) string {
|
||||
// Kubernetes maximum name length (for some names; others can be 253 chars)
|
||||
maxLen := 63
|
||||
|
||||
newName := strings.ToLower(old)
|
||||
|
||||
// replace disallowed chars with '-'
|
||||
inv, err := regexp.Compile("[^-a-z0-9]")
|
||||
CheckErr(err, "compile regexp")
|
||||
newName = string(inv.ReplaceAll([]byte(newName), []byte("-")))
|
||||
|
||||
// trim leading non-alphabetic
|
||||
leadingnonalpha, err := regexp.Compile("^[^a-z]+")
|
||||
CheckErr(err, "compile regexp")
|
||||
newName = string(leadingnonalpha.ReplaceAll([]byte(newName), []byte{}))
|
||||
|
||||
// trim trailing
|
||||
trailing, err := regexp.Compile("[^a-z0-9]+$")
|
||||
CheckErr(err, "compile regexp")
|
||||
newName = string(trailing.ReplaceAll([]byte(newName), []byte{}))
|
||||
|
||||
// truncate to length
|
||||
if len(newName) > maxLen {
|
||||
newName = newName[0:maxLen]
|
||||
}
|
||||
|
||||
// if we removed everything, call this thing "default". maybe
|
||||
// we should generate a unique name...
|
||||
if len(newName) == 0 {
|
||||
newName = "default"
|
||||
}
|
||||
|
||||
return newName
|
||||
}
|
||||
|
||||
// GetKubernetesClient builds a new kubernetes client. If the KUBECONFIG
|
||||
// environment variable is empty or doesn't exist, ~/.kube/config is used for
|
||||
// the kube config path
|
||||
func GetKubernetesClient() (*restclient.Config, *kubernetes.Clientset) {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
|
||||
kubeConfigPath := os.Getenv("KUBECONFIG")
|
||||
if len(kubeConfigPath) == 0 {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Could not get the current users directory: %s", err))
|
||||
}
|
||||
|
||||
kubeConfigPath = filepath.Join(usr.HomeDir, ".kube", "config")
|
||||
|
||||
if _, err := os.Stat(kubeConfigPath); os.IsNotExist(err) {
|
||||
log.Fatal("Couldn't find kubeconfig file. " +
|
||||
"Set the KUBECONFIG environment variable to your kubeconfig's path.")
|
||||
}
|
||||
loadingRules.ExplicitPath = kubeConfigPath
|
||||
log.Verbose(2, "Using kubeconfig from %q", kubeConfigPath)
|
||||
} else {
|
||||
log.Verbose(2, "Using kubeconfig from environment %q", kubeConfigPath)
|
||||
}
|
||||
|
||||
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
loadingRules, &clientcmd.ConfigOverrides{}).ClientConfig()
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed to build Kubernetes config: %s", err))
|
||||
}
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed to connect to Kubernetes: %s", err))
|
||||
}
|
||||
|
||||
return config, clientset
|
||||
}
|
||||
|
||||
// given a list of functions, this checks if the functions actually exist on the cluster
|
||||
func CheckFunctionExistence(fissionClient *client.Client, functions []string, fnNamespace string) (err error) {
|
||||
fnMissing := make([]string, 0)
|
||||
for _, fnName := range functions {
|
||||
meta := &metav1.ObjectMeta{
|
||||
Name: fnName,
|
||||
Namespace: fnNamespace,
|
||||
}
|
||||
|
||||
_, err := fissionClient.FunctionGet(meta)
|
||||
if err != nil {
|
||||
fnMissing = append(fnMissing, fnName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(fnMissing) > 0 {
|
||||
return fmt.Errorf("function(s) %s, not present in namespace : %s", fnMissing, fnNamespace)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/fission/fission/pkg/controller/client"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/plugin"
|
||||
"github.com/fission/fission/pkg/info"
|
||||
)
|
||||
|
||||
// Versions is a container of versions of the client (and its plugins) and server (and its plugins).
|
||||
type Versions struct {
|
||||
Client map[string]info.BuildMeta `json:"client"`
|
||||
Server map[string]info.BuildMeta `json:"server"`
|
||||
}
|
||||
|
||||
func GetVersion(client *client.Client) []byte {
|
||||
serverInfo, err := client.ServerInfo()
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("Error getting Fission API version: %v", err))
|
||||
}
|
||||
|
||||
// Fetch client versions
|
||||
versions := Versions{
|
||||
Client: map[string]info.BuildMeta{
|
||||
"fission/core": info.BuildInfo(),
|
||||
},
|
||||
}
|
||||
for _, pmd := range plugin.FindAll() {
|
||||
versions.Client[pmd.Name] = info.BuildMeta{
|
||||
Version: pmd.Version,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch server versions
|
||||
versions.Server = map[string]info.BuildMeta{
|
||||
"fission/core": serverInfo.Build,
|
||||
}
|
||||
// FUTURE: fetch versions of plugins server-side
|
||||
bs, err := yaml.Marshal(versions)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to format versions: " + err.Error())
|
||||
}
|
||||
|
||||
return bs
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/urfave/cli"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
|
||||
"github.com/fission/fission/pkg/fission-cli/log"
|
||||
"github.com/fission/fission/pkg/fission-cli/util"
|
||||
)
|
||||
|
||||
func wCreate(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
fnName := c.String("function")
|
||||
if len(fnName) == 0 {
|
||||
log.Fatal("Need a function name to create a watch, use --function")
|
||||
}
|
||||
fnNamespace := c.String("fnNamespace")
|
||||
|
||||
namespace := c.String("ns")
|
||||
if len(namespace) == 0 {
|
||||
fmt.Println("Watch 'default' namespace. Use --ns <namespace> to override.")
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
objType := c.String("type")
|
||||
if len(objType) == 0 {
|
||||
fmt.Println("Object type unspecified, will watch pods. Use --type <type> to override.")
|
||||
objType = "pod"
|
||||
}
|
||||
|
||||
labels := c.String("labels")
|
||||
// empty 'labels' selects everything
|
||||
if len(labels) == 0 {
|
||||
fmt.Printf("Watching all objects of type '%v', use --labels to refine selection.\n", objType)
|
||||
} else {
|
||||
// TODO
|
||||
fmt.Printf("Label selector not implemented, watching all objects")
|
||||
}
|
||||
|
||||
// automatically name watches
|
||||
watchName := uuid.NewV4().String()
|
||||
|
||||
w := &fv1.KubernetesWatchTrigger{
|
||||
Metadata: metav1.ObjectMeta{
|
||||
Name: watchName,
|
||||
Namespace: fnNamespace,
|
||||
},
|
||||
Spec: fv1.KubernetesWatchTriggerSpec{
|
||||
Namespace: namespace,
|
||||
Type: objType,
|
||||
//LabelSelector: labels,
|
||||
FunctionReference: fv1.FunctionReference{
|
||||
Name: fnName,
|
||||
Type: fv1.FunctionReferenceTypeFunctionName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// if we're writing a spec, don't call the API
|
||||
if c.Bool("spec") {
|
||||
specFile := fmt.Sprintf("kubewatch-%v.yaml", watchName)
|
||||
err := specSave(*w, specFile)
|
||||
util.CheckErr(err, "create kubernetes watch spec")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := client.WatchCreate(w)
|
||||
util.CheckErr(err, "create watch")
|
||||
|
||||
fmt.Printf("watch '%v' created\n", w.Metadata.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func wGet(c *cli.Context) error {
|
||||
// TODO
|
||||
log.Fatal("Not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func wUpdate(c *cli.Context) error {
|
||||
// TODO
|
||||
log.Fatal("Not implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func wDelete(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
wName := c.String("name")
|
||||
if len(wName) == 0 {
|
||||
log.Fatal("Need name of watch to delete, use --name")
|
||||
}
|
||||
wNs := c.String("triggerns")
|
||||
|
||||
err := client.WatchDelete(&metav1.ObjectMeta{
|
||||
Name: wName,
|
||||
Namespace: wNs,
|
||||
})
|
||||
util.CheckErr(err, "delete watch")
|
||||
|
||||
fmt.Printf("watch '%v' deleted\n", wName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func wList(c *cli.Context) error {
|
||||
client := util.GetApiClient(c.GlobalString("server"))
|
||||
|
||||
wNs := c.String("triggerns")
|
||||
|
||||
ws, err := client.WatchList(wNs)
|
||||
util.CheckErr(err, "list watches")
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\n",
|
||||
"NAME", "NAMESPACE", "OBJTYPE", "LABELS", "FUNCTION_NAME")
|
||||
for _, wa := range ws {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\n",
|
||||
wa.Metadata.Name, wa.Spec.Namespace, wa.Spec.Type, wa.Spec.LabelSelector, wa.Spec.FunctionReference.Name)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user