diff --git a/Makefile b/Makefile index 635266c..38dc5e9 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ all: build fmt vet lint test coverage default: build fmt vet lint test ALL_PACKAGES=$(shell go list ./... | grep -v "vendor") -APP_EXECUTABLE="out/weaver-server" +APP_EXECUTABLE="out/weaver" COMMIT_HASH=$(shell git rev-parse --verify head | cut -c-1-8) BUILD_DATE=$(shell date +%Y-%m-%dT%H:%M:%S%z) @@ -14,7 +14,7 @@ setup: compile: mkdir -p out/ - GO111MODULE=on go build -o $(APP_EXECUTABLE) -ldflags "-X main.BuildDate=$(BUILD_DATE) -X main.Commit=$(COMMIT_HASH) -s -w" ./cmd/weaver-server + GO111MODULE=on go build -o $(APP_EXECUTABLE) -ldflags "-X main.BuildDate=$(BUILD_DATE) -X main.Commit=$(COMMIT_HASH) -s -w" ./cmd/weaver build: deps compile fmt vet lint diff --git a/cmd/weaver-server/main.go b/cmd/weaver-server/main.go deleted file mode 100644 index 2d7c3ef..0000000 --- a/cmd/weaver-server/main.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "os/signal" - "syscall" - - raven "github.com/getsentry/raven-go" - "github.com/gojektech/weaver/config" - "github.com/gojektech/weaver/etcd" - "github.com/gojektech/weaver/pkg/instrumentation" - "github.com/gojektech/weaver/pkg/logger" - "github.com/gojektech/weaver/server" - cli "gopkg.in/urfave/cli.v1" -) - -func main() { - app := cli.NewApp() - app.Name = "weaver" - app.Usage = "run weaver-server" - app.Version = fmt.Sprintf("%s built on %s (commit: %s)", Version, BuildDate, Commit) - app.Description = "An Advanced HTTP Reverse Proxy with Dynamic Sharding Strategies" - app.Commands = []cli.Command{ - { - Name: "start", - Description: "Start weaver server", - Action: startWeaver, - }, - } - - app.Run(os.Args) -} - -func startWeaver(_ *cli.Context) error { - sigC := make(chan os.Signal, 1) - signal.Notify(sigC, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM) - - config.Load() - - raven.SetDSN(config.SentryDSN()) - logger.SetupLogger() - - err := instrumentation.InitiateStatsDMetrics() - if err != nil { - log.Printf("StatsD: Error initiating client %s", err) - } - defer instrumentation.CloseStatsDClient() - - instrumentation.InitNewRelic() - defer instrumentation.ShutdownNewRelic() - - routeLoader, err := etcd.NewRouteLoader() - if err != nil { - log.Printf("StartServer: failed to initialise etcd route loader: %s", err) - } - - ctx, cancel := context.WithCancel(context.Background()) - go server.StartServer(ctx, routeLoader) - - sig := <-sigC - log.Printf("Received %d, shutting down", sig) - - defer cancel() - server.ShutdownServer(ctx) - - return nil -} - -// Build information (will be injected during build) -var ( - Version = "1.0.0" - Commit = "n/a" - BuildDate = "n/a" -) diff --git a/cmd/weaver/main.go b/cmd/weaver/main.go new file mode 100644 index 0000000..a699321 --- /dev/null +++ b/cmd/weaver/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "github.com/gojektech/weaver/internal/cli" + _ "github.com/gojektech/weaver/internal/commands" + _ "github.com/gojektech/weaver/internal/commands/acls" + _ "github.com/gojektech/weaver/internal/commands/server" + "os" +) + +func main() { + app := cli.NewApp() + app.Name = "Weaver" + app.Version = fmt.Sprintf("%s built on %s (commit: %s)", Version, BuildDate, Commit) + app.Usage = "Start Server, Perform CRUD on ACLs" + app.Description = "An Advanced HTTP Reverse Proxy with Dynamic Sharding Strategies" + app.Commands = cli.GetBaseCommands() + app.Run(os.Args) +} + +// Build information (will be injected during build) +var ( + Version = "1.0.0" + Commit = "n/a" + BuildDate = "n/a" +) diff --git a/etcd/routeloader.go b/etcd/routeloader.go index b828437..61ee8ab 100644 --- a/etcd/routeloader.go +++ b/etcd/routeloader.go @@ -33,6 +33,29 @@ type RouteLoader struct { namespace string } +// ListAll - List all valid weaver acls +func (routeLoader *RouteLoader) ListAll() ([]*weaver.ACL, error) { + keysAPI, key := initEtcd(routeLoader) + res, err := keysAPI.Get(context.Background(), key, nil) + if err != nil { + return nil, fmt.Errorf("fail to LIST %s with %s", key, err) + } + acls := []*weaver.ACL{} + sort.Sort(res.Node.Nodes) + for _, nd := range res.Node.Nodes { + logger.Debugf("fetching node key %s", nd.Key) + aclKey := GenACLKey(nd.Key) + acl, err := routeLoader.GetACL(aclKey) + if err != nil { + logger.Errorf("error in fetching %s: %v", nd.Key, err) + } else { + acls = append(acls, acl) + + } + } + return acls, nil +} + // PutACL - Upserts a given ACL func (routeLoader *RouteLoader) PutACL(acl *weaver.ACL) (ACLKey, error) { key := GenKey(acl, routeLoader.namespace) @@ -81,8 +104,8 @@ func (routeLoader *RouteLoader) DelACL(key ACLKey) error { } func (routeLoader *RouteLoader) WatchRoutes(ctx context.Context, upsertRouteFunc server.UpsertRouteFunc, deleteRouteFunc server.DeleteRouteFunc) { - etc, key := initEtcd(routeLoader) - watcher := etc.Watcher(key, &etcd.WatcherOptions{Recursive: true}) + keysAPI, key := initEtcd(routeLoader) + watcher := keysAPI.Watcher(key, &etcd.WatcherOptions{Recursive: true}) logger.Infof("starting etcd watcher on %s", key) for { @@ -130,12 +153,12 @@ func (routeLoader *RouteLoader) WatchRoutes(ctx context.Context, upsertRouteFunc func (routeLoader *RouteLoader) BootstrapRoutes(ctx context.Context, upsertRouteFunc server.UpsertRouteFunc) error { // TODO: Consider error scenarios and return an error when it makes sense - etc, key := initEtcd(routeLoader) + keysAPI, key := initEtcd(routeLoader) logger.Infof("bootstrapping router using etcd on %s", key) - res, err := etc.Get(ctx, key, nil) + res, err := keysAPI.Get(ctx, key, nil) if err != nil { logger.Infof("creating router namespace on etcd using %s", key) - _, _ = etc.Set(ctx, key, "", &etcd.SetOptions{ + _, _ = keysAPI.Set(ctx, key, "", &etcd.SetOptions{ Dir: true, }) } @@ -164,7 +187,7 @@ func (routeLoader *RouteLoader) BootstrapRoutes(ctx context.Context, upsertRoute func initEtcd(routeLoader *RouteLoader) (etcd.KeysAPI, string) { key := fmt.Sprintf("/%s/acls/", routeLoader.namespace) - etc := etcd.NewKeysAPI(routeLoader.etcdClient) + keysAPI := etcd.NewKeysAPI(routeLoader.etcdClient) - return etc, key + return keysAPI, key } diff --git a/etcd/routeloader_test.go b/etcd/routeloader_test.go index 82a8a4d..66c535a 100644 --- a/etcd/routeloader_test.go +++ b/etcd/routeloader_test.go @@ -43,6 +43,28 @@ func TestRouteLoaderSuite(tst *testing.T) { suite.Run(tst, new(RouteLoaderSuite)) } +func (es *RouteLoaderSuite) TestListAll() { + aclPut := &weaver.ACL{ + ID: "svc-01", + Criterion: "Method(`GET`) && Path(`/ping`)", + EndpointConfig: &weaver.EndpointConfig{ + ShardFunc: "lookup", + Matcher: "path", + ShardExpr: "*", + ShardConfig: json.RawMessage(`{}`), + }, + } + + key, err := es.ng.PutACL(aclPut) + assert.NoError(es.T(), err, "failed to PUT %s", aclPut) + + aclList, err := es.ng.ListAll() + assert.Nil(es.T(), err, "fail to ListAll ACLs") + + deepEqual(es.T(), aclPut, aclList[0]) + assert.Nil(es.T(), es.ng.DelACL(key), "fail to DELETE %+v", aclPut) +} + func (es *RouteLoaderSuite) TestPutACL() { aclPut := &weaver.ACL{ ID: "svc-01", diff --git a/go.mod b/go.mod index 90fdae9..ceb3df8 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,9 @@ require ( github.com/vulcand/route v0.0.0-20160805191529-61904570391b github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect go.etcd.io/bbolt v1.3.2 // indirect - golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 // indirect + golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576 // indirect + golang.org/x/net v0.0.0-20190322120337-addf6b3196f6 // indirect + golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect google.golang.org/grpc v1.18.0 // indirect gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect diff --git a/go.sum b/go.sum index 181fafa..8b283fe 100644 --- a/go.sum +++ b/go.sum @@ -131,13 +131,16 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc= -golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576 h1:aUX/1G2gFSs4AsJJg2cL3HuoRhCSCz733FE5GUSuaT4= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190322120337-addf6b3196f6 h1:78jEq2G3J16aXneH23HSnTQQTCwMHoyO8VEiUH+bpPM= +golang.org/x/net v0.0.0-20190322120337-addf6b3196f6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= @@ -145,6 +148,9 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g= +golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..8787128 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,80 @@ +package cli + +import ( + "fmt" + "github.com/gojektech/weaver/config" + "github.com/gojektech/weaver/etcd" + "github.com/gojektech/weaver/pkg/logger" + baseCli "gopkg.in/urfave/cli.v1" + "os" +) + +var registeredCommands = Commands{} + +type Context struct { + RouteLoader *etcd.RouteLoader + *baseCli.Context +} + +func RegisterAsBaseCommand(cmd *Command) error { + cliHandler := cmd.CliHandler() + for _, eachCmd := range registeredCommands { + if eachCmd.CliHandler() == cliHandler { + return fmt.Errorf("Another Command Regsitered for Cli Handler: %s", cliHandler) + } + } + registeredCommands = append(registeredCommands, cmd) + return nil +} + +func GetBaseCommands() []baseCli.Command { + return getBaseCommandWithActionWrapper(registeredCommands, setup) +} + +func getBaseCommandWithActionWrapper(cmds Commands, fn cmdAction) []baseCli.Command { + if fn == nil { + fn = func(c *Context) error { return nil } + } + + baseCliCommands := []baseCli.Command{} + for idx, _ := range cmds { + eachCmd := cmds[idx] + baseCmd := baseCli.Command{ + Name: eachCmd.name, + Usage: eachCmd.usage, + Description: eachCmd.description, + Flags: eachCmd.flags, + } + if eachCmd.isParentCommand { + baseCmd.Subcommands = getBaseCommandWithActionWrapper( + eachCmd.subCommands, + func(c *Context) error { + fn(c) + eachCmd.Exec(c) + return nil + }, + ) + } else { + baseCmd.Action = func(ctx *baseCli.Context) error { + c := &Context{Context: ctx} + fn(c) + return eachCmd.Exec(c) + } + } + baseCliCommands = append(baseCliCommands, baseCmd) + } + return baseCliCommands +} + +func setup(c *Context) error { + os.Setenv("LOGGER_LEVEL", c.GlobalString("verbose")) + config.Load() + logger.SetupLogger() + return nil +} + +func NewApp() *baseCli.App { + app := baseCli.NewApp() + app.Flags = []baseCli.Flag{NewStringFlag("verbose", "Error", "Verbosity of log level, ex: debug, info, warn, error, fatal, panic", "LOGGER_LEVEL")} + return app +} diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go new file mode 100644 index 0000000..4a48923 --- /dev/null +++ b/internal/cli/cli_test.go @@ -0,0 +1,103 @@ +package cli_test + +import ( + "fmt" + "github.com/gojektech/weaver/config" + "github.com/gojektech/weaver/internal/cli" + "github.com/gojektech/weaver/pkg/logger" + "github.com/stretchr/testify/assert" + "testing" +) + +type testAppSetup struct { + name, usage, description string + cmd *cli.Command +} + +func TestAppShouldRegisterACommand(t *testing.T) { + ts := setupTestApp() + err := cli.RegisterAsBaseCommand(ts.cmd) + assert.NoError(t, err) +} + +func TestAppShouldReturnErrorOnDuplicateRegistration(t *testing.T) { + // This will throw error as previous command is also registered with same cli handler + ts := setupTestApp() + err := cli.RegisterAsBaseCommand(ts.cmd) + + assert.Error(t, err) + assert.Equal(t, err, fmt.Errorf("Another Command Regsitered for Cli Handler: %s", ts.name)) +} + +func TestCliGetCommandsShouldGiveBaseCommands(t *testing.T) { + ts := setupTestApp() + baseCliCommands := cli.GetBaseCommands() + + assert.Equal(t, len(baseCliCommands), 1) + assert.Equal(t, baseCliCommands[0].Name, ts.name) + assert.Equal(t, baseCliCommands[0].Usage, ts.usage) + assert.Equal(t, baseCliCommands[0].Description, ts.description) +} + +func TestCliGetCommandsExecutionSHouldSetupConfigAndLogger(t *testing.T) { + ts := setupTestApp() + + baseCliCommands := cli.GetBaseCommands() + app := cli.NewApp() + app.Commands = baseCliCommands + app.Run([]string{"binary", "--verbose", "debug", ts.name}) + + // config is supposed to have logger level set + assert.Equal(t, config.LogLevel(), "debug") + + // If it is setup logger, logging shouldn't panic + msg := "Should not panic if logger is setup" + assert.NotPanics(t, func() { logger.Info(msg) }) +} + +func TestAppRunWithOSArgsShouldExecuteBaseCommandAction(t *testing.T) { + isCmdActionExecuted := false + ts := setupTestApp() + ts.name = "exec-test" + ts.cmd = cli.NewDefaultCommand(ts.name, ts.usage, ts.description, func(c *cli.Context) error { isCmdActionExecuted = true; return nil }) + err := cli.RegisterAsBaseCommand(ts.cmd) + assert.NoError(t, err) + + baseCliCommands := cli.GetBaseCommands() + app := cli.NewApp() + app.Commands = baseCliCommands + app.Run([]string{"binary", "--verbose", "debug", ts.name}) + + assert.True(t, isCmdActionExecuted) +} + +func TestAppRunWithOSArgsShouldExecuteParentsSubCommand(t *testing.T) { + isParentActionExecuted := false + isCmdActionExecuted := false + ts := setupTestApp() + parentCmd := cli.NewParentCommandWithAction("parent", "parent-usage", "parent-desc", func(c *cli.Context) error { isParentActionExecuted = true; return nil }) + + ts.name = "exec-test-sub-command" + ts.cmd = cli.NewDefaultCommand(ts.name, ts.usage, ts.description, func(c *cli.Context) error { isCmdActionExecuted = true; return nil }) + + errFromBaseCommandRegistration := cli.RegisterAsBaseCommand(parentCmd) + assert.NoError(t, errFromBaseCommandRegistration) + + errFromSubCommandRegistration := parentCmd.RegisterCommand(ts.cmd) + assert.NoError(t, errFromSubCommandRegistration) + + baseCliCommands := cli.GetBaseCommands() + app := cli.NewApp() + app.Commands = baseCliCommands + app.Run([]string{"binary", "--verbose", "debug", "parent", ts.name}) + + assert.True(t, isParentActionExecuted) + assert.True(t, isCmdActionExecuted) +} + +func setupTestApp() *testAppSetup { + setup := &testAppSetup{name: "test", usage: "usage", description: "description"} + action := func(c *cli.Context) error { return nil } + setup.cmd = cli.NewDefaultCommand(setup.name, setup.usage, setup.description, action) + return setup +} diff --git a/internal/cli/command.go b/internal/cli/command.go new file mode 100644 index 0000000..b60f432 --- /dev/null +++ b/internal/cli/command.go @@ -0,0 +1,73 @@ +package cli + +import ( + "fmt" + baseCli "gopkg.in/urfave/cli.v1" + "strings" +) + +type cmdAction func(c *Context) error + +type Command struct { + name string + action cmdAction + usage string + description string + subCommands Commands + flags []baseCli.Flag + isParentCommand bool +} + +type Commands []*Command + +func (cmd *Command) CliHandler() string { + return cmd.name +} + +func (cmd *Command) SetFlag(flag baseCli.Flag) error { + cmd.flags = append(cmd.flags, flag) + return nil +} + +func (cmd *Command) Exec(c *Context) error { + if !cmd.isParentCommand { + return cmd.action(c) + } else { + cmd.action(c) + + cliHandler := strings.Split(c.Command.FullName(), " ")[0] + for _, eachCmd := range cmd.subCommands { + if eachCmd.CliHandler() == cliHandler { + return eachCmd.Exec(c) + } + } + } + return fmt.Errorf("No Command not registered for :%s", c.Command.FullName()) +} + +func (pc *Command) RegisterCommand(cmd *Command) error { + if pc.isParentCommand { + cliHandler := cmd.CliHandler() + for _, eachCmd := range pc.subCommands { + if eachCmd.CliHandler() == cliHandler { + return fmt.Errorf("Another Command Regsitered for Cli Handler: %s", cliHandler) + } + } + pc.subCommands = append(pc.subCommands, cmd) + return nil + } else { + return fmt.Errorf("Command Does Not Allow SubCommand Registration") + } +} + +func NewDefaultCommand(name, usage, description string, action cmdAction) *Command { + return &Command{name: name, usage: usage, description: description, action: action} +} + +func NewParentCommand(name, usage, description string) *Command { + return &Command{name: name, usage: usage, description: description, subCommands: Commands{}, isParentCommand: true, action: func(c *Context) error { return nil }} +} + +func NewParentCommandWithAction(name, usage, description string, action cmdAction) *Command { + return &Command{name: name, usage: usage, description: description, subCommands: Commands{}, isParentCommand: true, action: action} +} diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go new file mode 100644 index 0000000..5b225aa --- /dev/null +++ b/internal/cli/command_test.go @@ -0,0 +1,42 @@ +package cli_test + +import ( + "github.com/gojektech/weaver/internal/cli" + "github.com/stretchr/testify/assert" + "testing" +) + +type testCommandSetup struct { + name, usage, description string + isCalled bool + cmd *cli.Command +} + +func TestDefaultCommandInitialization(t *testing.T) { + ts := setupTestCommand() + assert.NotNil(t, ts.cmd) +} + +func TestCommandShouldHaveCliHandlerName(t *testing.T) { + ts := setupTestCommand() + assert.Equal(t, ts.cmd.CliHandler(), ts.name) +} + +func TestCommandShouldExecuteSpecifiedAction(t *testing.T) { + ts := setupTestCommand() + ts.cmd.Exec(&cli.Context{}) + assert.True(t, ts.isCalled) +} + +func TestShouldBeAbleToSetFlags(t *testing.T) { + ts := setupTestCommand() + flag := cli.NewStringFlag("test", "value", "usage", "env") + assert.NotPanics(t, func() { ts.cmd.SetFlag(flag) }, "Setting a flag panics") +} + +func setupTestCommand() *testCommandSetup { + setup := &testCommandSetup{name: "test", usage: "usage", description: "description", isCalled: false} + action := func(c *cli.Context) error { setup.isCalled = true; return nil } + setup.cmd = cli.NewDefaultCommand(setup.name, setup.usage, setup.description, action) + return setup +} diff --git a/internal/cli/flag.go b/internal/cli/flag.go new file mode 100644 index 0000000..1cbe63f --- /dev/null +++ b/internal/cli/flag.go @@ -0,0 +1,9 @@ +package cli + +import ( + baseCli "gopkg.in/urfave/cli.v1" +) + +func NewStringFlag(name, value, usage, env string) baseCli.Flag { + return baseCli.StringFlag{Name: name, Value: value, Usage: usage, EnvVar: env} +} diff --git a/internal/cli/flag_test.go b/internal/cli/flag_test.go new file mode 100644 index 0000000..f9fa044 --- /dev/null +++ b/internal/cli/flag_test.go @@ -0,0 +1,26 @@ +package cli_test + +import ( + "fmt" + "github.com/gojektech/weaver/internal/cli" + "github.com/stretchr/testify/assert" + baseCli "gopkg.in/urfave/cli.v1" + "testing" +) + +type testFlagSetup struct { + name, value, usage, env string + flag baseCli.Flag +} + +func TestInitializationOfStringFlag(t *testing.T) { + ts := setupTestFlag() + + assert.Equal(t, ts.flag.String(), fmt.Sprintf("--%s value\t%s (default: \"%s\") [$%s]", ts.name, ts.usage, ts.value, ts.env)) +} + +func setupTestFlag() *testFlagSetup { + setup := &testFlagSetup{name: "test", value: "default", usage: "use to set value for test", env: "TEST"} + setup.flag = cli.NewStringFlag(setup.name, setup.value, setup.usage, setup.env) + return setup +} diff --git a/internal/cli/parent_command_test.go b/internal/cli/parent_command_test.go new file mode 100644 index 0000000..2c0df91 --- /dev/null +++ b/internal/cli/parent_command_test.go @@ -0,0 +1,84 @@ +package cli_test + +import ( + "fmt" + "github.com/gojektech/weaver/internal/cli" + "github.com/stretchr/testify/assert" + baseCli "gopkg.in/urfave/cli.v1" + "testing" +) + +type testParentCommandSetup struct { + name, usage, description string + cmd *cli.Command +} + +func TestParentCommandInitialization(t *testing.T) { + ts := setupTestParentCommand() + assert.NotNil(t, ts.cmd) +} + +func TestParentCommandShouldBeAbleToInitializeWithAction(t *testing.T) { + ts := setupTestParentCommandWithAction() + assert.NotNil(t, ts.cmd) +} + +func TestParentCommandShouldRegisterCommand(t *testing.T) { + ts := setupTestParentCommand() + cmd := cli.NewDefaultCommand("test", "usage", "description", func(c *cli.Context) error { return nil }) + assert.NoError(t, ts.cmd.RegisterCommand(cmd)) +} + +func TestParentCommandShouldNotAllowMoreThanOneCommandPerCliHandler(t *testing.T) { + cliHandler := "test" + ts := setupTestParentCommand() + cmd := cli.NewDefaultCommand(cliHandler, "usage", "description", func(c *cli.Context) error { return nil }) + assert.NoError(t, ts.cmd.RegisterCommand(cmd)) + err := ts.cmd.RegisterCommand(cmd) + assert.Error(t, err) + assert.Equal(t, err, fmt.Errorf("Another Command Regsitered for Cli Handler: %s", cliHandler)) +} + +func TestParentCommandShouldExecuteSubCommand(t *testing.T) { + ts := setupTestParentCommand() + isCmdOneCalled := false + isCmdTwoCalled := false + cmdOne := cli.NewDefaultCommand("test-one", "usage", "description", func(c *cli.Context) error { isCmdOneCalled = true; return nil }) + cmdTwo := cli.NewDefaultCommand("test-two", "usage", "description", func(c *cli.Context) error { isCmdTwoCalled = true; return nil }) + errFromCmdOne := ts.cmd.RegisterCommand(cmdOne) + errFromCmdTwo := ts.cmd.RegisterCommand(cmdTwo) + + ctx := &cli.Context{Context: &baseCli.Context{Command: baseCli.Command{Name: "test-one"}}} + ts.cmd.Exec(ctx) + + assert.NoError(t, errFromCmdOne) + assert.NoError(t, errFromCmdTwo) + assert.True(t, isCmdOneCalled) + assert.False(t, isCmdTwoCalled) +} + +func TestParentCommandBeforeActionShouldBeCalledFirst(t *testing.T) { + ts := setupTestParentCommandWithAction() + orderOfExecution := []string{} + ts.cmd = cli.NewParentCommandWithAction(ts.name, ts.usage, ts.description, func(c *cli.Context) error { orderOfExecution = append(orderOfExecution, "parent"); return nil }) + cmdOne := cli.NewDefaultCommand("test-one", "usage", "description", func(c *cli.Context) error { orderOfExecution = append(orderOfExecution, "child"); return nil }) + errFromCmdOne := ts.cmd.RegisterCommand(cmdOne) + + ctx := &cli.Context{Context: &baseCli.Context{Command: baseCli.Command{Name: "test-one"}}} + ts.cmd.Exec(ctx) + + assert.NoError(t, errFromCmdOne) + assert.Equal(t, orderOfExecution, []string{"parent", "child"}) +} + +func setupTestParentCommand() *testParentCommandSetup { + ts := &testParentCommandSetup{name: "parent", usage: "parent usage", description: "parent description"} + ts.cmd = cli.NewParentCommand(ts.name, ts.usage, ts.description) + return ts +} + +func setupTestParentCommandWithAction() *testParentCommandSetup { + ts := &testParentCommandSetup{name: "parent", usage: "parent usage", description: "parent description"} + ts.cmd = cli.NewParentCommandWithAction(ts.name, ts.usage, ts.description, func(c *cli.Context) error { return nil }) + return ts +} diff --git a/internal/commands/acls/acl.go b/internal/commands/acls/acl.go new file mode 100644 index 0000000..bbeb200 --- /dev/null +++ b/internal/commands/acls/acl.go @@ -0,0 +1,39 @@ +package acls + +import ( + "github.com/gojektech/weaver/config" + "github.com/gojektech/weaver/etcd" + "github.com/gojektech/weaver/internal/cli" + "github.com/gojektech/weaver/internal/commands" + "github.com/gojektech/weaver/pkg/logger" + "os" +) + +const ( + aclsCmdName = "acls" + aclsCmdUsage = "ACLs - Perform CRUD Operations" + aclsCmdDescription = "ACLs - Perform CRUD Operations" +) + +var weaverACLSCmd = cli.NewParentCommandWithAction(aclsCmdName, aclsCmdUsage, aclsCmdDescription, setupACLs) + +func setupACLs(c *cli.Context) error { + os.Setenv("ETCD_ENDPOINTS", c.GlobalString("etcd-host")) + os.Setenv("ETCD_KEY_PREFIX", c.GlobalString("namespace")) + config.Load() + rl, err := etcd.NewRouteLoader() + + if err != nil { + logger.Fatalf("Couldn't create route loader: %s", err) + os.Exit(1) + } + + c.RouteLoader = rl + return nil +} + +func init() { + weaverACLSCmd.SetFlag(commands.ETCDFLAG) + weaverACLSCmd.SetFlag(commands.NAMESPACEFLAG) + cli.RegisterAsBaseCommand(weaverACLSCmd) +} diff --git a/internal/commands/acls/list.go b/internal/commands/acls/list.go new file mode 100644 index 0000000..94c4131 --- /dev/null +++ b/internal/commands/acls/list.go @@ -0,0 +1,40 @@ +package acls + +import ( + "github.com/gojektech/weaver/internal/cli" + "github.com/gojektech/weaver/internal/views" + "github.com/gojektech/weaver/pkg/logger" +) + +const ( + listCmdName = "list" + listCmdUsage = "List Weaver ACLS in ETCD Under a Namespace" + listCmdDescription = "List Weaver ACLS in ETCD Under a Namespace" +) + +var aclListCmd = cli.NewDefaultCommand(listCmdName, listCmdUsage, listCmdDescription, listACL) + +func listACL(c *cli.Context) error { + acls, err := c.RouteLoader.ListAll() + if err != nil { + logger.Fatalf("Error while listing acls: %s", err) + } + + type aclInfo struct { + ID string `json:"ACL ID"` + Criterion string `json:"Criterion"` + } + + formattedAcls := []aclInfo{} + + for _, eachACL := range acls { + formattedAcls = append(formattedAcls, aclInfo{eachACL.ID, eachACL.Criterion}) + } + + views.Render(formattedAcls) + return nil +} + +func init() { + weaverACLSCmd.RegisterCommand(aclListCmd) +} diff --git a/internal/commands/acls/show.go b/internal/commands/acls/show.go new file mode 100644 index 0000000..c02b791 --- /dev/null +++ b/internal/commands/acls/show.go @@ -0,0 +1,44 @@ +package acls + +import ( + baseCli "gopkg.in/urfave/cli.v1" + + "github.com/gojektech/weaver/internal/cli" + "github.com/gojektech/weaver/internal/views" + "github.com/gojektech/weaver/pkg/logger" +) + +const ( + showCmdName = "show" + showCmdUsage = "Show Weaver ACLS Given ACL ID" + showCmdDescription = "Show Weaver ACLS Given ACL ID" +) + +var aclShowCmd = cli.NewDefaultCommand(showCmdName, showCmdUsage, showCmdDescription, showACL) + +func showACL(c *cli.Context) error { + aclID := c.String("id") + if aclID == "" { + baseCli.ShowSubcommandHelp(c.Context) + return nil + } + acls, err := c.RouteLoader.ListAll() + if err != nil { + logger.Fatalf("Error while showing acls: %s", err) + } + + for _, eachACL := range acls { + if eachACL.ID == aclID { + views.Render(eachACL) + return nil + } + + } + logger.Fatalf("ACL with id: %s not found", aclID) + return nil +} + +func init() { + aclShowCmd.SetFlag(cli.NewStringFlag("id", "", "ID Of the ACL", "")) + weaverACLSCmd.RegisterCommand(aclShowCmd) +} diff --git a/internal/commands/commands.go b/internal/commands/commands.go new file mode 100644 index 0000000..71e6101 --- /dev/null +++ b/internal/commands/commands.go @@ -0,0 +1,8 @@ +package commands + +import ( + "github.com/gojektech/weaver/internal/cli" +) + +var ETCDFLAG = cli.NewStringFlag("etcd-host, etcd", "http://localhost:2379", "HOST Address of ETCD", "ETCD_ENDPOINTS") +var NAMESPACEFLAG = cli.NewStringFlag("namespace, ns", "weaver", "Namespace of Weaver ACLS", "ETCD_KEY_PREFIX") diff --git a/internal/commands/server/server.go b/internal/commands/server/server.go new file mode 100644 index 0000000..0f7cc4e --- /dev/null +++ b/internal/commands/server/server.go @@ -0,0 +1,39 @@ +package server + +import ( + "github.com/gojektech/weaver/config" + "github.com/gojektech/weaver/etcd" + "github.com/gojektech/weaver/internal/cli" + "github.com/gojektech/weaver/internal/commands" + "github.com/gojektech/weaver/pkg/logger" + "os" +) + +const ( + serverCmdName = "server" + serverCmdUsage = "Weaver - Run Server" + serverCmdDescription = "Weaver - Run Server" +) + +var weaverServerCmd = cli.NewParentCommandWithAction(serverCmdName, serverCmdUsage, serverCmdDescription, setupServer) + +func setupServer(c *cli.Context) error { + os.Setenv("ETCD_ENDPOINTS", c.GlobalString("etcd-host")) + os.Setenv("ETCD_KEY_PREFIX", c.GlobalString("namespace")) + config.Load() + rl, err := etcd.NewRouteLoader() + + if err != nil { + logger.Fatalf("Couldn't create route loader: %s", err) + os.Exit(1) + } + + c.RouteLoader = rl + return nil +} + +func init() { + weaverServerCmd.SetFlag(commands.ETCDFLAG) + weaverServerCmd.SetFlag(commands.NAMESPACEFLAG) + cli.RegisterAsBaseCommand(weaverServerCmd) +} diff --git a/internal/commands/server/start.go b/internal/commands/server/start.go new file mode 100644 index 0000000..eee0171 --- /dev/null +++ b/internal/commands/server/start.go @@ -0,0 +1,39 @@ +package server + +import ( + "context" + "github.com/gojektech/weaver/internal/cli" + "github.com/gojektech/weaver/pkg/logger" + "github.com/gojektech/weaver/server" + "os" + "os/signal" + "syscall" +) + +const ( + startCmdName = "start" + startCmdUsage = "Run Weaver server" + startCmdDescription = "Run Weaver server" +) + +var serverStartCmd = cli.NewDefaultCommand(startCmdName, startCmdUsage, startCmdDescription, startServer) + +func startServer(c *cli.Context) error { + sigC := make(chan os.Signal, 1) + signal.Notify(sigC, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM) + + ctx, cancel := context.WithCancel(context.Background()) + go server.StartServer(ctx, c.RouteLoader) + + sig := <-sigC + logger.Infof("Received %d, shutting down", sig) + + defer cancel() + server.ShutdownServer(ctx) + + return nil +} + +func init() { + weaverServerCmd.RegisterCommand(serverStartCmd) +} diff --git a/internal/commands/server/stop.go b/internal/commands/server/stop.go new file mode 100644 index 0000000..abb4e43 --- /dev/null +++ b/internal/commands/server/stop.go @@ -0,0 +1 @@ +package server diff --git a/internal/views/views.go b/internal/views/views.go new file mode 100644 index 0000000..4b21dd2 --- /dev/null +++ b/internal/views/views.go @@ -0,0 +1,22 @@ +package views + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/gojektech/weaver/pkg/logger" +) + +func Render(o interface{}) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + + err := encoder.Encode(o) + if err != nil { + logger.Fatalf("Error marshaling output: %s", err) + panic(err) + } + fmt.Print(string(buffer.Bytes())) +} diff --git a/internal/views/views_test.go b/internal/views/views_test.go new file mode 100644 index 0000000..3964c19 --- /dev/null +++ b/internal/views/views_test.go @@ -0,0 +1,34 @@ +package views_test + +import ( + "github.com/gojektech/weaver/internal/views" + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "testing" +) + +func TestShouldPrettyPrintAnyJsonEnabledStruct(t *testing.T) { + realStdout := os.Stdout + reader, fakeStdout, err := os.Pipe() + assert.NoError(t, err, "Error in setting fake stdout") + + os.Stdout = fakeStdout + defer func() { os.Stdout = realStdout }() + views.Render(struct { + Name string `json:"name"` + Age int `json:"age"` + }{"gowtham", 23}) + + fakeStdoutCloseErr := fakeStdout.Close() + assert.NoError(t, fakeStdoutCloseErr, "Error close fake stdout") + + outputBuffer, err := ioutil.ReadAll(reader) + assert.NoError(t, err, "Error in reading output from fake stdout") + assert.Equal(t, string(outputBuffer), + `{ + "name": "gowtham", + "age": 23 +} +`) +}