Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Autogenerated by makego. DO NOT EDIT.
.env/
.idea/
.tmp/
.vscode/
cmd/lekko/lekko
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Autogenerated by makego. DO NOT EDIT.
/.env/
/.idea/
/.tmp/
/.vscode/
/cmd/lekko/lekko
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ PROJECT := cli
GO_MODULE := github.com/lekkodev/cli
DOCKER_ORG := lekko
DOCKER_PROJECT := cli
FILE_IGNORES := .vscode/
FILE_IGNORES := .vscode/ .idea/

include make/cli/all.mk

Expand Down
55 changes: 55 additions & 0 deletions cmd/lekko/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# lekko CLI

## Design Principles

- Commands can be executed in the interactive mode (within an interactive terminal) as well as
in a non-interactive mode (called from another program or script). Exception to the rule could be the auth commands, as
they require in some cases cli-browser interaction.
- Arguments which are essential to the proper execution of the command must be passed as positional arguments.
That applies both to the mandatory and optional arguments. Flags are to be used as modifiers or filters. (This rule has
been derived from analyzing structure of linux shell commands as well as modern cli programs such as docker and git)

- In the non-interactive mode:
- interactive prompts for missing information is disabled. If the mandatory arguments are missing or they are
determined as malformed, the lekko cli immediately exits with error code 1
- colors are disabled
- the lekko cli needs to be called with -i=false or --interactive=false flags to run in the non-interactive mode

- In the interactive mode, if mandatory/optional parameters are not provided on the command line, the user will be prompted to enter them.
Interactive mode is the default mode of execution. The fallback that prompts the user for the required info is a useful,
user-friendly feature of modern CLIs, and should be supported in the interactive mode (together with the consistent
use of of colors to improve readability)

- Providing the user with sufficient information is one of the goals while constructing modern cli programs. Generally
giving the user ample of information of what the command does or regarding the nature of the execution results is encouraged.
- To support the ease and efficiency of parsing, the "quiet" (-q or --quite ) flag can be used to request that only
essential info, in the simplest, practical format should be produced as output.
- Verbose as well as dry-run options should be available when it is practical and useful


### Changes introduced to lekko cli
- adding non-interactive mode
- adding positional arguments and replacing flags with positional arguments when appropriate
- adding quiet mode for majority of commands
- hiding lekko backend url flag
- normalizing the cli "menus"
- checking for number of passed arguments and generating suitable errors depending on interactive or non-interactive contexts
- including generation of complete helloworld examples in go
- refactoring the code

### Pending changes
- more informative helps
- deciding/implementing more consistent formats for output (in addition to the quite mode/format) - text, lists, tables, json

- ### More info needed
- discussing potential changes to some features
- auth commands
- should there be a non-interactive mode implemented there. ANSWER: will look into it later
- upgrade (key is not needed). DECISION: remove. DONE
- k8s functionality. DECISION: remove. DONE
- auto-complete, DECISION: remove for the time being. DONE
- adding team delete be implemented(?). DECISION: work on it later
- auth commands: DECISION: no changes planned for the time being
- more info needed regarding:
- repo init
- should --no-colors flags be added
137 changes: 101 additions & 36 deletions cmd/lekko/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package main
import (
"fmt"
"os"
"strings"
"text/tabwriter"

bffv1beta1 "buf.build/gen/go/lekkodev/cli/protocolbuffers/go/lekko/bff/v1beta1"
Expand All @@ -32,7 +33,7 @@ import (
func apikeyCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "apikey",
Short: "api key management",
Short: "Api key management",
}
cmd.AddCommand(
createAPIKeyCmd(),
Expand All @@ -44,71 +45,112 @@ func apikeyCmd() *cobra.Command {
}

func createAPIKeyCmd() *cobra.Command {
var name string
var name, key string
var isQuiet, isDryRun bool
var err error
cmd := &cobra.Command{
Use: "create",
Short: "Create an api key",
Short: "Create an api key",
Use: formCmdUse("create", "apikey-name"),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error {
rArgs, n := getNArgs(1, args)
name = rArgs[0]
if n < 1 && !IsInteractive {
return errors.New("ApiKey name is required in non-interactive mode")
}

rs := secrets.NewSecretsOrFail(secrets.RequireLekko())
a := apikey.NewAPIKey(lekko.NewBFFClient(rs))
if len(name) == 0 {
if err := survey.AskOne(&survey.Input{
if err = survey.AskOne(&survey.Input{
Message: "Name:",
Help: "Name to give the api key",
}, &name); err != nil {
return errors.Wrap(err, "prompt")
}
}
fmt.Printf("Generating api key named '%s' for team '%s'...\n", name, rs.GetLekkoTeam())
key, err := a.Create(cmd.Context(), name)
if err != nil {
return err
if !isQuiet {
printLinef(cmd, "Generating api key named '%s' for team '%s'...\n", name, rs.GetLekkoTeam())
}

if !isDryRun {
if key, err = a.Create(cmd.Context(), name); err != nil {
return err
}
} else {
key = "xxxxxxxxxxx"
}

if !isQuiet {
printLinef(cmd, "Generated api key:\n\t%s\n", logging.Bold(key))
printLinef(cmd, "Please save the key somewhere safe, as you will not be able to access it again.\n")
printLinef(cmd, "Avoid sharing the key unnecessarily or storing it anywhere insecure.\n")
} else {
printLinef(cmd, "%s", key)
}
fmt.Printf("Generated api key:\n\t%s\n", logging.Bold(key))
fmt.Printf("Please save the key somewhere safe, as you will not be able to access it again.\n")
fmt.Printf("Avoid sharing the key unnecessarily or storing it anywhere insecure.\n")
return nil
},
}
cmd.Flags().StringVarP(&name, "name", "n", "", "Name to give the new api key")
cmd.Flags().BoolVarP(&isQuiet, QuietModeFlag, QuietModeFlagShort, QuietModeFlagDVal, QuietModeFlagDescription)
cmd.Flags().BoolVarP(&isDryRun, DryRunFlag, DryRunFlagShort, DryRunFlagDVal, DryRunFlagDescription)
return cmd
}

func listAPIKeysCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all api keys for the currently active team",
Short: "List all api keys for the currently active team",
Use: formCmdUse("list"),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error {
rs := secrets.NewSecretsOrFail(secrets.RequireLekko())
a := apikey.NewAPIKey(lekko.NewBFFClient(rs))
keys, err := a.List(cmd.Context())
if err != nil {
return errors.Wrap(err, "list")
}
printAPIKeys(keys...)
printAPIKeys(cmd, keys...)
return nil
},
}
cmd.Flags().BoolP(QuietModeFlag, QuietModeFlagShort, QuietModeFlagDVal, QuietModeFlagDescription)
return cmd
}

func printAPIKeys(keys ...*bffv1beta1.APIKey) {
w := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
fmt.Fprintf(w, "Team\tName\tCreated By\tCreated At\n")
for _, key := range keys {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", key.TeamName, key.Nickname, key.CreatedBy, key.CreatedAt.AsTime())
func printAPIKeys(cmd *cobra.Command, keys ...*bffv1beta1.APIKey) {
if isQuiet, _ := cmd.Flags().GetBool(QuietModeFlag); isQuiet {
keysStr := ""
for _, key := range keys {
keysStr += fmt.Sprintf("%s ", key.Nickname)
}
printLinef(cmd, "%s", strings.TrimSpace(keysStr))
} else {
w := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
fmt.Fprintf(w, "Team\tName\tCreated By\tCreated At\n")
for _, key := range keys {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", key.TeamName, key.Nickname, key.CreatedBy, key.CreatedAt.AsTime())
}
w.Flush()
}
w.Flush()
}

func checkAPIKeyCmd() *cobra.Command {
var key string
var isQuiet bool
cmd := &cobra.Command{
Use: "check",
Short: "Check an api key to ensure it can be used to authenticate with lekko",
Short: "Check an api key to ensure it can be used to authenticate with lekko",
Use: formCmdUse("check", "apikey"),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error {
rs := secrets.NewSecretsOrFail(secrets.RequireLekko())
a := apikey.NewAPIKey(lekko.NewBFFClient(rs))
if len(args) < 1 {
if !IsInteractive {
return errors.New("ApiKey is required.")
}
} else {
key = args[0]
}

if len(key) == 0 {
if err := survey.AskOne(&survey.Input{
Message: "API Key:",
Expand All @@ -123,21 +165,32 @@ func checkAPIKeyCmd() *cobra.Command {
fmt.Printf("Lekko: Unauthenticated %s\n", logging.Red("✖"))
return errors.Wrap(err, "check")
}
fmt.Printf("Lekko: Authenticated %s\n", logging.Green("✔"))
printAPIKeys(lekkoKey)
if !isQuiet {
fmt.Printf("Lekko: Authenticated %s\n", logging.Green("✔"))
printAPIKeys(cmd, lekkoKey)
} else {
printLinef(cmd, "OK")
}
return nil
},
}
cmd.Flags().StringVarP(&key, "key", "k", "", "api key to check authentication for")
cmd.Flags().BoolVarP(&isQuiet, QuietModeFlag, QuietModeFlagShort, QuietModeFlagDVal, QuietModeFlagDescription)
return cmd
}

func deleteAPIKeyCmd() *cobra.Command {
var name string
var isDryRun, isQuiet, isForce bool
cmd := &cobra.Command{
Use: "delete",
Short: "Delete an api key",
Short: "Delete an api key",
Use: formCmdUse("delete", "apikey-name"),
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error {
rArgs, n := getNArgs(1, args)
name = rArgs[0]
if n < 1 && !IsInteractive {
return errors.New("ApiKey is required in a non-interactive mode")
}
rs := secrets.NewSecretsOrFail(secrets.RequireLekko())
a := apikey.NewAPIKey(lekko.NewBFFClient(rs))
if len(name) == 0 {
Expand All @@ -157,17 +210,29 @@ func deleteAPIKeyCmd() *cobra.Command {
return errors.Wrap(err, "prompt")
}
}
fmt.Printf("Deleting api key '%s' in team '%s'...\n", name, rs.GetLekkoTeam())
if err := confirmInput(name); err != nil {
return err
if !isQuiet {
fmt.Printf("Deleting api key '%s' in team '%s'...\n", name, rs.GetLekkoTeam())
}
if !isForce {
if err := confirmInput(name); err != nil {
return err
}
}
if !isDryRun {
if err := a.Delete(cmd.Context(), name); err != nil {
return err
}
}
if err := a.Delete(cmd.Context(), name); err != nil {
return err
if !isQuiet {
printLinef(cmd, "Deleted '%s' api key.\n", name)
} else {
printLinef(cmd, "%s", name)
}
fmt.Printf("Deleted api key.\n")
return nil
},
}
cmd.Flags().StringVarP(&name, "name", "n", "", "Name of api key to delete")
cmd.Flags().BoolVarP(&isQuiet, QuietModeFlag, QuietModeFlagShort, QuietModeFlagDVal, QuietModeFlagDescription)
cmd.Flags().BoolVarP(&isDryRun, DryRunFlag, DryRunFlagShort, DryRunFlagDVal, DryRunFlagDescription)
cmd.Flags().BoolVarP(&isForce, ForceFlag, ForceFlagShort, ForceFlagDVal, ForceFlagDescription)
return cmd
}
4 changes: 2 additions & 2 deletions cmd/lekko/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
func authCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "authenticates lekko cli",
Short: "Authenticates lekko cli",
}

cmd.AddCommand(confirmUserCmd())
Expand All @@ -49,7 +49,7 @@ func authCmd() *cobra.Command {
func loginCmd() *cobra.Command {
return &cobra.Command{
Use: "login",
Short: "authenticate with lekko and github, if unauthenticated",
Short: "Authenticate with lekko and github, if unauthenticated",
RunE: func(cmd *cobra.Command, args []string) error {
return secrets.WithWriteSecrets(func(ws secrets.WriteSecrets) error {
auth := oauth.NewOAuth(lekko.NewBFFClient(ws))
Expand Down
69 changes: 69 additions & 0 deletions cmd/lekko/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2022 Lekko Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"os"

"github.com/lekkodev/cli/pkg/repo"
"github.com/lekkodev/cli/pkg/secrets"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

func commitCmd() *cobra.Command {
var message, hash string
var isQuiet, isVerify bool
cmd := &cobra.Command{
Short: "Commits local changes to the remote branch",
Use: formCmdUse("commit"),
DisableFlagsInUseLine: true,
//Use: "commit" + FlagOptions,
RunE: func(cmd *cobra.Command, args []string) error {
wd, err := os.Getwd()
if err != nil {
return err
}
rs := secrets.NewSecretsOrFail(secrets.RequireGithub())
r, err := repo.NewLocal(wd, rs)
if err != nil {
return errors.Wrap(err, "new repo")
}
ctx := cmd.Context()

if isQuiet {
r.ConfigureLogger(nil)
}

if isVerify {
if _, err := r.Verify(ctx, &repo.VerifyRequest{}); err != nil {
return errors.Wrap(err, "verify")
}
}

if hash, err = r.Commit(ctx, rs, message); err != nil {
return err
}
if isQuiet {
printLinef(cmd, "%s", hash)
}
return nil
},
}
cmd.Flags().BoolVarP(&isQuiet, QuietModeFlag, QuietModeFlagShort, QuietModeFlagDVal, QuietModeFlagDescription)
cmd.Flags().BoolVarP(&isVerify, "verify", "v", false, "verify changes before committing")
cmd.Flags().StringVarP(&message, "message", "m", "config change commit", "commit message")
return cmd
}
Loading