diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..00efbc2 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,105 @@ +version: 2 + +experimental: + notify: + branches: + only: + - master + +defaults: + environment: &environment + CIRCLE_TEST_REPORTS: /tmp/circle-reports + CIRCLE_ARTIFACTS: /tmp/circle-artifacts + COMMON_GO_PACKAGES: > + github.com/jstemmer/go-junit-report + github.com/kyoh86/richgo + + build_steps: &build_steps + working_directory: &working_dir /go/src/github.com/launchdarkly/ldc + steps: + - checkout + - run: go get -u $COMMON_GO_PACKAGES + - run: curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + - run: dep ensure -dry-run + - run: make init + - run: make lint + - run: + name: Set up Code Climate test-reporter + command: | + curl -sS -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + - run: + name: Run tests + command: | + mkdir -p $CIRCLE_TEST_REPORTS + mkdir -p $CIRCLE_ARTIFACTS + trap "go-junit-report < $CIRCLE_ARTIFACTS/report.txt > $CIRCLE_TEST_REPORTS/junit.xml" EXIT + if [ -z "$DISABLE_COVERAGE" ]; then + go_cover_args="-covermode=atomic -coverpkg=./... -coverprofile /tmp/circle-artifacts/coverage.txt" + fi + go test -race $go_cover_args -v $(go list ./... | grep -v /vendor/) | tee >(richgo testfilter) > $CIRCLE_ARTIFACTS/report.txt + if [[ -z "$DISABLE_COVERAGE" && -n "$CC_TEST_REPORTER_ID" ]]; then + ./cc-test-reporter format-coverage $CIRCLE_ARTIFACTS/coverage.txt -t gocov --output $CIRCLE_ARTIFACTS/coverage.json + ./cc-test-reporter upload-coverage --input $CIRCLE_ARTIFACTS/coverage.json + fi + - run: + name: Generate coverage report + command: | + if [ -z "$DISABLE_COVERAGE" ]; then + go tool cover -html=$CIRCLE_ARTIFACTS/coverage.txt -o $CIRCLE_ARTIFACTS/coverage.html + fi + when: always + - store_test_results: + path: /tmp/circle-reports + - store_artifacts: + path: /tmp/circle-artifacts + +jobs: + go-test: + docker: + - &build_image + image: circleci/golang:1.11 + environment: + <<: *environment + + <<: *build_steps + + test-publish: + docker: + - <<: *build_image + + working_directory: *working_dir + steps: + - checkout + - run: make release-snapshot + - store_artifacts: + path: dist/ + + publish: + docker: + - <<: *build_image + + working_directory: *working_dir + steps: + - checkout + - run: + name: Releasing and publishing + command: | + make release + - store_artifacts: + path: dist/ + +workflows: + version: 2 + test: + jobs: + - go-test + - test-publish + - publish: + filters: + tags: + only: /\d+\.\d+\.\d+(-.*)?/ + branches: + only: /v\d+/ + requires: + - go-test diff --git a/.gitignore b/.gitignore index a16e54d..89a6930 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -/ldc \ No newline at end of file +/ldc +/bin +/dist + diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..4b77b78 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,28 @@ +project_name: ldc + +run: + deadline: 120s + tests: false + +linters: + enable-all: true + disable: + - gochecknoinits + - gochecknoglobals + - gofmt + - lll + - maligned + - prealloc + - unparam + fast: false + +linter-settings: + goimports: + local-prefixes: github.com/launchdarkly/ldc + +issues: + exclude: + - "G104: Errors unhandled." # Let errcheck handle these + exclude-use-default: false + max-same-issues: 1000 + max-per-linter: 1000 diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..ca85de7 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,10 @@ +project_name: ldc + +builds: + - env: + - CGO_ENABLED=0 + main: . + binary: ldc + # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`. + ldflags: + - -s -w -X cmd.Version={{.Version}} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3471e3e --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +GOLANGCI_VERSION=v1.12.5 + +SHELL=/bin/bash + +test: lint + go test ./... + +lint: + ./bin/golangci-lint run ./... + +init: + curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s $(GOLANGCI_VERSION) + +RELEASE_CMD=curl -sL https://git.io/goreleaser | bash -s -- --rm-dist + +publish: + $(RELEASE_CMD) + +publish-snapshot: + $(RELEASE_CMD) + +release: + $(RELEASE_CMD) --skip-publish --skip-validate + +release-snapshot: + $(RELEASE_CMD) --skip-publish --skip-validate --snapshot + +.PHONY: docker init lint publish-snapshot release release-snapshot diff --git a/api/api.go b/api/api.go index c7b415d..d603e74 100644 --- a/api/api.go +++ b/api/api.go @@ -4,22 +4,38 @@ import ( "context" "net/http" - "github.com/launchdarkly/api-client-go" + ldapi "github.com/launchdarkly/api-client-go" ) +// Auth is the authorization context used by the ali client var Auth context.Context + +// Client is the api client var Client *ldapi.APIClient -const DefaultServer = "https://app.launchdarkly.com/api/v2" +const defaultServer = "https://app.launchdarkly.com/api/v2" +// CurrentToken is the api token var CurrentToken string + +// CurrentServer is the url of the api to use var CurrentServer string + +// CurrentProject is the project to use var CurrentProject = "default" + +// CurrentEnvironment is the environment to use var CurrentEnvironment = "production" -type LoggingTransport struct{} +// HTTPClient is an underlying http client with logging transport +var HTTPClient *http.Client + +// UserAgent is the current user agent for this version of the command +var UserAgent string -func (lt *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { +type loggingTransport struct{} + +func (lt *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { resp, err := http.DefaultTransport.RoundTrip(req) // TODO this is bad, don't do this @@ -27,40 +43,37 @@ func (lt *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) return resp, err } -var HttpClient *http.Client - -var UserAgent string - +// Initialize sets up api for use with a given user agent string func Initialize(userAgent string) { UserAgent = userAgent - HttpClient = &http.Client{ - Transport: &LoggingTransport{}, + HTTPClient = &http.Client{ + Transport: &loggingTransport{}, } Client = ldapi.NewAPIClient(&ldapi.Configuration{ - HTTPClient: HttpClient, + HTTPClient: HTTPClient, UserAgent: UserAgent, }) } func init() { - SetServer(DefaultServer) + SetServer(defaultServer) } -// TODO +// SetServer sets the server url to use func SetServer(newServer string) { CurrentServer = newServer Client = ldapi.NewAPIClient(&ldapi.Configuration{ BasePath: newServer, HTTPClient: &http.Client{ - Transport: &LoggingTransport{}, + Transport: &loggingTransport{}, }, UserAgent: "ldc/0.0.1/go", }) - } +// SetToken sets the authorization token func SetToken(newToken string) { CurrentToken = newToken Auth = context.WithValue(context.Background(), ldapi.ContextAPIKey, ldapi.APIKey{ diff --git a/cmd/audit_log.go b/cmd/audit_log.go index b53f249..3a1f137 100644 --- a/cmd/audit_log.go +++ b/cmd/audit_log.go @@ -11,7 +11,7 @@ import ( "github.com/launchdarkly/ldc/api" ) -func AddAuditLogCommands(shell *ishell.Shell) { +func addAuditLogCommands(shell *ishell.Shell) { root := &ishell.Cmd{ Name: "log", Help: "search audit log entries", @@ -38,7 +38,7 @@ func AddAuditLogCommands(shell *ishell.Shell) { } table.Render() if buf.Len() > 1000 { - c.ShowPaged(buf.String()) + c.Err(c.ShowPaged(buf.String())) } else { c.Println(buf.String()) } diff --git a/cmd/config.go b/cmd/config.go index 3c62a39..157a4d2 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -2,12 +2,13 @@ package cmd import ( "errors" - "gopkg.in/abiosoft/ishell.v2" + + ishell "gopkg.in/abiosoft/ishell.v2" "github.com/launchdarkly/ldc/api" ) -func listConfigs() (map[string]Config, error) { +func listConfigs() (map[string]config, error) { return configFile, nil } @@ -32,7 +33,7 @@ func configCompleter(args []string) []string { return completions } -func getConfigArg(c *ishell.Context) (string, *Config) { +func getConfigArg(c *ishell.Context) (string, *config) { configs, err := listConfigs() if err != nil { c.Err(err) @@ -53,19 +54,15 @@ func getConfigArg(c *ishell.Context) (string, *Config) { return options[choice], &config } - var foundConfig *Config configKey := c.Args[0] for c, v := range configs { if c == configKey { - foundConfig = &v - break + return c, &v // nolint:scopelint // ok because we break } } - if foundConfig == nil { - c.Err(errors.New("config does not exist")) - return "", nil - } - return configKey, foundConfig + + c.Err(errors.New("config does not exist")) + return "", nil } func selectConfig(c *ishell.Context) { @@ -78,10 +75,10 @@ func selectConfig(c *ishell.Context) { printCurrentSettings(c) } -func setConfig(name string, config Config) { +func setConfig(name string, config config) { currentConfig = name api.CurrentProject = config.DefaultProject api.CurrentEnvironment = config.DefaultEnvironment - api.SetToken(config.ApiToken) + api.SetToken(config.APIToken) api.SetServer(config.Server) } diff --git a/cmd/environment.go b/cmd/environment.go index 891e408..6f92354 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -9,10 +9,11 @@ import ( ishell "gopkg.in/abiosoft/ishell.v2" ldapi "github.com/launchdarkly/api-client-go" + "github.com/launchdarkly/ldc/api" ) -func AddEnvironmentCommands(shell *ishell.Shell) { +func addEnvironmentCommands(shell *ishell.Shell) { root := &ishell.Cmd{ Name: "environments", Aliases: []string{"environment", "env", "envs", "e"}, @@ -144,7 +145,7 @@ func showEnvironments(c *ishell.Context) { table.SetRowLine(true) table.Render() if buf.Len() > 1000 { - c.ShowPaged(buf.String()) + c.Err(c.ShowPaged(buf.String())) } else { c.Print(buf.String()) } @@ -159,31 +160,26 @@ func getEnvironmentArg(c *ishell.Context) *ldapi.Environment { c.Err(err) return nil } - var foundEnvironment *ldapi.Environment var environmentKey string if len(c.Args) > 0 { environmentKey = c.Args[0] for _, environment := range environments { if environment.Key == environmentKey { - copy := environment - foundEnvironment = © + return &environment // nolint:scopelint // ok because we return } } - } else { - // TODO LOL - options, err := listEnvironmentKeys() - if err != nil { - c.Err(err) - return nil - } - choice := c.MultiChoice(options, "Choose an environment") - if choice < 0 { - return nil - } - foundEnvironment = &environments[choice] - environmentKey = foundEnvironment.Key } - return foundEnvironment + + options, err := listEnvironmentKeys() + if err != nil { + c.Err(err) + return nil + } + choice := c.MultiChoice(options, "Choose an environment") + if choice < 0 { + return nil + } + return &environments[choice] } func createEnvironment(c *ishell.Context) { @@ -199,7 +195,7 @@ func createEnvironment(c *ishell.Context) { key = c.Args[0] name = c.Args[1] default: - c.Err(errors.New("too many arguments. Expected arguments are: key [name].")) + c.Err(errors.New(`expected arguments are "key [name]""`)) return } _, err := api.Client.EnvironmentsApi.PostEnvironment(api.Auth, api.CurrentProject, ldapi.EnvironmentPost{Key: key, Name: name, Color: "000000"}) diff --git a/cmd/flag.go b/cmd/flag.go index 09ccc6a..40e6406 100644 --- a/cmd/flag.go +++ b/cmd/flag.go @@ -6,16 +6,17 @@ import ( "fmt" "time" - ldapi "github.com/launchdarkly/api-client-go" - "github.com/launchdarkly/ldc/api" - "github.com/olekukonko/tablewriter" ishell "gopkg.in/abiosoft/ishell.v2" + + ldapi "github.com/launchdarkly/api-client-go" + + "github.com/launchdarkly/ldc/api" ) var flagCompleter = makeCompleter(emptyOnError(listFlagKeys)) -func AddFlagCommands(shell *ishell.Shell) { +func addFlagCommands(shell *ishell.Shell) { root := &ishell.Cmd{ Name: "flags", @@ -119,31 +120,29 @@ func getFlagArg(c *ishell.Context, pos int) *ldapi.FeatureFlag { c.Err(err) return nil } - var foundFlag *ldapi.FeatureFlag - var flagKey string + if len(c.Args) > pos { - flagKey = c.Args[pos] + flagKey := c.Args[pos] for _, flag := range flags { if flag.Key == flagKey { - copy := flag - foundFlag = © + return &flag // nolint:scopelint // ok because we return here } } - } else { - // TODO LOL - options, err := listFlagKeys() - if err != nil { - c.Err(err) - return nil - } - choice := c.MultiChoice(options, "Choose a flag: ") - if choice < 0 { - return nil - } - foundFlag = &flags[choice] - flagKey = foundFlag.Key + return nil + } + + options, err := listFlagKeys() + if err != nil { + c.Err(err) + return nil + } + + choice := c.MultiChoice(options, "Choose a flag: ") + if choice < 0 { + return nil } - return foundFlag + + return &flags[choice] } func listFlags() ([]ldapi.FeatureFlag, error) { @@ -191,14 +190,15 @@ func showFlags(c *ishell.Context) { } table.Render() if buf.Len() > 1000 { - c.ShowPaged(buf.String()) + + c.Err(c.ShowPaged(buf.String())) } else { c.Print(buf.String()) } } func renderFlag(c *ishell.Context, flag ldapi.FeatureFlag) { - if renderJson(c) { + if renderJSON(c) { data, err := json.MarshalIndent(flag, "", " ") if err != nil { c.Err(err) @@ -260,7 +260,7 @@ func createToggleFlag(c *ishell.Context) { c.Err(err) return } - if renderJson(c) { + if renderJSON(c) { renderFlag(c, flag) } } diff --git a/cmd/goal.go b/cmd/goal.go index ce33208..e9b3a8f 100644 --- a/cmd/goal.go +++ b/cmd/goal.go @@ -7,17 +7,17 @@ import ( "strconv" ldapi "github.com/launchdarkly/api-client-go" - "github.com/launchdarkly/ldc/api" - - "github.com/launchdarkly/ldc/goal_api" "github.com/olekukonko/tablewriter" ishell "gopkg.in/abiosoft/ishell.v2" + + "github.com/launchdarkly/ldc/api" + "github.com/launchdarkly/ldc/goalapi" ) var goalCompleter = makeCompleter(emptyOnError(listGoalNames)) -func AddGoalsCommands(shell *ishell.Shell) { +func addGoalCommands(shell *ishell.Shell) { root := &ishell.Cmd{ Name: "goals", @@ -81,13 +81,13 @@ func AddGoalsCommands(shell *ishell.Shell) { shell.AddCmd(root) } -func getGoalArg(c *ishell.Context) *goal_api.Goal { - goals, _ := goal_api.GetGoals() +func getGoalArg(c *ishell.Context) *goalapi.Goal { + goals, _ := goalapi.GetGoals() if len(c.Args) > 0 { goalKey := c.Args[0] for _, g := range goals { - if g.Id == goalKey || g.Name == goalKey { - foundGoal, err := goal_api.GetGoal(g.Id) + if g.ID == goalKey || g.Name == goalKey { + foundGoal, err := goalapi.GetGoal(g.ID) if err != nil { c.Err(err) return nil @@ -106,13 +106,13 @@ func getGoalArg(c *ishell.Context) *goal_api.Goal { if choice < 0 { return nil } - foundGoal, _ := goal_api.GetGoal(options[choice]) + foundGoal, _ := goalapi.GetGoal(options[choice]) return foundGoal } func listGoalNames() ([]string, error) { var keys []string - g, err := goal_api.GetGoals() + g, err := goalapi.GetGoals() if err != nil { return nil, err } @@ -133,29 +133,29 @@ func showGoals(c *ishell.Context) { return } - goals, err := goal_api.GetGoals() + goals, err := goalapi.GetGoals() if err != nil { c.Err(err) return } buf := bytes.Buffer{} table := tablewriter.NewWriter(&buf) - table.SetHeader([]string{"Name", "Id", "Description", "Kind", "Attached Flags"}) + table.SetHeader([]string{"Name", "ID", "Description", "Kind", "Attached Flags"}) for _, goal := range goals { - table.Append([]string{goal.Name, goal.Id, goal.Description, goal.Kind, strconv.Itoa(goal.AttachedFeatureCount)}) + table.Append([]string{goal.Name, goal.ID, goal.Description, goal.Kind, strconv.Itoa(goal.AttachedFeatureCount)}) } table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetAutoWrapText(false) table.Render() if buf.Len() > 1000 { - c.ShowPaged(buf.String()) + c.Err(c.ShowPaged(buf.String())) } else { c.Print(buf.String()) } } -func renderGoal(c *ishell.Context, goal *goal_api.Goal) { - if renderJson(c) { +func renderGoal(c *ishell.Context, goal *goalapi.Goal) { + if renderJSON(c) { data, err := json.MarshalIndent(goal, "", " ") if err != nil { c.Err(err) @@ -204,7 +204,7 @@ func editGoal(c *ishell.Context) { return } - _, err = goal_api.PatchGoal(goal.Id, *patchComment) + _, err = goalapi.PatchGoal(goal.ID, *patchComment) if err != nil { c.Err(err) return @@ -225,12 +225,12 @@ func createCustomGoal(c *ishell.Context) { c.Print("Key: ") key = c.ReadLine() } - goal := goal_api.Goal{ + goal := goalapi.Goal{ Name: name, Kind: "custom", Key: &key, } - newGoal, err := goal_api.CreateGoal(goal) + newGoal, err := goalapi.CreateGoal(goal) if err != nil { c.Err(err) return @@ -238,7 +238,7 @@ func createCustomGoal(c *ishell.Context) { if isInteractive(c) { c.Println("Created goal") } - if renderJson(c) { + if renderJSON(c) { renderGoal(c, newGoal) } } @@ -246,7 +246,7 @@ func createCustomGoal(c *ishell.Context) { func deleteGoal(c *ishell.Context) { goal := getGoalArg(c) - err := goal_api.DeleteGoal(goal.Id) + err := goalapi.DeleteGoal(goal.ID) if err != nil { c.Err(err) } else { @@ -257,26 +257,25 @@ func deleteGoal(c *ishell.Context) { func boolToCheck(b bool) string { if b { return "X" - } else { - return " " } + return " " } func attachGoal(c *ishell.Context) { - var goal *goal_api.Goal + var goal *goalapi.Goal var flag *ldapi.FeatureFlag goal = getGoalArg(c) flag = getFlagArg(c, 1) for _, g := range flag.GoalIds { - if g == goal.Id { + if g == goal.ID { c.Println("Goal already attached") return } } patchComment := ldapi.PatchComment{ - Patch: []ldapi.PatchOperation{{Op: "add", Path: "/goalIds/-", Value: interfacePtr(goal.Id)}}, + Patch: []ldapi.PatchOperation{{Op: "add", Path: "/goalIds/-", Value: interfacePtr(goal.ID)}}, } _, _, err := api.Client.FeatureFlagsApi.PatchFeatureFlag(api.Auth, api.CurrentProject, flag.Key, patchComment) if err != nil { @@ -286,15 +285,15 @@ func attachGoal(c *ishell.Context) { } func detachGoal(c *ishell.Context) { - var goal *goal_api.Goal + var goal *goalapi.Goal var flag *ldapi.FeatureFlag goal = getGoalArg(c) flag = getFlagArg(c, 1) var pos *int for p, g := range flag.GoalIds { - if g == goal.Id { - pos = &p + if g == goal.ID { + pos = &p // nolint:scopelint // ok because we break break } } @@ -338,11 +337,11 @@ func detachGoal(c *ishell.Context) { // return nil // } // -// goals, _ := goal_api.GetGoals() +// goals, _ := goalapi.GetGoals() // goalKey := args[0] // for _, g := range goals { -// if g.Id == goalKey || g.Name == goalKey { -// goal, err := goal_api.GetGoal(g.Id) +// if g.ID == goalKey || g.Name == goalKey { +// goal, err := goalapi.GetGoal(g.ID) // if err != nil { // return nil // } diff --git a/cmd/project.go b/cmd/project.go index 8ee67ee..481a2c4 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -10,10 +10,11 @@ import ( ishell "gopkg.in/abiosoft/ishell.v2" ldapi "github.com/launchdarkly/api-client-go" + "github.com/launchdarkly/ldc/api" ) -func AddProjectCommands(shell *ishell.Shell) { +func addProjectCommands(shell *ishell.Shell) { root := &ishell.Cmd{ Name: "projects", Aliases: []string{"project"}, @@ -95,7 +96,7 @@ func listProjectsTable(c *ishell.Context) { } table.Render() if buf.Len() > 1000 { - c.ShowPaged(buf.String()) + c.Err(c.ShowPaged(buf.String())) } else { c.Print(buf.String()) } @@ -130,33 +131,29 @@ func getProjectArg(c *ishell.Context) *ldapi.Project { c.Err(err) return nil } - var foundProject *ldapi.Project if len(c.Args) > 0 { projectKey := c.Args[0] for _, project := range projects { if project.Key == projectKey { - copy := project - foundProject = © + return &project // nolint:scopelint // ok because we return immediately } } - if foundProject == nil { - c.Err(fmt.Errorf("Project %s does not exist\n", projectKey)) - return nil - } - } else { - // TODO LOL - options, err := listProjectKeys() - if err != nil { - c.Err(err) - return nil - } - choice := c.MultiChoice(options, "Choose a project") - if choice < 0 { - return nil - } - foundProject = &projects[choice] + c.Err(fmt.Errorf(`project "%s" does not exist`, projectKey)) + return nil + } + + options, err := listProjectKeys() + if err != nil { + c.Err(err) + return nil + } + + choice := c.MultiChoice(options, "Choose a project") + if choice < 0 { + return nil } - return foundProject + + return &projects[choice] } func createProject(c *ishell.Context) { @@ -172,7 +169,7 @@ func createProject(c *ishell.Context) { key = c.Args[0] name = c.Args[1] default: - c.Err(errors.New("too many arguments. Expected arguments are: key [name].")) + c.Err(errors.New(`expected arguments are "key [name]"`)) return } // TODO: openapi should be updated to return the new project @@ -180,7 +177,7 @@ func createProject(c *ishell.Context) { c.Err(err) return } - if !renderJson(c) { + if !renderJSON(c) { c.Printf("Created project %s\n", key) } project, _, err := api.Client.ProjectsApi.GetProject(api.Auth, key) @@ -189,7 +186,7 @@ func createProject(c *ishell.Context) { return } switchToProject(c, &project) - if renderJson(c) { + if renderJSON(c) { data, err := json.MarshalIndent(project, "", " ") if err != nil { c.Err(err) @@ -218,10 +215,3 @@ func deleteProject(c *ishell.Context) { c.Printf("Deleted project %s\n", project.Key) } } - -func updateProject(c *ishell.Context) { - //??? - // this sucks, json patch - //api.Client.ProjectsApi.PatchProject(api.Auth, "abc" - -} diff --git a/cmd/root.go b/cmd/root.go index 104eb27..afc2891 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,15 +23,15 @@ var cfgFile string var rootCmd = &cobra.Command{ Use: "ldc", Short: "ldc is a command-line api client for LaunchDarkly", - PersistentPreRun: PreRunCmd, - Run: RootCmd, + PersistentPreRun: preRunCmd, + Run: runRootCmd, } // rootCmd represents the base command when called without any subcommands var shellCmd = &cobra.Command{ Use: "shell", Short: "start an interactive shell", - Run: ShellCmd, + Run: runShellCmd, } // Execute adds all child commands to the root command and sets flags appropriately. @@ -47,14 +47,18 @@ func Execute() { } } -type Config struct { - ApiToken string - Server string - DefaultProject string +type config struct { + // APIToken is the authorization token + APIToken string + // Server is the api url (.../v2) + Server string + // DefaultProject is the initial project to use + DefaultProject string + // DefaultEnvironment is the initial environment to use DefaultEnvironment string } -var configFile map[string]Config +var configFile map[string]config var configViper *viper.Viper @@ -101,10 +105,12 @@ func init() { viper.AutomaticEnv() viper.SetEnvPrefix("ldc") viper.SetConfigName("ldc") - viper.BindPFlags(pflag.CommandLine) + if err := viper.BindPFlags(pflag.CommandLine); err != nil { + panic(err) + } } -func PreRunCmd(cmd *cobra.Command, args []string) { +func preRunCmd(cmd *cobra.Command, args []string) { configs, err := listConfigs() if err != nil { configs = nil @@ -157,21 +163,24 @@ func PreRunCmd(cmd *cobra.Command, args []string) { } } -func AddTokenCommand(shell *ishell.Shell) { +func addTokenCommands(shell *ishell.Shell) { root := &ishell.Cmd{ Name: "token", Help: "set api key", Func: func(c *ishell.Context) { - var token string - if len(c.Args) == 1 { - token = c.Args[0] - } if len(c.Args) > 1 { c.Err(errors.New("Only one argument, the api key, is allowed")) return } - c.Print("API Key: ") - token = c.ReadPassword() + + var token string + if len(c.Args) == 1 { + token = c.Args[0] + } else { + c.Print("API Key: ") + token = c.ReadPassword() + } + api.SetToken(token) printCurrentToken(c) }, @@ -200,7 +209,7 @@ func createShell(interactive bool) *ishell.Shell { Name: "json", Help: "set json mode", Completer: boolCompleter, - Func: setJsonMode, + Func: setJSONMode, }) shell.AddCmd(&ishell.Cmd{ @@ -281,34 +290,34 @@ func createShell(interactive bool) *ishell.Shell { Help: "Run shell", }) - AddFlagCommands(shell) - AddProjectCommands(shell) - AddEnvironmentCommands(shell) - AddAuditLogCommands(shell) - AddTokenCommand(shell) - AddGoalsCommands(shell) + addFlagCommands(shell) + addProjectCommands(shell) + addEnvironmentCommands(shell) + addAuditLogCommands(shell) + addTokenCommands(shell) + addGoalCommands(shell) - isJson := viper.GetBool("json") - shell.Set(JSON, isJson) - if !isJson { + isJSON := viper.GetBool("json") + shell.Set(cJSON, isJSON) + if !isJSON { if configViper.ConfigFileUsed() != "" { fmt.Printf("Using config file: %s\n", configViper.ConfigFileUsed()) } } - shell.Set(EDITOR, "vi") - if editor := os.Getenv("EDITOR"); editor != "" { - shell.Set(EDITOR, editor) + shell.Set(cEDITOR, "vi") + if editor := os.Getenv("cEDITOR"); editor != "" { + shell.Set(cEDITOR, editor) } - shell.Set(INTERACTIVE, interactive) + shell.Set(cINTERACTIVE, interactive) return shell } -func RootCmd(cmd *cobra.Command, args []string) { +func runRootCmd(cmd *cobra.Command, args []string) { shell := createShell(false) if len(args) == 0 { - cmd.Usage() + _ = cmd.Usage() fmt.Print(shell.HelpText()) os.Exit(0) } @@ -319,10 +328,10 @@ func RootCmd(cmd *cobra.Command, args []string) { } } -func ShellCmd(cmd *cobra.Command, args []string) { +func runShellCmd(cmd *cobra.Command, args []string) { shell := createShell(true) shell.Printf("LaunchDarkly CLI %s\n", Version) - shell.Process("pwd") + _ = shell.Process("pwd") shell.Run() } @@ -348,7 +357,7 @@ func last4(s string) string { var boolOptions = []string{"false", "true"} var boolCompleter = makeCompleter(func() []string { return boolOptions }) -func setJsonMode(c *ishell.Context) { +func setJSONMode(c *ishell.Context) { var value string if len(c.Args) == 1 { value = c.Args[0] @@ -364,9 +373,9 @@ func setJsonMode(c *ishell.Context) { } value = boolOptions[choice] } - isJson := strings.ToLower(value) == "true" || strings.ToLower(value) == "t" - setJson(isJson) - if isJson { + isJSON := strings.ToLower(value) == "true" || strings.ToLower(value) == "t" + setJSON(isJSON) + if isJSON { c.Println("JSON enabled") } else { c.Println("JSON disabled") diff --git a/cmd/util.go b/cmd/util.go index 2704622..b45e03d 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -7,15 +7,16 @@ import ( "reflect" "strings" - ldapi "github.com/launchdarkly/api-client-go" "github.com/mattbaird/jsonpatch" ishell "gopkg.in/abiosoft/ishell.v2" + + ldapi "github.com/launchdarkly/api-client-go" ) const ( - INTERACTIVE = "interactive" - EDITOR = "editor" - JSON = "json" + cINTERACTIVE = "interactive" + cEDITOR = "editor" + cJSON = "json" ) func confirmDelete(c *ishell.Context, name string, expectedValue string) bool { @@ -62,8 +63,8 @@ func makeCompleter(fetch func() []string) func(args []string) []string { } func editFile(c *ishell.Context, original []byte) (patch *ldapi.PatchComment, err error) { - editor := c.Get(EDITOR).(string) - cmd := exec.Command("command", "-v", editor) + editor := c.Get(cEDITOR).(string) + cmd := exec.Command("command", "-v", editor) // nolint:gosec // ok to launch subprocess with variable editorPathRaw, err := cmd.Output() if err != nil { c.Err(err) @@ -99,7 +100,7 @@ func editFile(c *ishell.Context, original []byte) (patch *ldapi.PatchComment, er return nil, err } - file, err = os.Open(name) + file, err = os.Open(name) // nolint:gosec // G304: Potential file inclusion via variable // ok because we created name if err != nil { return nil, err } @@ -181,17 +182,17 @@ func yesOrNo(c *ishell.Context) (yes bool) { var jsonMode *bool -func setJson(val bool) { +func setJSON(val bool) { jsonMode = &val } -func renderJson(c *ishell.Context) bool { +func renderJSON(c *ishell.Context) bool { if jsonMode != nil { return *jsonMode } - return reflect.DeepEqual(c.Get(JSON), true) + return reflect.DeepEqual(c.Get(cJSON), true) } func isInteractive(c *ishell.Context) bool { - return reflect.DeepEqual(c.Get(INTERACTIVE), true) + return reflect.DeepEqual(c.Get(cINTERACTIVE), true) } diff --git a/cmd/version.go b/cmd/version.go index d40b911..856453f 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -1,3 +1,4 @@ package cmd +// Version is the current version of the command const Version = "0.0.1" diff --git a/goal_api/goal_api.go b/goalapi/goalapi.go similarity index 61% rename from goal_api/goal_api.go rename to goalapi/goalapi.go index d415f36..d52a630 100644 --- a/goal_api/goal_api.go +++ b/goalapi/goalapi.go @@ -1,4 +1,5 @@ -package goal_api +// Package goalapi provides goals via the v1 api. goals are not yet included in the v2 api +package goalapi import ( "bytes" @@ -14,48 +15,58 @@ import ( ) const ( - Click = "click" - Custom = "custom" + // Click represents a click goal + Click = "click" + // Custom represents a custom event goal + Custom = "custom" + // PageView indicates a page view goal PageView = "pageview" ) -var AvailableKinds = []string{Click, Custom, PageView} +// Kinds are all the kinds that we can use for a goal +var Kinds = []string{Click, Custom, PageView} -type UrlMatcherBase struct { +// URLMatcherBase includes the kind of a url matcher in url matcher definitions +type URLMatcherBase struct { Kind string `json:"kind"` } -type UrlMatcherCanonical struct { - UrlMatcherBase `json:",inline"` - Url string `json:"url"` +// URLMatcherCanonical describes a canonical url matcher +type URLMatcherCanonical struct { + URLMatcherBase `json:",inline"` + URL string `json:"url"` } -type UrlMatcherExact struct { - UrlMatcherBase `json:",inline"` - Url string `json:"url"` +// URLMatcherExact describes an exact url matcher +type URLMatcherExact struct { + URLMatcherBase `json:",inline"` + URL string `json:"url"` } -type UrlMatcherSubstring struct { - UrlMatcherBase `json:",inline"` +// URLMatcherSubstring describes a substring url matcher +type URLMatcherSubstring struct { + URLMatcherBase `json:",inline"` Substring string `json:"substring"` } -type UrlMatcherRegex struct { - UrlMatcherBase `json:",inline"` +// URLMatcherRegex describes a regex url matcher +type URLMatcherRegex struct { + URLMatcherBase `json:",inline"` Pattern string `json:"pattern"` } -type GoalUrlMatchers struct { - ExactUrls []UrlMatcherExact `json:"exactUrls,omitempty"` - CanonicalUrls []UrlMatcherCanonical `json:"canonicalUrls,omitempty"` - RegexUrls []UrlMatcherRegex `json:"regexUrls,omitempty"` - SubstringUrls []UrlMatcherSubstring `json:"substringUrls,omitempty"` +// GoalURLMatchers describes the url matchers for a goal +type GoalURLMatchers struct { + ExactURLs []URLMatcherExact `json:"exactUrls,omitempty"` + CanonicalURLs []URLMatcherCanonical `json:"canonicalUrls,omitempty"` + RegexURLs []URLMatcherRegex `json:"regexUrls,omitempty"` + SubstringURLs []URLMatcherSubstring `json:"substringUrls,omitempty"` } -// Manually declare the goal type since it isn't part of the v2 api +// Goal describes the goal type. type Goal struct { - // Id of the goal - Id string `json:"_id,omitempty"` + // ID of the goal + ID string `json:"_id,omitempty"` // Name of the goal Name string `json:"name,omitempty"` @@ -63,45 +74,54 @@ type Goal struct { // Description of the goal Description string `json:"description,omitempty"` - // Whether the goal is custom, pageView or click + // Kind tells whether the goal is custom, pageView or click Kind string `json:"kind,omitempty"` // Key for custom goals Key *string `json:"key,omitempty"` - // Whether the goal is being tracked by a flag + // IsActive indicates whether the goal is being tracked by a flag IsActive bool `json:"isActive,omitempty"` - // A unix epoch time in milliseconds specifying the last modification time of this goal. + // LastModified is a unix epoch time in milliseconds specifying the last modification time of this goal. LastModified float32 `json:"lastModified,omitempty"` + // AttachedFeatureCount is the number of attached goals AttachedFeatureCount int `json:"_attachedFeatureCount,omitempty"` - Urls []GoalUrlMatchers `json:"urls,omitempty"` + // URLs are the url matchers attached to the goal + URLs []GoalURLMatchers `json:"urls,omitempty"` - // This is on the individual goal view + // AttachedFeatures describes the flags attached to this goal. This is on the individual goal view. AttachedFeatures []struct { Key string `json:"key"` Name string `json:"name"` On bool `json:"on"` } `json:"_attachedFeatures,omitempty"` + // IsDeleteable indicates if the goal can be deleted IsDeleteable bool `json:"_isDeleteable,omitempty"` - Source *struct { + + // Source is the source of the goal + Source *struct { + // Name is the name of the source Name string `json:"name"` } `json:"_source,omitempty"` + + // Version is the version of the goal Version int `json:"_version,omitempty"` } -func GetGoal(key string) (*Goal, error) { - req, _ := http.NewRequest(http.MethodGet, makeURL("/api/goals/%s", key), nil) +// GetGoal returns the goal with a given id +func GetGoal(id string) (*Goal, error) { + req, _ := http.NewRequest(http.MethodGet, makeURL("/api/goals/%s", id), nil) sdkKey, err := getCurrentSdkKey() if err != nil { return nil, err } req.Header.Add("Authorization", sdkKey) - resp, err := api.HttpClient.Do(req) + resp, err := api.HTTPClient.Do(req) if err != nil { return nil, err } @@ -110,7 +130,7 @@ func GetGoal(key string) (*Goal, error) { if err != nil { return nil, err } - resp.Body.Close() + _ = resp.Body.Close() var goal Goal if err := json.Unmarshal(body, &goal); err != nil { @@ -127,6 +147,7 @@ func getCurrentSdkKey() (string, error) { return env.ApiKey, nil } +// GetGoals returns all goals for the current environment func GetGoals() ([]Goal, error) { req, _ := http.NewRequest(http.MethodGet, makeURL("/api/goals"), nil) sdkKey, err := getCurrentSdkKey() @@ -135,7 +156,7 @@ func GetGoals() ([]Goal, error) { } req.Header.Add("Authorization", sdkKey) - resp, err := api.HttpClient.Do(req) + resp, err := api.HTTPClient.Do(req) if err != nil { return nil, err } @@ -144,7 +165,7 @@ func GetGoals() ([]Goal, error) { if err != nil { return nil, err } - resp.Body.Close() + _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected response: %s", resp.Status) @@ -161,6 +182,7 @@ func GetGoals() ([]Goal, error) { return respData.Items, nil } +// CreateGoal creates a goal in the current environment func CreateGoal(goal Goal) (*Goal, error) { body, _ := json.Marshal(goal) req, _ := http.NewRequest(http.MethodPost, makeURL("/api/goals"), bytes.NewBuffer(body)) @@ -171,7 +193,7 @@ func CreateGoal(goal Goal) (*Goal, error) { req.Header.Add("Authorization", sdkKey) req.Header.Add("Content-Type", "application/json") - resp, err := api.HttpClient.Do(req) + resp, err := api.HTTPClient.Do(req) if err != nil { return nil, err } @@ -180,7 +202,7 @@ func CreateGoal(goal Goal) (*Goal, error) { if err != nil { return nil, err } - resp.Body.Close() + _ = resp.Body.Close() if resp.StatusCode != http.StatusCreated { return nil, fmt.Errorf("unexpected response: %s", resp.Status) @@ -193,6 +215,7 @@ func CreateGoal(goal Goal) (*Goal, error) { return &newGoal, nil } +// DeleteGoal deletes a goal in the current environment func DeleteGoal(id string) error { req, _ := http.NewRequest(http.MethodDelete, makeURL("/api/goals/%s", id), nil) sdkKey, err := getCurrentSdkKey() @@ -201,7 +224,7 @@ func DeleteGoal(id string) error { } req.Header.Add("Authorization", sdkKey) - resp, err := api.HttpClient.Do(req) + resp, err := api.HTTPClient.Do(req) if err != nil { return err } @@ -213,6 +236,7 @@ func DeleteGoal(id string) error { return nil } +// PatchGoal patches a goal in the current environment func PatchGoal(id string, patchComment ldapi.PatchComment) (*Goal, error) { body, _ := json.Marshal(patchComment) req, _ := http.NewRequest(http.MethodPatch, makeURL("/api/goals/%s", id), bytes.NewBuffer(body)) @@ -222,7 +246,7 @@ func PatchGoal(id string, patchComment ldapi.PatchComment) (*Goal, error) { } req.Header.Add("Authorization", sdkKey) - resp, err := api.HttpClient.Do(req) + resp, err := api.HTTPClient.Do(req) if err != nil { return nil, err } @@ -231,7 +255,7 @@ func PatchGoal(id string, patchComment ldapi.PatchComment) (*Goal, error) { if err != nil { return nil, err } - resp.Body.Close() + _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected response: %s", resp.Status)