Refactor plugin & version subcommands (#1359)

This commit is contained in:
Ta-Ching Chen
2019-10-22 17:57:56 +08:00
committed by GitHub
parent 22de11190c
commit fed30e1b24
10 changed files with 118 additions and 87 deletions
@@ -23,7 +23,6 @@ import (
"strings"
"time"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/urfave/cli"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,12 +31,13 @@ import (
"github.com/fission/fission/pkg/fission-cli/cmd"
"github.com/fission/fission/pkg/fission-cli/cmd/environment"
_package "github.com/fission/fission/pkg/fission-cli/cmd/package"
plugincmd "github.com/fission/fission/pkg/fission-cli/cmd/plugin"
"github.com/fission/fission/pkg/fission-cli/cmd/spec"
"github.com/fission/fission/pkg/fission-cli/cmd/support"
"github.com/fission/fission/pkg/fission-cli/cmd/version"
"github.com/fission/fission/pkg/fission-cli/log"
"github.com/fission/fission/pkg/fission-cli/plugin"
"github.com/fission/fission/pkg/fission-cli/util"
"github.com/fission/fission/pkg/info"
"github.com/fission/fission/pkg/plugin"
"github.com/fission/fission/pkg/types"
)
@@ -59,16 +59,15 @@ func NewCliApp() *cli.App {
app := cli.NewApp()
app.Name = "fission"
app.Usage = "Serverless functions for Kubernetes"
app.Version = info.Version
cli.VersionPrinter = versionPrinter
app.HideVersion = true
app.CustomAppHelpTemplate = helpTemplate
app.ExtraInfo = func() map[string]string {
info := map[string]string{}
pluginInfo := map[string]string{}
for _, pmd := range plugin.FindAll() {
names := strings.Join(append([]string{pmd.Name}, pmd.Aliases...), ", ")
info[names] = pmd.Usage
pluginInfo[names] = pmd.Usage
}
return info
return pluginInfo
}
app.Flags = []cli.Flag{
@@ -312,6 +311,10 @@ func NewCliApp() *cli.App {
{Name: "list", Usage: "List all canary configs in a namespace", Flags: []cli.Flag{canaryNamespaceFlag}, Action: canaryConfigList},
}
pluginSubCommands := []cli.Command{
{Name: "list", Usage: "List installed client plugins", Action: urfavecli.Wrapper(plugincmd.List)},
}
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},
@@ -325,8 +328,9 @@ func NewCliApp() *cli.App {
{Name: "package", Aliases: []string{"pkg"}, Usage: "Manage packages", Subcommands: pkgSubCommands},
{Name: "spec", Aliases: []string{"specs"}, Usage: "Manage a declarative app specification", Subcommands: specSubCommands},
{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},
{Name: "plugin", Aliases: []string{"plugins"}, Usage: "Manage Fission CLI plugins", Subcommands: pluginSubCommands},
{Name: "version", Usage: "Version information", Action: urfavecli.Wrapper(version.Version)},
}
app.Before = cliHook
@@ -335,10 +339,6 @@ func NewCliApp() *cli.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,
@@ -399,16 +399,6 @@ To install it for your local Fission CLI:
}
}
func versionPrinter(_ *cli.Context) {
client := util.GetApiClient(util.GetServerUrl())
ver := util.GetVersion(client)
bs, err := yaml.Marshal(ver)
if err != nil {
log.Fatal("Error formatting versions: " + err.Error())
}
fmt.Print(string(bs))
}
func flagValueParser(args []string) error {
// all input value for flags are properly set
if len(args) == 0 {
@@ -1,5 +1,5 @@
/*
Copyright 2016 The Fission Authors.
Copyright 2019 The Fission Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,32 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package fission_cli
package plugin
import (
"fmt"
"os"
"text/tabwriter"
"github.com/urfave/cli"
"github.com/fission/fission/pkg/fission-cli/plugin"
"github.com/fission/fission/pkg/controller/client"
"github.com/fission/fission/pkg/fission-cli/cliwrapper/cli"
"github.com/fission/fission/pkg/fission-cli/cmd"
"github.com/fission/fission/pkg/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,
},
},
type ListSubCommand struct {
client *client.Client
}
func pluginList(_ *cli.Context) error {
func List(flags cli.Input) error {
opts := &ListSubCommand{
client: cmd.GetServer(flags),
}
return opts.do(flags)
}
func (opts *ListSubCommand) do(flags cli.Input) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
fmt.Fprintln(w, "NAME\tVERSION\tPATH")
for _, p := range plugin.FindAll() {
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright 2019 The Fission Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package version
import (
"fmt"
"github.com/ghodss/yaml"
"github.com/fission/fission/pkg/controller/client"
"github.com/fission/fission/pkg/fission-cli/cliwrapper/cli"
"github.com/fission/fission/pkg/fission-cli/cmd"
"github.com/fission/fission/pkg/fission-cli/log"
"github.com/fission/fission/pkg/fission-cli/util"
)
type VersionSubCommand struct {
client *client.Client
}
func Version(flags cli.Input) error {
opts := &VersionSubCommand{
client: cmd.GetServer(flags),
}
return opts.do(flags)
}
func (opts *VersionSubCommand) do(flags cli.Input) error {
ver := util.GetVersion(opts.client)
bs, err := yaml.Marshal(ver)
if err != nil {
log.Fatal("Error formatting versions: " + err.Error())
}
fmt.Print(string(bs))
return nil
}
-179
View File
@@ -1,179 +0,0 @@
/*
Copyright 2016 The Fission Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package 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
@@ -1,104 +0,0 @@
/*
Copyright 2016 The Fission Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
@@ -1,29 +0,0 @@
/*
Copyright 2016 The Fission Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
}
+32
View File
@@ -31,6 +31,8 @@ import (
"github.com/fission/fission/pkg/controller/client"
"github.com/fission/fission/pkg/fission-cli/log"
"github.com/fission/fission/pkg/info"
"github.com/fission/fission/pkg/plugin"
)
func GetApiClient(serverUrl string) *client.Client {
@@ -180,3 +182,33 @@ func CheckFunctionExistence(fissionClient *client.Client, functions []string, fn
return nil
}
func GetVersion(client *client.Client) info.Versions {
// Fetch client versions
versions := info.Versions{
Client: map[string]info.BuildMeta{
"fission/core": info.BuildInfo(),
},
}
for _, pmd := range plugin.FindAll() {
versions.Client[pmd.Name] = info.BuildMeta{
Version: pmd.Version,
}
}
serverInfo, err := client.ServerInfo()
if err != nil {
log.Warn(fmt.Sprintf("Error getting Fission API version: %v", err))
serverInfo = &info.ServerInfo{}
}
// Fetch server versions
versions.Server = map[string]info.BuildMeta{
"fission/core": serverInfo.Build,
}
// FUTURE: fetch versions of plugins server-side
return versions
}
-46
View File
@@ -1,46 +0,0 @@
package util
import (
"fmt"
"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) Versions {
// 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,
}
}
serverInfo, err := client.ServerInfo()
if err != nil {
log.Warn(fmt.Sprintf("Error getting Fission API version: %v", err))
serverInfo = &info.ServerInfo{}
}
// Fetch server versions
versions.Server = map[string]info.BuildMeta{
"fission/core": serverInfo.Build,
}
// FUTURE: fetch versions of plugins server-side
return versions
}