Move packages to proejct/pkg to follow go project folder structure convention (#1190)

This commit is contained in:
Ta-Ching Chen
2019-05-31 16:28:55 +08:00
committed by GitHub
parent 1c5fd92ad6
commit a0e9a39511
196 changed files with 1672 additions and 1716 deletions
+121
View File
@@ -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)
}
+235
View File
@@ -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
}
+353
View File
@@ -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
}
+851
View File
@@ -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
}
+289
View File
@@ -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)
}
}
}
+306
View File
@@ -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
}
+46
View File
@@ -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...)
}
}
+202
View File
@@ -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 ""
}
+58
View File
@@ -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)
}
+467
View File
@@ -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}}
`
+235
View File
@@ -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))
}
}
}
+771
View File
@@ -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))
}
+50
View File
@@ -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
}
+179
View File
@@ -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
}
+104
View File
@@ -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)
}
+29
View File
@@ -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
}
+238
View File
@@ -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
}
+145
View File
@@ -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
}
+51
View File
@@ -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
+141
View File
@@ -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
}
+158
View File
@@ -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))
}
}
+211
View File
@@ -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
}
+77
View File
@@ -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"`
}
)
+416
View File
@@ -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
}
+207
View File
@@ -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()
}
+176
View File
@@ -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
}
+49
View File
@@ -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
}
+147
View File
@@ -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
}