diff --git a/.gitignore b/.gitignore index f90fd7219..719f863c6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,19 +2,21 @@ build .env .DS_Store .idea/ +.vscode/ proxies/*.yaml proxies/*.yml .temp temp shield !proto/* -.shield.yaml +/config.yaml dist coverage.txt coverage.out -rules/test.yaml *.pprof ignore/ vendor/ buf.lock buf.yaml +/resources_config +/rules \ No newline at end of file diff --git a/.shield.sample.yaml b/.shield.sample.yaml deleted file mode 100644 index fbf5a428d..000000000 --- a/.shield.sample.yaml +++ /dev/null @@ -1,30 +0,0 @@ -version: 1 - -# logging configuration -log: - # debug, info, warning, error, fatal - default 'info' - level: debug - -# proxy configuration -proxy: - services: - - name: test - host: 0.0.0.0 - # port where the proxy will be listening on for requests - port: 5556 - - # full path prefixed with scheme where ruleset yaml files are kept - # e.g.: - # local storage file "file:///tmp/rules" - # GCS Bucket "gs://shield-bucket-example" - ruleset: file://absolute_path_to_rules_directory - - # secret required to access ruleset - # e.g.: - # system environment variable "env://TEST_RULESET_SECRET" - # local file "file:///opt/auth.json" - # secret string "val://user:password" - # - # +optional - # ruleset_secret: env://TEST_RULESET_SECRET - resources_config_path: file://absolute_path_to_rules_directory \ No newline at end of file diff --git a/Makefile b/Makefile index a08ad8499..f548aa5f5 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ GOVERSION := $(shell go version | cut -d ' ' -f 3 | cut -d '.' -f 2) .PHONY: build check fmt lint test test-race vet test-cover-html help install proto .DEFAULT_GOAL := build +PROTON_COMMIT := "1497165f2f48facb3ec6f5c5556ccd44f0a7119f" install: @echo "Clean up imports..." @@ -26,8 +27,12 @@ coverage: ## print code coverage clean : rm -rf dist -proto: - ./buf.gen.yaml && cp -R proto/odpf/shield/* proto/ && rm -Rf proto/odpf +proto: ## Generate the protobuf files + @echo " > generating protobuf from odpf/proton" + @echo " > [info] make sure correct version of dependencies are installed using 'make install'" + @buf generate https://github.com/odpf/proton/archive/${PROTON_COMMIT}.zip#strip_components=1 --template buf.gen.yaml --path odpf/shield + @cp -R proto/odpf/shield/* proto/ && rm -Rf proto/odpf + @echo " > protobuf compilation finished" help: @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/buf.gen.yaml b/buf.gen.yaml index 2a618baa6..ddf0fa1fc 100755 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -1,4 +1,3 @@ -#!/usr/bin/env -S buf generate buf.build/odpf/proton:1497165f2f48facb3ec6f5c5556ccd44f0a7119f --path odpf/shield --template --- version: "v1" plugins: diff --git a/cmd/action.go b/cmd/action.go index 8bc640083..f5e90e881 100644 --- a/cmd/action.go +++ b/cmd/action.go @@ -1,20 +1,17 @@ package cmd import ( - "context" "fmt" "os" - "strconv" "github.com/MakeNowJust/heredoc" - "github.com/odpf/salt/log" "github.com/odpf/salt/printer" - "github.com/odpf/shield/config" + "github.com/odpf/shield/pkg/file" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" cli "github.com/spf13/cobra" ) -func ActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func ActionCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "action", Aliases: []string{"actions"}, @@ -29,19 +26,22 @@ func ActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { $ shield action list `), Annotations: map[string]string{ - "action:core": "true", + "group:core": "true", + "client": "true", }, } - cmd.AddCommand(createActionCommand(logger, appConfig)) - cmd.AddCommand(editActionCommand(logger, appConfig)) - cmd.AddCommand(viewActionCommand(logger, appConfig)) - cmd.AddCommand(listActionCommand(logger, appConfig)) + cmd.AddCommand(createActionCommand(cliConfig)) + cmd.AddCommand(editActionCommand(cliConfig)) + cmd.AddCommand(viewActionCommand(cliConfig)) + cmd.AddCommand(listActionCommand(cliConfig)) + + bindFlagsFromClientConfig(cmd) return cmd } -func createActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func createActionCommand(cliConfig *Config) *cli.Command { var filePath, header string cmd := &cli.Command{ @@ -59,7 +59,7 @@ func createActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma defer spinner.Stop() var reqBody shieldv1beta1.ActionRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -68,16 +68,13 @@ func createActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - ctx = setCtxHeader(ctx, header) - + ctx := setCtxHeader(cmd.Context(), header) res, err := client.CreateAction(ctx, &shieldv1beta1.CreateActionRequest{ Body: &reqBody, }) @@ -86,7 +83,7 @@ func createActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma } spinner.Stop() - logger.Info(fmt.Sprintf("successfully created action %s with id %s", res.GetAction().GetName(), res.GetAction().GetId())) + fmt.Printf("successfully created action %s with id %s\n", res.GetAction().GetName(), res.GetAction().GetId()) return nil }, } @@ -99,7 +96,7 @@ func createActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma return cmd } -func editActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func editActionCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -117,7 +114,7 @@ func editActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command defer spinner.Stop() var reqBody shieldv1beta1.ActionRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -126,16 +123,14 @@ func editActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() actionID := args[0] - _, err = client.UpdateAction(ctx, &shieldv1beta1.UpdateActionRequest{ + _, err = client.UpdateAction(cmd.Context(), &shieldv1beta1.UpdateActionRequest{ Id: actionID, Body: &reqBody, }) @@ -144,7 +139,7 @@ func editActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command } spinner.Stop() - logger.Info(fmt.Sprintf("successfully edited action with id %s", actionID)) + fmt.Printf("successfully edited action with id %s\n", actionID) return nil }, } @@ -155,7 +150,7 @@ func editActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return cmd } -func viewActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func viewActionCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "view", Short: "View an action", @@ -170,16 +165,14 @@ func viewActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() actionID := args[0] - res, err := client.GetAction(ctx, &shieldv1beta1.GetActionRequest{ + res, err := client.GetAction(cmd.Context(), &shieldv1beta1.GetActionRequest{ Id: actionID, }) if err != nil { @@ -207,7 +200,7 @@ func viewActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return cmd } -func listActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func listActionCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "list", Short: "List all actions", @@ -222,15 +215,13 @@ func listActionCommand(logger log.Logger, appConfig *config.Shield) *cli.Command spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.ListActions(ctx, &shieldv1beta1.ListActionsRequest{}) + res, err := client.ListActions(cmd.Context(), &shieldv1beta1.ListActionsRequest{}) if err != nil { return err } diff --git a/cmd/action_test.go b/cmd/action_test.go new file mode 100644 index 000000000..3f61bb1ee --- /dev/null +++ b/cmd/action_test.go @@ -0,0 +1,120 @@ +package cmd_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/shield/cmd" + "github.com/stretchr/testify/assert" +) + +var expectedActionUsageHelp = heredoc.Doc(` + +USAGE + shield action [flags] + +CORE COMMANDS + create Create an action + edit Edit an action + list List all actions + view View an action + +FLAGS + -h, --host string Shield API service to connect to + +INHERITED FLAGS + --help Show help for command + +EXAMPLES + $ shield action create + $ shield action edit + $ shield action view + $ shield action list + +`) + +func TestClientAction(t *testing.T) { + t.Run("without config file", func(t *testing.T) { + tests := []struct { + name string + cliConfig *cmd.Config + subCommands []string + want string + err error + }{ + { + name: "`action` only should show usage help", + want: expectedActionUsageHelp, + err: nil, + }, + { + name: "`action` list only should throw error host not found", + want: "", + subCommands: []string{"list"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`action` list with host flag should pass", + want: "", + subCommands: []string{"list", "-h", "test"}, + err: context.DeadlineExceeded, + }, + { + name: "`action` create only should throw error host not found", + want: "", + subCommands: []string{"create"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`action` create with host flag should throw error missing required flag", + want: "", + subCommands: []string{"create", "-h", "test"}, + err: errors.New("required flag(s) \"file\", \"header\" not set"), + }, + { + name: "`action` edit without host should throw error host not found", + want: "", + subCommands: []string{"edit", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`action` edit with host flag should throw error missing required flag", + want: "", + subCommands: []string{"edit", "123", "-h", "test"}, + err: errors.New("required flag(s) \"file\" not set"), + }, + { + name: "`action` view without host should throw error host not found", + want: "", + subCommands: []string{"view", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`action` view with host flag should pass", + want: "", + subCommands: []string{"view", "123", "-h", "test"}, + err: context.DeadlineExceeded, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.cliConfig = &cmd.Config{} + cli := cmd.New(tt.cliConfig) + + buf := new(bytes.Buffer) + cli.SetOutput(buf) + args := append([]string{"action"}, tt.subCommands...) + cli.SetArgs(args) + + err := cli.Execute() + got := buf.String() + + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.want, got) + }) + } + }) +} diff --git a/cmd/client.go b/cmd/client.go index 43970a17f..6d89be176 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -5,12 +5,14 @@ import ( "time" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" + "github.com/spf13/cobra" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) func createConnection(ctx context.Context, host string) (*grpc.ClientConn, error) { opts := []grpc.DialOption{ - grpc.WithInsecure(), + grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), } @@ -24,7 +26,6 @@ func createClient(ctx context.Context, host string) (shieldv1beta1.ShieldService dialCancel() return nil, nil, err } - cancel := func() { dialCancel() conn.Close() @@ -33,3 +34,34 @@ func createClient(ctx context.Context, host string) (shieldv1beta1.ShieldService client := shieldv1beta1.NewShieldServiceClient(conn) return client, cancel, nil } + +func isClientCLI(cmd *cobra.Command) bool { + for c := cmd; c.Parent() != nil; c = c.Parent() { + if c.Annotations != nil && c.Annotations["client"] == "true" { + return true + } + } + return false +} + +func overrideClientConfigHost(cmd *cobra.Command, cliConfig *Config) error { + if cliConfig == nil { + return ErrClientConfigNotFound + } + + host, err := cmd.Flags().GetString("host") + if err == nil && host != "" { + cliConfig.Host = host + return nil + } + + if cliConfig.Host == "" { + return ErrClientConfigHostNotFound + } + + return nil +} + +func bindFlagsFromClientConfig(cmd *cobra.Command) { + cmd.PersistentFlags().StringP("host", "h", "", "Shield API service to connect to") +} diff --git a/cmd/cmd.go b/cmd/cmd.go deleted file mode 100644 index b8e874d7b..000000000 --- a/cmd/cmd.go +++ /dev/null @@ -1,27 +0,0 @@ -package cmd - -import ( - "github.com/odpf/salt/log" - "github.com/odpf/shield/config" - cli "github.com/spf13/cobra" -) - -func New(logger log.Logger, appConfig *config.Shield) *cli.Command { - var cmd = &cli.Command{ - Use: "shield", - SilenceUsage: true, - } - - cmd.AddCommand(serveCommand(logger, appConfig)) - cmd.AddCommand(migrationsCommand(logger, appConfig)) - cmd.AddCommand(migrationsRollbackCommand(logger, appConfig)) - cmd.AddCommand(NamespaceCommand(logger, appConfig)) - cmd.AddCommand(UserCommand(logger, appConfig)) - cmd.AddCommand(OrganizationCommand(logger, appConfig)) - cmd.AddCommand(GroupCommand(logger, appConfig)) - cmd.AddCommand(ProjectCommand(logger, appConfig)) - cmd.AddCommand(RoleCommand(logger, appConfig)) - cmd.AddCommand(ActionCommand(logger, appConfig)) - cmd.AddCommand(PolicyCommand(logger, appConfig)) - return cmd -} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 000000000..3379352b7 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/salt/cmdx" + "github.com/spf13/cobra" +) + +var cliConfig *Config + +type Config struct { + Host string `mapstructure:"host"` +} + +func LoadConfig() (*Config, error) { + var config Config + + cfg := cmdx.SetConfig("shield") + err := cfg.Load(&config) + + return &config, err +} + +func configCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "config ", + Short: "Manage client configurations", + Example: heredoc.Doc(` + $ shield config init + $ shield config list`), + } + + cmd.AddCommand(configInitCommand()) + cmd.AddCommand(configListCommand()) + + return cmd +} + +func configInitCommand() *cobra.Command { + return &cobra.Command{ + Use: "init", + Short: "Initialize a new client configuration", + Example: heredoc.Doc(` + $ shield config init + `), + Annotations: map[string]string{ + "group:core": "true", + }, + RunE: func(cmd *cobra.Command, args []string) error { + cfg := cmdx.SetConfig("shield") + + if err := cfg.Init(&Config{}); err != nil { + return err + } + + fmt.Printf("config created: %v\n", cfg.File()) + return nil + }, + } +} + +func configListCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "list", + Short: "List client configuration settings", + Example: heredoc.Doc(` + $ shield config list + `), + Annotations: map[string]string{ + "group:core": "true", + }, + RunE: func(cmd *cobra.Command, args []string) error { + cfg := cmdx.SetConfig("shield") + + data, err := cfg.Read() + if err != nil { + return ErrClientConfigNotFound + } + + fmt.Println(data) + return nil + }, + } + return cmd +} diff --git a/cmd/context.go b/cmd/context.go new file mode 100644 index 000000000..eec116d68 --- /dev/null +++ b/cmd/context.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "context" + "strings" + + "google.golang.org/grpc/metadata" +) + +func setCtxHeader(ctx context.Context, header string) context.Context { + s := strings.Split(header, ":") + key := s[0] + val := s[1] + + md := metadata.New(map[string]string{key: val}) + ctx = metadata.NewOutgoingContext(ctx, md) + + return ctx +} diff --git a/cmd/errors.go b/cmd/errors.go new file mode 100644 index 000000000..68fe6a993 --- /dev/null +++ b/cmd/errors.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "errors" + + "github.com/MakeNowJust/heredoc" +) + +var ( + ErrClientConfigNotFound = errors.New(heredoc.Doc(` + Shield client config not found. + + Run "shield config init" to initialize a new client config or + Run "shield help environment" for more information. + `)) + ErrClientConfigHostNotFound = errors.New(heredoc.Doc(` + Shield client config "host" not found. + + Pass shield server host with "--host" flag or + set host in shield config. + + Run "shield config " or + "shield help environment" for more information. + `)) + ErrClientNotAuthorized = errors.New(heredoc.Doc(` + Shield auth error. Shield requires an auth header. + + Run "shield help auth" for more information. + `)) +) diff --git a/cmd/group.go b/cmd/group.go index 5f8689ee4..6a33c8d8c 100644 --- a/cmd/group.go +++ b/cmd/group.go @@ -1,20 +1,17 @@ package cmd import ( - "context" "fmt" "os" - "strconv" "github.com/MakeNowJust/heredoc" - "github.com/odpf/salt/log" "github.com/odpf/salt/printer" - "github.com/odpf/shield/config" + "github.com/odpf/shield/pkg/file" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" cli "github.com/spf13/cobra" ) -func GroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func GroupCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "group", Aliases: []string{"groups"}, @@ -30,18 +27,21 @@ func GroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { `), Annotations: map[string]string{ "group:core": "true", + "client": "true", }, } - cmd.AddCommand(createGroupCommand(logger, appConfig)) - cmd.AddCommand(editGroupCommand(logger, appConfig)) - cmd.AddCommand(viewGroupCommand(logger, appConfig)) - cmd.AddCommand(listGroupCommand(logger, appConfig)) + cmd.AddCommand(createGroupCommand(cliConfig)) + cmd.AddCommand(editGroupCommand(cliConfig)) + cmd.AddCommand(viewGroupCommand(cliConfig)) + cmd.AddCommand(listGroupCommand(cliConfig)) + + bindFlagsFromClientConfig(cmd) return cmd } -func createGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func createGroupCommand(cliConfig *Config) *cli.Command { var filePath, header string cmd := &cli.Command{ @@ -59,7 +59,7 @@ func createGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman defer spinner.Stop() var reqBody shieldv1beta1.GroupRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -68,17 +68,13 @@ func createGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - ctx = setCtxHeader(ctx, header) - - res, err := client.CreateGroup(ctx, &shieldv1beta1.CreateGroupRequest{ + res, err := client.CreateGroup(setCtxHeader(cmd.Context(), header), &shieldv1beta1.CreateGroupRequest{ Body: &reqBody, }) if err != nil { @@ -86,7 +82,7 @@ func createGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman } spinner.Stop() - logger.Info(fmt.Sprintf("successfully created group %s with id %s", res.GetGroup().GetName(), res.GetGroup().GetId())) + fmt.Printf("successfully created group %s with id %s\n", res.GetGroup().GetName(), res.GetGroup().GetId()) return nil }, } @@ -99,7 +95,7 @@ func createGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman return cmd } -func editGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func editGroupCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -117,7 +113,7 @@ func editGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command defer spinner.Stop() var reqBody shieldv1beta1.GroupRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -126,16 +122,14 @@ func editGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() groupID := args[0] - _, err = client.UpdateGroup(ctx, &shieldv1beta1.UpdateGroupRequest{ + _, err = client.UpdateGroup(cmd.Context(), &shieldv1beta1.UpdateGroupRequest{ Id: groupID, Body: &reqBody, }) @@ -144,7 +138,7 @@ func editGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command } spinner.Stop() - logger.Info(fmt.Sprintf("successfully edited group with id %s", groupID)) + fmt.Printf("successfully edited group with id %s\n", groupID) return nil }, } @@ -155,7 +149,7 @@ func editGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return cmd } -func viewGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func viewGroupCommand(cliConfig *Config) *cli.Command { var metadata bool cmd := &cli.Command{ @@ -172,16 +166,14 @@ func viewGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() groupID := args[0] - res, err := client.GetGroup(ctx, &shieldv1beta1.GetGroupRequest{ + res, err := client.GetGroup(cmd.Context(), &shieldv1beta1.GetGroupRequest{ Id: groupID, }) if err != nil { @@ -229,7 +221,7 @@ func viewGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return cmd } -func listGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func listGroupCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "list", Short: "List all groups", @@ -244,15 +236,13 @@ func listGroupCommand(logger log.Logger, appConfig *config.Shield) *cli.Command spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.ListGroups(ctx, &shieldv1beta1.ListGroupsRequest{}) + res, err := client.ListGroups(cmd.Context(), &shieldv1beta1.ListGroupsRequest{}) if err != nil { return err } diff --git a/cmd/group_test.go b/cmd/group_test.go new file mode 100644 index 000000000..536fe6dc9 --- /dev/null +++ b/cmd/group_test.go @@ -0,0 +1,120 @@ +package cmd_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/shield/cmd" + "github.com/stretchr/testify/assert" +) + +var expectedGroupUsageHelp = heredoc.Doc(` + +USAGE + shield group [flags] + +CORE COMMANDS + create Create a group + edit Edit a group + list List all groups + view View a group + +FLAGS + -h, --host string Shield API service to connect to + +INHERITED FLAGS + --help Show help for command + +EXAMPLES + $ shield group create + $ shield group edit + $ shield group view + $ shield group list + +`) + +func TestClientGroup(t *testing.T) { + t.Run("without config file", func(t *testing.T) { + tests := []struct { + name string + cliConfig *cmd.Config + subCommands []string + want string + err error + }{ + { + name: "`group` only should show usage help", + want: expectedGroupUsageHelp, + err: nil, + }, + { + name: "`group` list only should throw error host not found", + want: "", + subCommands: []string{"list"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`group` list with host flag should pass", + want: "", + subCommands: []string{"list", "-h", "test"}, + err: context.DeadlineExceeded, + }, + { + name: "`group` create only should throw error host not found", + want: "", + subCommands: []string{"create"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`group` create with host flag should throw error missing required flag", + want: "", + subCommands: []string{"create", "-h", "test"}, + err: errors.New("required flag(s) \"file\", \"header\" not set"), + }, + { + name: "`group` edit without host should throw error host not found", + want: "", + subCommands: []string{"edit", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`group` edit with host flag should throw error missing required flag", + want: "", + subCommands: []string{"edit", "123", "-h", "test"}, + err: errors.New("required flag(s) \"file\" not set"), + }, + { + name: "`group` view without host should throw error host not found", + want: "", + subCommands: []string{"view", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`group` view with host flag should pass", + want: "", + subCommands: []string{"view", "123", "-h", "test"}, + err: context.DeadlineExceeded, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.cliConfig = &cmd.Config{} + cli := cmd.New(tt.cliConfig) + + buf := new(bytes.Buffer) + cli.SetOutput(buf) + args := append([]string{"group"}, tt.subCommands...) + cli.SetArgs(args) + + err := cli.Execute() + got := buf.String() + + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.want, got) + }) + } + }) +} diff --git a/cmd/help.go b/cmd/help.go new file mode 100644 index 000000000..752b804aa --- /dev/null +++ b/cmd/help.go @@ -0,0 +1,22 @@ +package cmd + +import "github.com/MakeNowJust/heredoc" + +var envHelp = map[string]string{ + "short": "Environment variables that can be used with shield", + "long": heredoc.Doc(` + ODPF_CONFIG_DIR: the directory where shield will store configuration files. Default: + "$XDG_CONFIG_HOME/odpf" or "$HOME/.config/odpf". + NO_COLOR: set to any value to avoid printing ANSI escape sequences for color output. + CLICOLOR: set to "0" to disable printing ANSI colors in output. + `), +} + +var authHelp = map[string]string{ + "short": "Auth configs that need to be used with shield", + "long": heredoc.Doc(` + Send an additional flag header with "key:value" format. + Example: + shield create user -f user.yaml -H X-Shield-Email:user@odpf.io + `), +} diff --git a/cmd/migrations.go b/cmd/migrations.go deleted file mode 100644 index a09abb17d..000000000 --- a/cmd/migrations.go +++ /dev/null @@ -1,39 +0,0 @@ -package cmd - -import ( - "github.com/odpf/salt/log" - "github.com/odpf/shield/config" - "github.com/odpf/shield/internal/store/postgres/migrations" - "github.com/odpf/shield/pkg/db" - cli "github.com/spf13/cobra" -) - -func migrationsCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { - c := &cli.Command{ - Use: "migrate", - Short: "Run DB Schema Migrations", - Example: "shield migrate", - RunE: func(c *cli.Command, args []string) error { - return db.RunMigrations(db.Config{ - Driver: appConfig.DB.Driver, - URL: appConfig.DB.URL, - }, migrations.MigrationFs, migrations.ResourcePath) - }, - } - return c -} - -func migrationsRollbackCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { - c := &cli.Command{ - Use: "migration-rollback", - Short: "Run DB Schema Migrations Rollback to last state", - Example: "shield migration-rollback", - RunE: func(c *cli.Command, args []string) error { - return db.RunRollback(db.Config{ - Driver: appConfig.DB.Driver, - URL: appConfig.DB.URL, - }, migrations.MigrationFs, migrations.ResourcePath) - }, - } - return c -} diff --git a/cmd/namespace.go b/cmd/namespace.go index 6fac70f2c..a00ab0488 100644 --- a/cmd/namespace.go +++ b/cmd/namespace.go @@ -1,20 +1,17 @@ package cmd import ( - "context" "fmt" "os" - "strconv" "github.com/MakeNowJust/heredoc" - "github.com/odpf/salt/log" "github.com/odpf/salt/printer" - "github.com/odpf/shield/config" + "github.com/odpf/shield/pkg/file" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" cli "github.com/spf13/cobra" ) -func NamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func NamespaceCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "namespace", Aliases: []string{"namespaces"}, @@ -30,18 +27,21 @@ func NamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Command `), Annotations: map[string]string{ "group:core": "true", + "client": "true", }, } - cmd.AddCommand(createNamespaceCommand(logger, appConfig)) - cmd.AddCommand(editNamespaceCommand(logger, appConfig)) - cmd.AddCommand(viewNamespaceCommand(logger, appConfig)) - cmd.AddCommand(listNamespaceCommand(logger, appConfig)) + cmd.AddCommand(createNamespaceCommand(cliConfig)) + cmd.AddCommand(editNamespaceCommand(cliConfig)) + cmd.AddCommand(viewNamespaceCommand(cliConfig)) + cmd.AddCommand(listNamespaceCommand(cliConfig)) + + bindFlagsFromClientConfig(cmd) return cmd } -func createNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func createNamespaceCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -59,7 +59,7 @@ func createNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Co defer spinner.Stop() var reqBody shieldv1beta1.NamespaceRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -68,15 +68,13 @@ func createNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Co return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.CreateNamespace(ctx, &shieldv1beta1.CreateNamespaceRequest{ + res, err := client.CreateNamespace(cmd.Context(), &shieldv1beta1.CreateNamespaceRequest{ Body: &reqBody, }) if err != nil { @@ -84,7 +82,7 @@ func createNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Co } spinner.Stop() - logger.Info(fmt.Sprintf("successfully created namespace %s with id %s", res.GetNamespace().GetName(), res.GetNamespace().GetId())) + fmt.Printf("successfully created namespace %s with id %s\n", res.GetNamespace().GetName(), res.GetNamespace().GetId()) return nil }, } @@ -95,7 +93,7 @@ func createNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Co return cmd } -func editNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func editNamespaceCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -113,7 +111,7 @@ func editNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm defer spinner.Stop() var reqBody shieldv1beta1.NamespaceRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -122,16 +120,14 @@ func editNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() namespaceID := args[0] - res, err := client.UpdateNamespace(ctx, &shieldv1beta1.UpdateNamespaceRequest{ + res, err := client.UpdateNamespace(cmd.Context(), &shieldv1beta1.UpdateNamespaceRequest{ Id: namespaceID, Body: &reqBody, }) @@ -140,7 +136,7 @@ func editNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm } spinner.Stop() - logger.Info(fmt.Sprintf("successfully edited namespace with id %s to id %s and name %s", namespaceID, res.GetNamespace().GetId(), res.GetNamespace().GetName())) + fmt.Printf("successfully edited namespace with id %s to id %s and name %s\n", namespaceID, res.GetNamespace().GetId(), res.GetNamespace().GetName()) return nil }, } @@ -151,7 +147,7 @@ func editNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm return cmd } -func viewNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func viewNamespaceCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "view", Short: "View a namespace", @@ -166,16 +162,14 @@ func viewNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() namespaceID := args[0] - res, err := client.GetNamespace(ctx, &shieldv1beta1.GetNamespaceRequest{ + res, err := client.GetNamespace(cmd.Context(), &shieldv1beta1.GetNamespaceRequest{ Id: namespaceID, }) if err != nil { @@ -206,7 +200,7 @@ func viewNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm return cmd } -func listNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func listNamespaceCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "list", Short: "List all namespaces", @@ -221,15 +215,13 @@ func listNamespaceCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.ListNamespaces(ctx, &shieldv1beta1.ListNamespacesRequest{}) + res, err := client.ListNamespaces(cmd.Context(), &shieldv1beta1.ListNamespacesRequest{}) if err != nil { return err } diff --git a/cmd/namespace_test.go b/cmd/namespace_test.go new file mode 100644 index 000000000..b5fa70c81 --- /dev/null +++ b/cmd/namespace_test.go @@ -0,0 +1,120 @@ +package cmd_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/shield/cmd" + "github.com/stretchr/testify/assert" +) + +var expectedNamespaceUsageHelp = heredoc.Doc(` + +USAGE + shield namespace [flags] + +CORE COMMANDS + create Create a namespace + edit Edit a namespace + list List all namespaces + view View a namespace + +FLAGS + -h, --host string Shield API service to connect to + +INHERITED FLAGS + --help Show help for command + +EXAMPLES + $ shield namespace create + $ shield namespace edit + $ shield namespace view + $ shield namespace list + +`) + +func TestClientNamespace(t *testing.T) { + t.Run("without config file", func(t *testing.T) { + tests := []struct { + name string + cliConfig *cmd.Config + subCommands []string + want string + err error + }{ + { + name: "`namespace` only should show usage help", + want: expectedNamespaceUsageHelp, + err: nil, + }, + { + name: "`namespace` list only should throw error host not found", + want: "", + subCommands: []string{"list"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`namespace` list with host flag should pass", + want: "", + subCommands: []string{"list", "-h", "test"}, + err: context.DeadlineExceeded, + }, + { + name: "`namespace` create only should throw error host not found", + want: "", + subCommands: []string{"create"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`namespace` create with host flag should throw error missing required flag", + want: "", + subCommands: []string{"create", "-h", "test"}, + err: errors.New("required flag(s) \"file\" not set"), + }, + { + name: "`namespace` edit without host should throw error host not found", + want: "", + subCommands: []string{"edit", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`namespace` edit with host flag should throw error missing required flag", + want: "", + subCommands: []string{"edit", "123", "-h", "test"}, + err: errors.New("required flag(s) \"file\" not set"), + }, + { + name: "`namespace` view without host should throw error host not found", + want: "", + subCommands: []string{"view", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`namespace` view with host flag should pass", + want: "", + subCommands: []string{"view", "123", "-h", "test"}, + err: context.DeadlineExceeded, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.cliConfig = &cmd.Config{} + cli := cmd.New(tt.cliConfig) + + buf := new(bytes.Buffer) + cli.SetOutput(buf) + args := append([]string{"namespace"}, tt.subCommands...) + cli.SetArgs(args) + + err := cli.Execute() + got := buf.String() + + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.want, got) + }) + } + }) +} diff --git a/cmd/organization.go b/cmd/organization.go index 40d07cf16..17173ee75 100644 --- a/cmd/organization.go +++ b/cmd/organization.go @@ -1,20 +1,17 @@ package cmd import ( - "context" "fmt" "os" - "strconv" "github.com/MakeNowJust/heredoc" - "github.com/odpf/salt/log" "github.com/odpf/salt/printer" - "github.com/odpf/shield/config" + "github.com/odpf/shield/pkg/file" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" cli "github.com/spf13/cobra" ) -func OrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func OrganizationCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "organization", Aliases: []string{"organizations"}, @@ -30,21 +27,24 @@ func OrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma `), Annotations: map[string]string{ "group:core": "true", + "client": "true", }, } - cmd.AddCommand(createOrganizationCommand(logger, appConfig)) - cmd.AddCommand(editOrganizationCommand(logger, appConfig)) - cmd.AddCommand(viewOrganizationCommand(logger, appConfig)) - cmd.AddCommand(listOrganizationCommand(logger, appConfig)) - cmd.AddCommand(admaddOrganizationCommand(logger, appConfig)) - cmd.AddCommand(admremoveOrganizationCommand(logger, appConfig)) - cmd.AddCommand(admlistOrganizationCommand(logger, appConfig)) + cmd.AddCommand(createOrganizationCommand(cliConfig)) + cmd.AddCommand(editOrganizationCommand(cliConfig)) + cmd.AddCommand(viewOrganizationCommand(cliConfig)) + cmd.AddCommand(listOrganizationCommand(cliConfig)) + cmd.AddCommand(admaddOrganizationCommand(cliConfig)) + cmd.AddCommand(admremoveOrganizationCommand(cliConfig)) + cmd.AddCommand(admlistOrganizationCommand(cliConfig)) + + bindFlagsFromClientConfig(cmd) return cmd } -func createOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func createOrganizationCommand(cliConfig *Config) *cli.Command { var filePath, header string cmd := &cli.Command{ @@ -62,7 +62,7 @@ func createOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli defer spinner.Stop() var reqBody shieldv1beta1.OrganizationRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -71,16 +71,13 @@ func createOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - ctx = setCtxHeader(ctx, header) - + ctx := setCtxHeader(cmd.Context(), header) res, err := client.CreateOrganization(ctx, &shieldv1beta1.CreateOrganizationRequest{ Body: &reqBody, }) @@ -89,7 +86,7 @@ func createOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli } spinner.Stop() - logger.Info(fmt.Sprintf("successfully created organization %s with id %s", res.GetOrganization().GetName(), res.GetOrganization().GetId())) + fmt.Printf("successfully created organization %s with id %s\n", res.GetOrganization().GetName(), res.GetOrganization().GetId()) return nil }, } @@ -102,7 +99,7 @@ func createOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli return cmd } -func editOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func editOrganizationCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -120,7 +117,7 @@ func editOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.C defer spinner.Stop() var reqBody shieldv1beta1.OrganizationRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -129,16 +126,14 @@ func editOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.C return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() organizationID := args[0] - _, err = client.UpdateOrganization(ctx, &shieldv1beta1.UpdateOrganizationRequest{ + _, err = client.UpdateOrganization(cmd.Context(), &shieldv1beta1.UpdateOrganizationRequest{ Id: organizationID, Body: &reqBody, }) @@ -147,7 +142,7 @@ func editOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.C } spinner.Stop() - logger.Info(fmt.Sprintf("successfully edited organization with id %s", organizationID)) + fmt.Printf("successfully edited organization with id %s\n", organizationID) return nil }, } @@ -158,7 +153,7 @@ func editOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.C return cmd } -func viewOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func viewOrganizationCommand(cliConfig *Config) *cli.Command { var metadata bool cmd := &cli.Command{ @@ -175,16 +170,14 @@ func viewOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.C spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() organizationID := args[0] - res, err := client.GetOrganization(ctx, &shieldv1beta1.GetOrganizationRequest{ + res, err := client.GetOrganization(cmd.Context(), &shieldv1beta1.GetOrganizationRequest{ Id: organizationID, }) if err != nil { @@ -231,7 +224,7 @@ func viewOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.C return cmd } -func listOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func listOrganizationCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "list", Short: "List all organizations", @@ -246,15 +239,13 @@ func listOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.C spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.ListOrganizations(ctx, &shieldv1beta1.ListOrganizationsRequest{}) + res, err := client.ListOrganizations(cmd.Context(), &shieldv1beta1.ListOrganizationsRequest{}) if err != nil { return err } @@ -288,7 +279,7 @@ func listOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.C return cmd } -func admaddOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func admaddOrganizationCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -306,7 +297,7 @@ func admaddOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli defer spinner.Stop() var reqBody shieldv1beta1.AddOrganizationAdminRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -315,16 +306,14 @@ func admaddOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() organizationID := args[0] - _, err = client.AddOrganizationAdmin(ctx, &shieldv1beta1.AddOrganizationAdminRequest{ + _, err = client.AddOrganizationAdmin(cmd.Context(), &shieldv1beta1.AddOrganizationAdminRequest{ Id: organizationID, Body: &reqBody, }) @@ -333,7 +322,7 @@ func admaddOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli } spinner.Stop() - logger.Info("successfully added admin(s) to organization") + fmt.Println("successfully added admin(s) to organization") return nil }, } @@ -344,7 +333,7 @@ func admaddOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli return cmd } -func admremoveOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func admremoveOrganizationCommand(cliConfig *Config) *cli.Command { var userID string cmd := &cli.Command{ @@ -361,16 +350,14 @@ func admremoveOrganizationCommand(logger log.Logger, appConfig *config.Shield) * spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() organizationID := args[0] - _, err = client.RemoveOrganizationAdmin(ctx, &shieldv1beta1.RemoveOrganizationAdminRequest{ + _, err = client.RemoveOrganizationAdmin(cmd.Context(), &shieldv1beta1.RemoveOrganizationAdminRequest{ Id: organizationID, UserId: userID, }) @@ -379,7 +366,7 @@ func admremoveOrganizationCommand(logger log.Logger, appConfig *config.Shield) * } spinner.Stop() - logger.Info("successfully removed admin from organization") + fmt.Println("successfully removed admin from organization") return nil }, } @@ -390,7 +377,7 @@ func admremoveOrganizationCommand(logger log.Logger, appConfig *config.Shield) * return cmd } -func admlistOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func admlistOrganizationCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "admlist", Short: "list admins of an organization", @@ -405,16 +392,14 @@ func admlistOrganizationCommand(logger log.Logger, appConfig *config.Shield) *cl spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() organizationID := args[0] - res, err := client.ListOrganizationAdmins(ctx, &shieldv1beta1.ListOrganizationAdminsRequest{ + res, err := client.ListOrganizationAdmins(cmd.Context(), &shieldv1beta1.ListOrganizationAdminsRequest{ Id: organizationID, }) if err != nil { diff --git a/cmd/organization_test.go b/cmd/organization_test.go new file mode 100644 index 000000000..d1d1a9fe8 --- /dev/null +++ b/cmd/organization_test.go @@ -0,0 +1,123 @@ +package cmd_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/shield/cmd" + "github.com/stretchr/testify/assert" +) + +var expectedOrganizationUsageHelp = heredoc.Doc(` + +USAGE + shield organization [flags] + +CORE COMMANDS + admadd add admins to an organization + admlist list admins of an organization + admremove remove admins from an organization + create Create an organization + edit Edit an organization + list List all organizations + view View an organization + +FLAGS + -h, --host string Shield API service to connect to + +INHERITED FLAGS + --help Show help for command + +EXAMPLES + $ shield organization create + $ shield organization edit + $ shield organization view + $ shield organization list + +`) + +func TestClientOrganization(t *testing.T) { + t.Run("without config file", func(t *testing.T) { + tests := []struct { + name string + cliConfig *cmd.Config + subCommands []string + want string + err error + }{ + { + name: "`organization` only should show usage help", + want: expectedOrganizationUsageHelp, + err: nil, + }, + { + name: "`organization` list only should throw error host not found", + want: "", + subCommands: []string{"list"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`organization` list with host flag should pass", + want: "", + subCommands: []string{"list", "-h", "test"}, + err: context.DeadlineExceeded, + }, + { + name: "`organization` create only should throw error host not found", + want: "", + subCommands: []string{"create"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`organization` create with host flag should throw error missing required flag", + want: "", + subCommands: []string{"create", "-h", "test"}, + err: errors.New("required flag(s) \"file\", \"header\" not set"), + }, + { + name: "`organization` edit without host should throw error host not found", + want: "", + subCommands: []string{"edit", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`organization` edit with host flag should throw error missing required flag", + want: "", + subCommands: []string{"edit", "123", "-h", "test"}, + err: errors.New("required flag(s) \"file\" not set"), + }, + { + name: "`organization` view without host should throw error host not found", + want: "", + subCommands: []string{"view", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`organization` view with host flag should pass", + want: "", + subCommands: []string{"view", "123", "-h", "test"}, + err: context.DeadlineExceeded, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.cliConfig = &cmd.Config{} + cli := cmd.New(tt.cliConfig) + + buf := new(bytes.Buffer) + cli.SetOutput(buf) + args := append([]string{"organization"}, tt.subCommands...) + cli.SetArgs(args) + + err := cli.Execute() + got := buf.String() + + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.want, got) + }) + } + }) +} diff --git a/cmd/policy.go b/cmd/policy.go index 5cc055c50..ef9b99ff2 100644 --- a/cmd/policy.go +++ b/cmd/policy.go @@ -1,20 +1,17 @@ package cmd import ( - "context" "fmt" "os" - "strconv" "github.com/MakeNowJust/heredoc" - "github.com/odpf/salt/log" "github.com/odpf/salt/printer" - "github.com/odpf/shield/config" + "github.com/odpf/shield/pkg/file" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" cli "github.com/spf13/cobra" ) -func PolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func PolicyCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "policy", Aliases: []string{"policies"}, @@ -29,19 +26,22 @@ func PolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { $ shield policy list `), Annotations: map[string]string{ - "policy:core": "true", + "group:core": "true", + "client": "true", }, } - cmd.AddCommand(createPolicyCommand(logger, appConfig)) - cmd.AddCommand(editPolicyCommand(logger, appConfig)) - cmd.AddCommand(viewPolicyCommand(logger, appConfig)) - cmd.AddCommand(listPolicyCommand(logger, appConfig)) + cmd.AddCommand(createPolicyCommand(cliConfig)) + cmd.AddCommand(editPolicyCommand(cliConfig)) + cmd.AddCommand(viewPolicyCommand(cliConfig)) + cmd.AddCommand(listPolicyCommand(cliConfig)) + + bindFlagsFromClientConfig(cmd) return cmd } -func createPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func createPolicyCommand(cliConfig *Config) *cli.Command { var filePath, header string cmd := &cli.Command{ @@ -59,7 +59,7 @@ func createPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma defer spinner.Stop() var reqBody shieldv1beta1.PolicyRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -68,16 +68,13 @@ func createPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - ctx = setCtxHeader(ctx, header) - + ctx := setCtxHeader(cmd.Context(), header) _, err = client.CreatePolicy(ctx, &shieldv1beta1.CreatePolicyRequest{ Body: &reqBody, }) @@ -86,7 +83,7 @@ func createPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma } spinner.Stop() - logger.Info("successfully created policy") + fmt.Println("successfully created policy") return nil }, } @@ -99,7 +96,7 @@ func createPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Comma return cmd } -func editPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func editPolicyCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -117,7 +114,7 @@ func editPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command defer spinner.Stop() var reqBody shieldv1beta1.PolicyRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -126,16 +123,14 @@ func editPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() policyID := args[0] - _, err = client.UpdatePolicy(ctx, &shieldv1beta1.UpdatePolicyRequest{ + _, err = client.UpdatePolicy(cmd.Context(), &shieldv1beta1.UpdatePolicyRequest{ Id: policyID, Body: &reqBody, }) @@ -144,7 +139,7 @@ func editPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command } spinner.Stop() - logger.Info("successfully edited policy") + fmt.Println("successfully edited policy") return nil }, } @@ -155,7 +150,7 @@ func editPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return cmd } -func viewPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func viewPolicyCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "view", Short: "View a policy", @@ -170,16 +165,14 @@ func viewPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() policyID := args[0] - res, err := client.GetPolicy(ctx, &shieldv1beta1.GetPolicyRequest{ + res, err := client.GetPolicy(cmd.Context(), &shieldv1beta1.GetPolicyRequest{ Id: policyID, }) if err != nil { @@ -207,7 +200,7 @@ func viewPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return cmd } -func listPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func listPolicyCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "list", Short: "List all policies", @@ -222,15 +215,13 @@ func listPolicyCommand(logger log.Logger, appConfig *config.Shield) *cli.Command spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.ListPolicies(ctx, &shieldv1beta1.ListPoliciesRequest{}) + res, err := client.ListPolicies(cmd.Context(), &shieldv1beta1.ListPoliciesRequest{}) if err != nil { return err } diff --git a/cmd/policy_test.go b/cmd/policy_test.go new file mode 100644 index 000000000..d0a636380 --- /dev/null +++ b/cmd/policy_test.go @@ -0,0 +1,120 @@ +package cmd_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/shield/cmd" + "github.com/stretchr/testify/assert" +) + +var expectedPolicyUsageHelp = heredoc.Doc(` + +USAGE + shield policy [flags] + +CORE COMMANDS + create Create a policy + edit Edit a policy + list List all policies + view View a policy + +FLAGS + -h, --host string Shield API service to connect to + +INHERITED FLAGS + --help Show help for command + +EXAMPLES + $ shield policy create + $ shield policy edit + $ shield policy view + $ shield policy list + +`) + +func TestClientPolicy(t *testing.T) { + t.Run("without config file", func(t *testing.T) { + tests := []struct { + name string + cliConfig *cmd.Config + subCommands []string + want string + err error + }{ + { + name: "`policy` only should show usage help", + want: expectedPolicyUsageHelp, + err: nil, + }, + { + name: "`policy` list only should throw error host not found", + want: "", + subCommands: []string{"list"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`policy` list with host flag should pass", + want: "", + subCommands: []string{"list", "-h", "test"}, + err: context.DeadlineExceeded, + }, + { + name: "`policy` create only should throw error host not found", + want: "", + subCommands: []string{"create"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`policy` create with host flag should throw error missing required flag", + want: "", + subCommands: []string{"create", "-h", "test"}, + err: errors.New("required flag(s) \"file\", \"header\" not set"), + }, + { + name: "`policy` edit without host should throw error host not found", + want: "", + subCommands: []string{"edit", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`policy` edit with host flag should throw error missing required flag", + want: "", + subCommands: []string{"edit", "123", "-h", "test"}, + err: errors.New("required flag(s) \"file\" not set"), + }, + { + name: "`policy` view without host should throw error host not found", + want: "", + subCommands: []string{"view", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`policy` view with host flag should pass", + want: "", + subCommands: []string{"view", "123", "-h", "test"}, + err: context.DeadlineExceeded, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.cliConfig = &cmd.Config{} + cli := cmd.New(tt.cliConfig) + + buf := new(bytes.Buffer) + cli.SetOutput(buf) + args := append([]string{"policy"}, tt.subCommands...) + cli.SetArgs(args) + + err := cli.Execute() + got := buf.String() + + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.want, got) + }) + } + }) +} diff --git a/cmd/project.go b/cmd/project.go index 6e7dfb15a..fac5dcce8 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -1,20 +1,17 @@ package cmd import ( - "context" "fmt" "os" - "strconv" "github.com/MakeNowJust/heredoc" - "github.com/odpf/salt/log" "github.com/odpf/salt/printer" - "github.com/odpf/shield/config" + "github.com/odpf/shield/pkg/file" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" cli "github.com/spf13/cobra" ) -func ProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func ProjectCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "project", Aliases: []string{"projects"}, @@ -29,19 +26,22 @@ func ProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { $ shield project list `), Annotations: map[string]string{ - "project:core": "true", + "group:core": "true", + "client": "true", }, } - cmd.AddCommand(createProjectCommand(logger, appConfig)) - cmd.AddCommand(editProjectCommand(logger, appConfig)) - cmd.AddCommand(viewProjectCommand(logger, appConfig)) - cmd.AddCommand(listProjectCommand(logger, appConfig)) + cmd.AddCommand(createProjectCommand(cliConfig)) + cmd.AddCommand(editProjectCommand(cliConfig)) + cmd.AddCommand(viewProjectCommand(cliConfig)) + cmd.AddCommand(listProjectCommand(cliConfig)) + + bindFlagsFromClientConfig(cmd) return cmd } -func createProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func createProjectCommand(cliConfig *Config) *cli.Command { var filePath, header string cmd := &cli.Command{ @@ -59,7 +59,7 @@ func createProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm defer spinner.Stop() var reqBody shieldv1beta1.ProjectRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -68,16 +68,13 @@ func createProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - ctx = setCtxHeader(ctx, header) - + ctx := setCtxHeader(cmd.Context(), header) res, err := client.CreateProject(ctx, &shieldv1beta1.CreateProjectRequest{ Body: &reqBody, }) @@ -86,7 +83,7 @@ func createProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm } spinner.Stop() - logger.Info(fmt.Sprintf("successfully created project %s with id %s", res.GetProject().GetName(), res.GetProject().GetId())) + fmt.Printf("successfully created project %s with id %s\n", res.GetProject().GetName(), res.GetProject().GetId()) return nil }, } @@ -99,7 +96,7 @@ func createProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comm return cmd } -func editProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func editProjectCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -117,7 +114,7 @@ func editProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman defer spinner.Stop() var reqBody shieldv1beta1.ProjectRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -126,16 +123,14 @@ func editProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() projectID := args[0] - _, err = client.UpdateProject(ctx, &shieldv1beta1.UpdateProjectRequest{ + _, err = client.UpdateProject(cmd.Context(), &shieldv1beta1.UpdateProjectRequest{ Id: projectID, Body: &reqBody, }) @@ -144,7 +139,7 @@ func editProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman } spinner.Stop() - logger.Info(fmt.Sprintf("successfully edited project with id %s", projectID)) + fmt.Printf("successfully edited project with id %s\n", projectID) return nil }, } @@ -155,7 +150,7 @@ func editProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman return cmd } -func viewProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func viewProjectCommand(cliConfig *Config) *cli.Command { var metadata bool cmd := &cli.Command{ @@ -172,16 +167,14 @@ func viewProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() projectID := args[0] - res, err := client.GetProject(ctx, &shieldv1beta1.GetProjectRequest{ + res, err := client.GetProject(cmd.Context(), &shieldv1beta1.GetProjectRequest{ Id: projectID, }) if err != nil { @@ -229,7 +222,7 @@ func viewProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman return cmd } -func listProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func listProjectCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "list", Short: "List all projects", @@ -244,15 +237,13 @@ func listProjectCommand(logger log.Logger, appConfig *config.Shield) *cli.Comman spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.ListProjects(ctx, &shieldv1beta1.ListProjectsRequest{}) + res, err := client.ListProjects(cmd.Context(), &shieldv1beta1.ListProjectsRequest{}) if err != nil { return err } diff --git a/cmd/project_test.go b/cmd/project_test.go new file mode 100644 index 000000000..cf6fb7450 --- /dev/null +++ b/cmd/project_test.go @@ -0,0 +1,120 @@ +package cmd_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/shield/cmd" + "github.com/stretchr/testify/assert" +) + +var expectedProjectUsageHelp = heredoc.Doc(` + +USAGE + shield project [flags] + +CORE COMMANDS + create Create a project + edit Edit a project + list List all projects + view View a project + +FLAGS + -h, --host string Shield API service to connect to + +INHERITED FLAGS + --help Show help for command + +EXAMPLES + $ shield project create + $ shield project edit + $ shield project view + $ shield project list + +`) + +func TestClientProject(t *testing.T) { + t.Run("without config file", func(t *testing.T) { + tests := []struct { + name string + cliConfig *cmd.Config + subCommands []string + want string + err error + }{ + { + name: "`project` only should show usage help", + want: expectedProjectUsageHelp, + err: nil, + }, + { + name: "`project` list only should throw error host not found", + want: "", + subCommands: []string{"list"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`project` list with host flag should pass", + want: "", + subCommands: []string{"list", "-h", "test"}, + err: context.DeadlineExceeded, + }, + { + name: "`project` create only should throw error host not found", + want: "", + subCommands: []string{"create"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`project` create with host flag should throw error missing required flag", + want: "", + subCommands: []string{"create", "-h", "test"}, + err: errors.New("required flag(s) \"file\", \"header\" not set"), + }, + { + name: "`project` edit without host should throw error host not found", + want: "", + subCommands: []string{"edit", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`project` edit with host flag should throw error missing required flag", + want: "", + subCommands: []string{"edit", "123", "-h", "test"}, + err: errors.New("required flag(s) \"file\" not set"), + }, + { + name: "`project` view without host should throw error host not found", + want: "", + subCommands: []string{"view", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`project` view with host flag should pass", + want: "", + subCommands: []string{"view", "123", "-h", "test"}, + err: context.DeadlineExceeded, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.cliConfig = &cmd.Config{} + cli := cmd.New(tt.cliConfig) + + buf := new(bytes.Buffer) + cli.SetOutput(buf) + args := append([]string{"project"}, tt.subCommands...) + cli.SetArgs(args) + + err := cli.Execute() + got := buf.String() + + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.want, got) + }) + } + }) +} diff --git a/cmd/role.go b/cmd/role.go index 80291c788..767cab749 100644 --- a/cmd/role.go +++ b/cmd/role.go @@ -1,21 +1,18 @@ package cmd import ( - "context" "fmt" "os" - "strconv" "strings" "github.com/MakeNowJust/heredoc" - "github.com/odpf/salt/log" "github.com/odpf/salt/printer" - "github.com/odpf/shield/config" + "github.com/odpf/shield/pkg/file" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" cli "github.com/spf13/cobra" ) -func RoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func RoleCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "role", Aliases: []string{"roles"}, @@ -30,19 +27,22 @@ func RoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { $ shield role list `), Annotations: map[string]string{ - "role:core": "true", + "group:core": "true", + "client": "true", }, } - cmd.AddCommand(createRoleCommand(logger, appConfig)) - cmd.AddCommand(editRoleCommand(logger, appConfig)) - cmd.AddCommand(viewRoleCommand(logger, appConfig)) - cmd.AddCommand(listRoleCommand(logger, appConfig)) + cmd.AddCommand(createRoleCommand(cliConfig)) + cmd.AddCommand(editRoleCommand(cliConfig)) + cmd.AddCommand(viewRoleCommand(cliConfig)) + cmd.AddCommand(listRoleCommand(cliConfig)) + + bindFlagsFromClientConfig(cmd) return cmd } -func createRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func createRoleCommand(cliConfig *Config) *cli.Command { var filePath, header string cmd := &cli.Command{ @@ -60,7 +60,7 @@ func createRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command defer spinner.Stop() var reqBody shieldv1beta1.RoleRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -69,15 +69,13 @@ func createRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - ctx = setCtxHeader(ctx, header) + ctx := setCtxHeader(cmd.Context(), header) res, err := client.CreateRole(ctx, &shieldv1beta1.CreateRoleRequest{ Body: &reqBody, @@ -87,7 +85,7 @@ func createRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command } spinner.Stop() - logger.Info(fmt.Sprintf("successfully created role %s with id %s", res.GetRole().GetName(), res.GetRole().GetId())) + fmt.Printf("successfully created role %s with id %s\n", res.GetRole().GetName(), res.GetRole().GetId()) return nil }, } @@ -100,7 +98,7 @@ func createRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return cmd } -func editRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func editRoleCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -118,7 +116,7 @@ func editRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { defer spinner.Stop() var reqBody shieldv1beta1.RoleRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -127,16 +125,14 @@ func editRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() roleID := args[0] - _, err = client.UpdateRole(ctx, &shieldv1beta1.UpdateRoleRequest{ + _, err = client.UpdateRole(cmd.Context(), &shieldv1beta1.UpdateRoleRequest{ Id: roleID, Body: &reqBody, }) @@ -145,7 +141,7 @@ func editRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { } spinner.Stop() - logger.Info(fmt.Sprintf("successfully edited role with id %s", roleID)) + fmt.Printf("successfully edited role with id %s\n", roleID) return nil }, } @@ -156,7 +152,7 @@ func editRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { return cmd } -func viewRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func viewRoleCommand(cliConfig *Config) *cli.Command { var metadata bool cmd := &cli.Command{ @@ -173,16 +169,14 @@ func viewRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() roleID := args[0] - res, err := client.GetRole(ctx, &shieldv1beta1.GetRoleRequest{ + res, err := client.GetRole(cmd.Context(), &shieldv1beta1.GetRoleRequest{ Id: roleID, }) if err != nil { @@ -230,7 +224,7 @@ func viewRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { return cmd } -func listRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func listRoleCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "list", Short: "List all roles", @@ -245,15 +239,13 @@ func listRoleCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) - ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.ListRoles(ctx, &shieldv1beta1.ListRolesRequest{}) + res, err := client.ListRoles(cmd.Context(), &shieldv1beta1.ListRolesRequest{}) if err != nil { return err } diff --git a/cmd/role_test.go b/cmd/role_test.go new file mode 100644 index 000000000..b34716b2f --- /dev/null +++ b/cmd/role_test.go @@ -0,0 +1,120 @@ +package cmd_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/shield/cmd" + "github.com/stretchr/testify/assert" +) + +var expectedRoleUsageHelp = heredoc.Doc(` + +USAGE + shield role [flags] + +CORE COMMANDS + create Create a role + edit Edit a role + list List all roles + view View a role + +FLAGS + -h, --host string Shield API service to connect to + +INHERITED FLAGS + --help Show help for command + +EXAMPLES + $ shield role create + $ shield role edit + $ shield role view + $ shield role list + +`) + +func TestClientRole(t *testing.T) { + t.Run("without config file", func(t *testing.T) { + tests := []struct { + name string + cliConfig *cmd.Config + subCommands []string + want string + err error + }{ + { + name: "`role` only should show usage help", + want: expectedRoleUsageHelp, + err: nil, + }, + { + name: "`role` list only should throw error host not found", + want: "", + subCommands: []string{"list"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`role` list with host flag should pass", + want: "", + subCommands: []string{"list", "-h", "test"}, + err: context.DeadlineExceeded, + }, + { + name: "`role` create only should throw error host not found", + want: "", + subCommands: []string{"create"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`role` create with host flag should throw error missing required flag", + want: "", + subCommands: []string{"create", "-h", "test"}, + err: errors.New("required flag(s) \"file\", \"header\" not set"), + }, + { + name: "`role` edit without host should throw error host not found", + want: "", + subCommands: []string{"edit", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`role` edit with host flag should throw error missing required flag", + want: "", + subCommands: []string{"edit", "123", "-h", "test"}, + err: errors.New("required flag(s) \"file\" not set"), + }, + { + name: "`role` view without host should throw error host not found", + want: "", + subCommands: []string{"view", "123"}, + err: cmd.ErrClientConfigHostNotFound, + }, + { + name: "`role` view with host flag should pass", + want: "", + subCommands: []string{"view", "123", "-h", "test"}, + err: context.DeadlineExceeded, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.cliConfig = &cmd.Config{} + cli := cmd.New(tt.cliConfig) + + buf := new(bytes.Buffer) + cli.SetOutput(buf) + args := append([]string{"role"}, tt.subCommands...) + cli.SetArgs(args) + + err := cli.Execute() + got := buf.String() + + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.want, got) + }) + } + }) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 000000000..6dbede7af --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "github.com/MakeNowJust/heredoc" + "github.com/odpf/salt/cmdx" + "github.com/spf13/cobra" + cli "github.com/spf13/cobra" +) + +func New(cfg *Config) *cli.Command { + cliConfig = cfg + + var cmd = &cli.Command{ + Use: "shield [flags]", + Short: "A cloud native role-based authorization aware reverse-proxy service", + Long: heredoc.Doc(` + A cloud native role-based authorization aware reverse-proxy service.`), + SilenceUsage: true, + SilenceErrors: true, + Example: heredoc.Doc(` + $ shield group list + $ shield organization list + $ shield project list + $ shield user create --file user.yaml + `), + Annotations: map[string]string{ + "group:core": "true", + "help:learn": heredoc.Doc(` + Use 'shield --help' for more information about a command. + Read the manual at https://odpf.github.io/shield/ + `), + "help:feedback": heredoc.Doc(` + Open an issue here https://github.com/odpf/shield/issues + `), + "help:environment": heredoc.Doc(` + See 'shield help environment' for the list of supported environment variables. + `), + }, + } + + cmd.PersistentPreRunE = func(subCmd *cobra.Command, args []string) error { + if isClientCLI(subCmd) { + if err := overrideClientConfigHost(subCmd, cliConfig); err != nil { + return err + } + } + return nil + } + + cmd.AddCommand(ServerCommand()) + cmd.AddCommand(NamespaceCommand(cliConfig)) + cmd.AddCommand(UserCommand(cliConfig)) + cmd.AddCommand(OrganizationCommand(cliConfig)) + cmd.AddCommand(GroupCommand(cliConfig)) + cmd.AddCommand(ProjectCommand(cliConfig)) + cmd.AddCommand(RoleCommand(cliConfig)) + cmd.AddCommand(ActionCommand(cliConfig)) + cmd.AddCommand(PolicyCommand(cliConfig)) + cmd.AddCommand(configCommand()) + + // Help topics + cmdx.SetHelp(cmd) + cmd.AddCommand(cmdx.SetCompletionCmd("shield")) + cmd.AddCommand(cmdx.SetHelpTopic("environment", envHelp)) + cmd.AddCommand(cmdx.SetHelpTopic("auth", authHelp)) + cmd.AddCommand(cmdx.SetRefCmd(cmd)) + return cmd +} diff --git a/cmd/serve.go b/cmd/serve.go index adb9a2581..363823378 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -9,6 +9,7 @@ import ( "syscall" "time" + _ "github.com/authzed/authzed-go/proto/authzed/api/v0" _ "github.com/jackc/pgx/v4/stdlib" newrelic "github.com/newrelic/go-agent" "github.com/odpf/shield/core/action" @@ -34,25 +35,12 @@ import ( "github.com/odpf/salt/log" salt_server "github.com/odpf/salt/server" "github.com/pkg/profile" - cli "github.com/spf13/cobra" ) var ( ruleCacheRefreshDelay = time.Minute * 2 ) -func serveCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { - c := &cli.Command{ - Use: "serve", - Short: "Start server and proxy default on port 8080", - Example: "shield serve", - RunE: func(cmd *cli.Command, args []string) error { - return serve(logger, appConfig) - }, - } - return c -} - func serve(logger log.Logger, cfg *config.Shield) error { if profiling := os.Getenv("SHIELD_PROFILE"); profiling == "true" || profiling == "1" { defer profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook).Stop() diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 000000000..6d9731e50 --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,166 @@ +package cmd + +import ( + "fmt" + "os" + "path" + + "github.com/MakeNowJust/heredoc" + "github.com/odpf/shield/config" + "github.com/odpf/shield/internal/store/postgres/migrations" + "github.com/odpf/shield/pkg/db" + shieldlogger "github.com/odpf/shield/pkg/logger" + "github.com/spf13/cobra" + cli "github.com/spf13/cobra" +) + +func ServerCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "server ", + Aliases: []string{"s"}, + Short: "Server management", + Long: "Server management commands.", + Example: heredoc.Doc(` + $ shield server init + $ shield server start + $ shield server start -c ./config.yaml + $ shield server migrate + $ shield server migrate -c ./config.yaml + $ shield server migrate-rollback + $ shield server migrate-rollback -c ./config.yaml + `), + } + + cmd.AddCommand(serverInitCommand()) + cmd.AddCommand(serverStartCommand()) + cmd.AddCommand(serverMigrateCommand()) + cmd.AddCommand(serverMigrateRollbackCommand()) + + return cmd +} + +func serverInitCommand() *cobra.Command { + var configFile string + var resourcesURL string + var rulesURL string + + c := &cli.Command{ + Use: "init", + Short: "Initialize server", + Long: heredoc.Doc(` + Initializing server. Creating a sample of shield server config. + Default: ./config.yaml + `), + Example: "shield server init", + RunE: func(cmd *cli.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return err + } + defaultResourcesURL := fmt.Sprintf("file://%s", path.Join(cwd, "resources_config")) + defaultRulesURL := fmt.Sprintf("file://%s", path.Join(cwd, "rules")) + + if resourcesURL == "" { + resourcesURL = defaultResourcesURL + } + if rulesURL == "" { + rulesURL = defaultRulesURL + } + + if err := config.Init(resourcesURL, rulesURL, configFile); err != nil { + return err + } + + fmt.Printf("server config created: %v\n", configFile) + return nil + }, + } + + c.Flags().StringVarP(&configFile, "output", "o", "./config.yaml", "Output config file path") + c.Flags().StringVarP(&resourcesURL, "resources", "r", "", heredoc.Doc(` + URL path of resources. Full path prefixed with scheme where resources config yaml files are kept + e.g.: + local storage file "file:///tmp/resources_config" + GCS Bucket "gs://shield-bucket-example" + (default: file://{pwd}/resources_config) + `)) + c.Flags().StringVarP(&rulesURL, "rule", "u", "", heredoc.Doc(` + URL path of rules. Full path prefixed with scheme where ruleset yaml files are kept + e.g.: + local storage file "file:///tmp/rules" + GCS Bucket "gs://shield-bucket-example" + (default: file://{pwd}/rules) + `)) + + return c +} + +func serverStartCommand() *cobra.Command { + var configFile string + + c := &cli.Command{ + Use: "start", + Short: "Start server and proxy default on port 8080", + Example: "shield server start", + RunE: func(cmd *cli.Command, args []string) error { + appConfig, err := config.Load(configFile) + if err != nil { + panic(err) + } + logger := shieldlogger.InitLogger(appConfig.Log) + + return serve(logger, appConfig) + }, + } + + c.Flags().StringVarP(&configFile, "config", "c", "", "Config file path") + return c +} + +func serverMigrateCommand() *cobra.Command { + var configFile string + + c := &cli.Command{ + Use: "migrate", + Short: "Run DB Schema Migrations", + Example: "shield migrate", + RunE: func(c *cli.Command, args []string) error { + appConfig, err := config.Load(configFile) + if err != nil { + panic(err) + } + + return db.RunMigrations(db.Config{ + Driver: appConfig.DB.Driver, + URL: appConfig.DB.URL, + }, migrations.MigrationFs, migrations.ResourcePath) + }, + } + + c.Flags().StringVarP(&configFile, "config", "c", "", "Config file path") + return c +} + +func serverMigrateRollbackCommand() *cobra.Command { + var configFile string + + c := &cli.Command{ + Use: "migration-rollback", + Short: "Run DB Schema Migrations Rollback to last state", + Example: "shield migration-rollback", + RunE: func(c *cli.Command, args []string) error { + appConfig, err := config.Load(configFile) + if err != nil { + panic(err) + } + + return db.RunRollback(db.Config{ + Driver: appConfig.DB.Driver, + URL: appConfig.DB.URL, + }, migrations.MigrationFs, migrations.ResourcePath) + }, + } + + c.Flags().StringVarP(&configFile, "config", "c", "", "Config file path") + return c +} diff --git a/cmd/user.go b/cmd/user.go index d15d32c85..263d98647 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -4,17 +4,15 @@ import ( "context" "fmt" "os" - "strconv" "github.com/MakeNowJust/heredoc" - "github.com/odpf/salt/log" "github.com/odpf/salt/printer" - "github.com/odpf/shield/config" + "github.com/odpf/shield/pkg/file" shieldv1beta1 "github.com/odpf/shield/proto/v1beta1" cli "github.com/spf13/cobra" ) -func UserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func UserCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "user", Aliases: []string{"users"}, @@ -30,19 +28,22 @@ func UserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { `), Annotations: map[string]string{ "group:core": "true", + "client": "true", }, } - cmd.AddCommand(createUserCommand(logger, appConfig)) - cmd.AddCommand(editUserCommand(logger, appConfig)) - cmd.AddCommand(viewUserCommand(logger, appConfig)) - cmd.AddCommand(listUserCommand(logger, appConfig)) + cmd.AddCommand(createUserCommand(cliConfig)) + cmd.AddCommand(editUserCommand(cliConfig)) + cmd.AddCommand(viewUserCommand(cliConfig)) + cmd.AddCommand(listUserCommand(cliConfig)) + + bindFlagsFromClientConfig(cmd) return cmd } -func createUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { - var filePath string +func createUserCommand(cliConfig *Config) *cli.Command { + var filePath, header string cmd := &cli.Command{ Use: "create", @@ -59,7 +60,7 @@ func createUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command defer spinner.Stop() var reqBody shieldv1beta1.UserRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -68,15 +69,14 @@ func createUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } defer cancel() - res, err := client.CreateUser(ctx, &shieldv1beta1.CreateUserRequest{ + res, err := client.CreateUser(setCtxHeader(ctx, header), &shieldv1beta1.CreateUserRequest{ Body: &reqBody, }) if err != nil { @@ -84,18 +84,20 @@ func createUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command } spinner.Stop() - logger.Info(fmt.Sprintf("successfully created user %s with id %s", res.GetUser().GetName(), res.GetUser().GetId())) + fmt.Printf("successfully created user %s with id %s\n", res.GetUser().GetName(), res.GetUser().GetId()) return nil }, } cmd.Flags().StringVarP(&filePath, "file", "f", "", "Path to the user body file") cmd.MarkFlagRequired("file") + cmd.Flags().StringVarP(&header, "header", "H", "", "Header :") + cmd.MarkFlagRequired("header") return cmd } -func editUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func editUserCommand(cliConfig *Config) *cli.Command { var filePath string cmd := &cli.Command{ @@ -113,7 +115,7 @@ func editUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { defer spinner.Stop() var reqBody shieldv1beta1.UserRequestBody - if err := parseFile(filePath, &reqBody); err != nil { + if err := file.Parse(filePath, &reqBody); err != nil { return err } @@ -122,9 +124,8 @@ func editUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { return err } - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } @@ -140,7 +141,7 @@ func editUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { } spinner.Stop() - logger.Info(fmt.Sprintf("successfully edited user with id %s", userID)) + fmt.Printf("successfully edited user with id %s\n", userID) return nil }, } @@ -151,7 +152,7 @@ func editUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { return cmd } -func viewUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func viewUserCommand(cliConfig *Config) *cli.Command { var metadata bool cmd := &cli.Command{ @@ -168,9 +169,8 @@ func viewUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } @@ -219,7 +219,7 @@ func viewUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { return cmd } -func listUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { +func listUserCommand(cliConfig *Config) *cli.Command { cmd := &cli.Command{ Use: "list", Short: "List all users", @@ -234,9 +234,8 @@ func listUserCommand(logger log.Logger, appConfig *config.Shield) *cli.Command { spinner := printer.Spin("") defer spinner.Stop() - host := appConfig.App.Host + ":" + strconv.Itoa(appConfig.App.Port) ctx := context.Background() - client, cancel, err := createClient(ctx, host) + client, cancel, err := createClient(cmd.Context(), cliConfig.Host) if err != nil { return err } diff --git a/cmd/utils.go b/cmd/utils.go deleted file mode 100644 index f6f4168d6..000000000 --- a/cmd/utils.go +++ /dev/null @@ -1,47 +0,0 @@ -package cmd - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "path/filepath" - "strings" - - "google.golang.org/grpc/metadata" - "gopkg.in/yaml.v3" -) - -func parseFile(filePath string, v interface{}) error { - b, err := ioutil.ReadFile(filePath) - if err != nil { - return err - } - - switch filepath.Ext(filePath) { - case ".json": - if err := json.Unmarshal(b, v); err != nil { - return fmt.Errorf("invalid json: %w", err) - } - case ".yaml", ".yml": - if err := yaml.Unmarshal(b, v); err != nil { - return fmt.Errorf("invalid yaml: %w", err) - } - default: - return errors.New("unsupported file type") - } - - return nil -} - -func setCtxHeader(ctx context.Context, header string) context.Context { - s := strings.Split(header, ":") - key := s[0] - val := s[1] - - md := metadata.New(map[string]string{key: val}) - ctx = metadata.NewOutgoingContext(ctx, md) - - return ctx -} diff --git a/config/config.go b/config/config.go index 07ade0200..e1da6f1d4 100644 --- a/config/config.go +++ b/config/config.go @@ -21,7 +21,7 @@ type Shield struct { NewRelic NewRelic `yaml:"new_relic"` App server.Config `yaml:"app"` DB db.Config `yaml:"db"` - SpiceDB spicedb.Config `yaml:"spice_db"` + SpiceDB spicedb.Config `yaml:"spicedb"` } type NewRelic struct { @@ -30,11 +30,11 @@ type NewRelic struct { Enabled bool `yaml:"enabled" mapstructure:"enabled"` } -func Load() (*Shield, error) { +func Load(serverConfigfileFromFlag string) (*Shield, error) { conf := &Shield{} var options []config.LoaderOption - options = append(options, config.WithName(".shield.yaml")) + options = append(options, config.WithName("config.yaml")) options = append(options, config.WithEnvKeyReplacer(".", "_")) options = append(options, config.WithEnvPrefix("SHIELD")) if p, err := os.Getwd(); err == nil { @@ -48,6 +48,11 @@ func Load() (*Shield, error) { options = append(options, config.WithPath(filepath.Join(currentHomeDir, ".config"))) } + // override all config sources and prioritize one from file + if serverConfigfileFromFlag != "" { + options = []config.LoaderOption{config.WithFile(serverConfigfileFromFlag)} + } + l := config.NewLoader(options...) if err := l.Load(conf); err != nil { if errors.As(err, &config.ConfigFileNotFoundError{}) { diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 000000000..618f12abc --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,52 @@ +version: 1 + +# logging configuration +log: + # debug, info, warning, error, fatal - default 'info' + level: debug + +app: + port: 8000 + identity_proxy_header: X-Shield-Email + # full path prefixed with scheme where resources config yaml files are kept + # e.g.: + # local storage file "file:///tmp/resources_config" + # GCS Bucket "gs://shield/resources_config" + resources_config_path: file:///tmp/resources_config\ + # secret required to access resources config + # e.g.: + # system environment variable "env://TEST_RULESET_SECRET" + # local file "file:///opt/auth.json" + # secret string "val://user:password" + # optional + resources_config_path_secret: env://TEST_RESOURCE_CONFIG_SECRET + +db: + driver: postgres + url: postgres://shield:@localhost:5432/shield?sslmode=disable + max_query_timeout: 500ms + +spicedb: + host: spicedb.localhost + pre_shared_key: randomkey + port: 50051 + +# proxy configuration +proxy: + services: + - name: test + host: 0.0.0.0 + # port where the proxy will be listening on for requests + port: 5556 + # full path prefixed with scheme where ruleset yaml files are kept + # e.g.: + # local storage file "file:///tmp/rules" + # GCS Bucket "gs://shield/rules" + ruleset: file:///tmp/rules + # secret required to access ruleset + # e.g.: + # system environment variable "env://TEST_RULESET_SECRET" + # local file "file:///opt/auth.json" + # secret string "val://user:password" + # optional + ruleset_secret: env://TEST_RULESET_SECRET \ No newline at end of file diff --git a/config/init.go b/config/init.go new file mode 100644 index 000000000..10cc9d4cf --- /dev/null +++ b/config/init.go @@ -0,0 +1,163 @@ +package config + +import ( + "embed" + "errors" + "io/ioutil" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + _ "embed" + + "github.com/mcuadros/go-defaults" + "github.com/odpf/shield/internal/proxy" + "github.com/odpf/shield/pkg/file" + "gopkg.in/yaml.v2" +) + +//go:embed resources_config/* +var resourcesConfig embed.FS + +//go:embed rules/* +var rulesConfig embed.FS + +func Init(resourcesURL, rulesURL, configFile string) error { + if file.Exist(configFile) { + return errors.New("config file already exists") + } + + cfg := &Shield{} + + defaults.SetDefaults(cfg) + + if err := initResourcesPath(resourcesURL); err != nil { + return err + } + if err := initRulesPath(rulesURL); err != nil { + return err + } + + cfg.App.RulesPath = rulesURL + cfg.App.ResourcesConfigPath = resourcesURL + // sample proxy + cfg.Proxy = proxy.ServicesConfig{ + Services: []proxy.Config{ + { + Name: "base", + Host: "0.0.0.0", + Port: 5556, + RulesPath: rulesURL, + }, + }, + } + + data, err := yaml.Marshal(cfg) + if err != nil { + return err + } + + if _, err := os.Stat(configFile); os.IsNotExist(err) { + if !file.DirExists(configFile) { + _ = os.MkdirAll(filepath.Dir(configFile), 0755) + } + } + + if err := ioutil.WriteFile(configFile, data, 0655); err != nil { + return err + } + + return nil +} + +func initResourcesPath(resURL string) error { + resourceURL, err := url.Parse(resURL) + if err != nil { + return err + } + + if resourceURL.Scheme != "file" { + // skip creating + return nil + } + + resourcesPath := resourceURL.Path + if !file.DirExists(resourcesPath) { + _ = os.MkdirAll(resourcesPath, 0755) + } + + files, err := ioutil.ReadDir(resourcesPath) + if err != nil { + return err + } + + for _, f := range files { + if strings.HasSuffix(f.Name(), ".yaml") || strings.HasSuffix(f.Name(), ".yml") { + // skip creating + return nil + } + } + + resourceYaml, err := resourcesConfig.ReadFile("resources_config/resources.yaml") + if err != nil { + return err + } + + if err := ioutil.WriteFile(path.Join(resourcesPath, "resources.yaml"), resourceYaml, 0655); err != nil { + return err + } + + return nil +} + +func initRulesPath(rURL string) error { + rulesURL, err := url.Parse(rURL) + if err != nil { + return err + } + + if rulesURL.Scheme != "file" { + // skip creating + return nil + } + + rulesPath := rulesURL.Path + + if !file.DirExists(rulesPath) { + _ = os.MkdirAll(rulesPath, 0755) + } + + files, err := ioutil.ReadDir(rulesPath) + if err != nil { + return err + } + + for _, f := range files { + if strings.HasSuffix(f.Name(), ".yaml") || strings.HasSuffix(f.Name(), ".yml") { + // skip creating + return nil + } + } + + ruleRestYaml, err := rulesConfig.ReadFile("rules/sample.rest.yaml") + if err != nil { + return err + } + + if err := ioutil.WriteFile(path.Join(rulesPath, "sample.rest.yaml"), ruleRestYaml, 0655); err != nil { + return err + } + + ruleRestGrpc, err := resourcesConfig.ReadFile("rules/sample.grpc.yaml") + if err != nil { + return err + } + + if err := ioutil.WriteFile(path.Join(rulesPath, "sample.grpc.yaml"), ruleRestGrpc, 0655); err != nil { + return err + } + + return nil +} diff --git a/config/rules/sample.grpc.yaml b/config/rules/sample.grpc.yaml index 56b8ae4b2..9e734e3f0 100644 --- a/config/rules/sample.grpc.yaml +++ b/config/rules/sample.grpc.yaml @@ -1,6 +1,5 @@ rules: - - - frontend: + - frontend: url: "proto.v1.RuntimeService/" methods: ["POST"] backend: diff --git a/core/namespace/namespace.go b/core/namespace/namespace.go index 5c45e4e68..3a5881a4d 100644 --- a/core/namespace/namespace.go +++ b/core/namespace/namespace.go @@ -33,7 +33,7 @@ func IsSystemNamespaceID(nsID string) bool { return strListHas(systemIdsDefinition, nsID) } -//postgres://shield:@:5432/ +// postgres://shield:@:5432/ func CreateID(backend, resourceType string) string { return fmt.Sprintf("%s_%s", backend, resourceType) } diff --git a/core/resource/resource.go b/core/resource/resource.go index 63206c8ea..ac85ae11f 100644 --- a/core/resource/resource.go +++ b/core/resource/resource.go @@ -45,9 +45,7 @@ type Resource struct { UpdatedAt time.Time } -/* - /project/uuid/ -*/ +// /project/uuid/ func (res Resource) CreateURN() string { isSystemNS := namespace.IsSystemNamespaceID(res.NamespaceID) if isSystemNS { diff --git a/docs/docs/getting-started/configurations.md b/docs/docs/getting-started/configurations.md index 7f944bdb3..9f5f66782 100644 --- a/docs/docs/getting-started/configurations.md +++ b/docs/docs/getting-started/configurations.md @@ -1,6 +1,6 @@ ## Configurations -Shield can be configured with .shield.yaml file. An example of such is: +Shield can be configured with config.yaml file. An example of such is: ```yaml version: 1 diff --git a/go.mod b/go.mod index f8ce74e8d..abaab40ed 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,6 @@ require ( google.golang.org/grpc v1.46.0 google.golang.org/protobuf v1.28.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b - gotest.tools v2.2.0+incompatible ) require ( diff --git a/internal/store/postgres/policy_repository.go b/internal/store/postgres/policy_repository.go index 03c7fb27c..9f9dc07e8 100644 --- a/internal/store/postgres/policy_repository.go +++ b/internal/store/postgres/policy_repository.go @@ -119,7 +119,7 @@ func (r PolicyRepository) List(ctx context.Context) ([]policy.Policy, error) { return transformedPolicies, nil } -//TODO this is actually upsert +// TODO this is actually upsert func (r PolicyRepository) Create(ctx context.Context, pol policy.Policy) (string, error) { // TODO need to check actionID != "" diff --git a/internal/store/postgres/role_repository.go b/internal/store/postgres/role_repository.go index 8fba54368..6ac176ecf 100644 --- a/internal/store/postgres/role_repository.go +++ b/internal/store/postgres/role_repository.go @@ -69,7 +69,7 @@ func (r RoleRepository) Get(ctx context.Context, id string) (role.Role, error) { return transformedRole, nil } -//TODO this is actually an upsert +// TODO this is actually an upsert func (r RoleRepository) Create(ctx context.Context, rl role.Role) (string, error) { if rl.ID == "" { return "", role.ErrInvalidID diff --git a/main.go b/main.go index 6c221e927..a6b0d632f 100644 --- a/main.go +++ b/main.go @@ -4,22 +4,17 @@ import ( "fmt" "os" - shieldlogger "github.com/odpf/shield/pkg/logger" - "github.com/odpf/shield/cmd" - "github.com/odpf/shield/config" _ "github.com/authzed/authzed-go/proto/authzed/api/v0" ) func main() { - appConfig, err := config.Load() + cliConfig, err := cmd.LoadConfig() if err != nil { - panic(err) + cliConfig = &cmd.Config{} } - logger := shieldlogger.InitLogger(appConfig.Log) - - if err := cmd.New(logger, appConfig).Execute(); err != nil { + if err := cmd.New(cliConfig).Execute(); err != nil { fmt.Printf("%+v", err) os.Exit(1) } diff --git a/pkg/body_extractor/grpc_payload.go b/pkg/body_extractor/grpc_payload.go index 89e77ddcb..c9818fd87 100644 --- a/pkg/body_extractor/grpc_payload.go +++ b/pkg/body_extractor/grpc_payload.go @@ -134,10 +134,11 @@ type grpcRequestParser struct { // format. The caller owns the returned msg memory. // // If there is an error, possible values are: -// * io.EOF, when no messages remain -// * io.ErrUnexpectedEOF -// * of type transport.ConnectionError -// * an error from the status package +// - io.EOF, when no messages remain +// - io.ErrUnexpectedEOF +// - of type transport.ConnectionError +// - an error from the status package +// // No other error values or types must be returned, which also means // that the underlying io.Reader must not return an incompatible // error. diff --git a/pkg/file/file.go b/pkg/file/file.go new file mode 100644 index 000000000..022cfa746 --- /dev/null +++ b/pkg/file/file.go @@ -0,0 +1,53 @@ +package file + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +// Exist checks whether a file with filename exists +// return true if exists, else false +func Exist(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +// DirExists checks whether a directory path exists +// return true if exists, else false +func DirExists(path string) bool { + f, err := os.Stat(path) + return err == nil && f.IsDir() +} + +// Parse tries to read json or yaml file +// and transform the content into a struct passed +// in the 2nd argument +// File extension matters, only file with extension +// json, yaml, or yml that is parsable +func Parse(filePath string, v interface{}) error { + b, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + + switch filepath.Ext(filePath) { + case ".json": + if err := json.Unmarshal(b, v); err != nil { + return fmt.Errorf("invalid json: %w", err) + } + case ".yaml", ".yml": + if err := yaml.Unmarshal(b, v); err != nil { + return fmt.Errorf("invalid yaml: %w", err) + } + default: + return errors.New("unsupported file type") + } + + return nil +} diff --git a/pkg/metadata/metadata.go b/pkg/metadata/metadata.go index 57976302f..47f16f1ff 100644 --- a/pkg/metadata/metadata.go +++ b/pkg/metadata/metadata.go @@ -6,8 +6,12 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) +// Metadata is a structure to store dynamic values in +// shield. it could be use as an additional information +// of a specific entity type Metadata map[string]any +// ToStructPB transforms Metadata to *structpb.Struct func (m Metadata) ToStructPB() (*structpb.Struct, error) { newMap := make(map[string]interface{}) @@ -18,6 +22,7 @@ func (m Metadata) ToStructPB() (*structpb.Struct, error) { return structpb.NewStruct(newMap) } +// Build transforms a Metadata from map[string]interface{} func Build(m map[string]interface{}) (Metadata, error) { newMap := make(Metadata) diff --git a/pkg/uuid/uuid.go b/pkg/uuid/uuid.go index aa21ce8ba..248433b73 100644 --- a/pkg/uuid/uuid.go +++ b/pkg/uuid/uuid.go @@ -2,9 +2,12 @@ package uuid import "github.com/google/uuid" -// type alias +// NewString is type alias to `github.com/google/uuid`.NewString var NewString = uuid.NewString +// IsValid returns true if passed string in uuid format +// defined by `github.com/google/uuid`.Parse +// else return false func IsValid(key string) bool { _, err := uuid.Parse(key) return err == nil