Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dce auth; Refactor Config file mgmt; Add integration tests #38

Merged
merged 1 commit into from
Nov 25, 2019

Conversation

eschwartz
Copy link
Contributor

@eschwartz eschwartz commented Nov 21, 2019

Stuff going on here

  • Add dce auth command
  • Remove dce.yml configs we aren't using
  • Configure a AWS creds provider that will load creds from the auth token, but fallback to env vars or the ~/.aws/credentials file
  • Run dce auth before any command, if we don't have any valid creds in our chain
  • Refactor how to load and save our config YAML, so it's easier to work with from inside multiple commands
  • Remove viper dependency (wasn't really being used)
  • Some other cleanup/refactor around init logic, to get this all wired up properly
    • I'll try to point these out in the code
  • New setup for integration tests
    • These test call our cobra CLI programmatically. For some reasons:
      • Lets us use debugger to run through tests
      • Don't need to rebuild for every test run
      • Can still use mocks at boundaries (eg. mock out web browser call)
    • I'm hoping this new testing pattern makes it easier to write tests for CLI commands going forward.

cmd/root.go Outdated
isAuthCurrentCommand := len(os.Args) == 2 && os.Args[1] == authCmd.Name()
if !hasValidCreds && !isAuthCurrentCommand {
log.Print("No valid DCE credentials found")
service.Authenticate()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

junit-report/

dce-cli
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore the compiled binary, from running make build

@@ -33,7 +33,7 @@ var accountsDescribeCmd = &cobra.Command{
Short: "describe an account",
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
service.GetAccount(args[0])
Service.GetAccount(args[0])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've exposed this global service objects. This is the only way we inject mocks into these service containers (at least without a bigger refactor to how we do DI)

func init() {
RootCmd.AddCommand(authCmd)
authCmd.Flags().StringVarP(&authUrl, "url-override", "u", "", "Override the DCE login url")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no longer used (auth endpoint is always at <dce-api>/auth)

Run: func(cmd *cobra.Command, args []string) {
service.Authenticate(authUrl)
RunE: func(cmd *cobra.Command, args []string) error {
return Service.Authenticate()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to move towards CLI commands returning errors.
This allows us to make assertions against these errors in integration tests

@@ -12,6 +12,6 @@ var initCmd = &cobra.Command{
Use: "init",
Short: "First time DCE cli setup. Creates config file at ~/.dce.yaml",
Run: func(cmd *cobra.Command, args []string) {
service.InitializeDCE(cfgFile)
Service.InitializeDCE()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The location of the config file is now globally configured within our FileSystemer object.
This provides a common path for services to access the config.


func init() {
cobra.OnInitialize(initObservation, initConfig, initUtil, initService)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved initialization to PreRun hook, just because the init functions don't return errors, which makes testing them difficult.

// If config file does not exist,
// run the `dce init` command
if !fsUtil.IsExistingFile(cfgFile) {
if cmd.Name() != initCmd.Name() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to actually just run dce init here, similar to how to do a hook to verify dce auth.
I was just running into some circular reasoning around initialization.

return serialized, nil
}

func onInit(cmd *cobra.Command, args []string) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most of this logic was just moved from the original initConfig function

@@ -2,17 +2,17 @@ package configs

// Root contains config
type Root struct {
System struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

system.auth.loginurl is no longer used

@@ -19,15 +19,18 @@ replace github.com/ugorji/go v1.1.4 => github.com/ugorji/go v0.0.0-2018120416352

require (
github.com/aws/aws-sdk-go v1.25.16
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/coreos/bbolt v1.3.2 // indirect
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed viper.
Added ptr (tiny utility for creating pointers)

token *string
}

func NewAPIClient(input *NewAPIClientInput) *operations.Client {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored into a constructor method.
Some new logic being dealt with here, around how creds are passed in.

// WriteConfig writes the Config objects as YAML
// to the config file location (dce.yml)
func (u *FileSystemUtil) WriteConfig() error {
return u.writeToYAMLFile(u.ConfigFile, u.Config)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are replacing what viper was doing,
but in a way that's easily exposed to all our services

@@ -87,28 +103,28 @@ func (s *FileSystemUtil) MvToTempDir(prefix string) (string, string) {
return destinationDir, originDir
}

func (s *FileSystemUtil) RemoveAll(path string) {
func (u *FileSystemUtil) RemoveAll(path string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just standardizing the var name for receivers (my IDE was mad at me about it)

// First, we'll check for credentials in the
// dce.yaml's `api.token` config.
// then we'll use AWS's standard chain (env vars, ~/aws/credentials file)
creds := credentials.NewChainCredentials([]credentials.Provider{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we're setting up the AWS credentials chain, which allows us to either use the configured API token (from cognito), or regular IAM creds

var session = session.New(&aws.Config{
Region: config.Region,
})
var awsSession *session.Session
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting up a common session object, using our new creds chain,
so that both the DCE API and AWS services are using the same creds

}

// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was actually printing error messages twice (I guess cobra prints them for us?)

Stuff going on here

- Add `dce auth` command
  - pops you out to a browser at the `/auth` endpoint that @joshmarsh is working on
  - see #33
- Remove dce.yml configs we aren't using
- Configure a AWS creds provider that will load creds from the auth token, but fallback to env vars or the `~/.aws/credentials` file
- Run `dce auth` before any command, if we don't have any valid creds in our chain
- Refactor how to load and save our config YAML, so it's easier to work with from inside multiple commands
- Remove viper dependency (wasn't really being used)
- Some other cleanup/refactor around init logic, to get this all wired up properly
  - I'll try to point these out in the code
- New setup for integration tests
  - These test call our cobra CLI programmatically. For some reasons:
    - Lets us use debugger to run through tests
    - Don't need to rebuild for every test run
    - Can still use mocks at boundaries (eg. mock out web browser call)
  - I'm hoping this new testing pattern makes it easier to write tests for CLI commands going forward.
serialized = []byte(entry.Message)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
return serialized, nil
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was breaking tests (we're inspecting the logger in tests, to do assertions against CLI outputs)

if config.API.Host != nil {
initializedApiClient = apiUtil.InitApiClient()
var apiClient APIer
if config.API.Host != nil && config.API.BasePath != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed some logic here, that would break if the dce.yml didn't have API configs

s.promptUserForConfig(s.Config)

// Write the config to dce.yml
err := s.Util.WriteConfig()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using our new common config file mgmt methods

newConfig.API.Host = s.Util.PromptBasic("Host name of the DCE API (example: abcde12345.execute-api.us-east-1.amazonaws.com)", nil)
newConfig.API.BasePath = s.Util.PromptBasic("Base path of the DCE API (example: /apigw-stage-name)", nil)

newConfig.GithubToken = s.Util.PromptBasic("Github token used to download releases from github (Leave blank to use GITHUB_TOKEN env variable)", nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to remove prompts for values that aren't required.
I'd rather keep my github token in an env var, then have to keep it in sync with the dce.yml file.
So I don't want dce init to force me to enter a value here.


newConfig.GithubToken = s.Util.PromptBasic("Github token used to download releases from github (Leave blank to use GITHUB_TOKEN env variable)", nil)
return &newConfig
if config.API.Host == nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you run dce init a second time, and everythings already configured, it will just be a no-op

"testing"
)

func TestAuthCommand(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the new integration tests

})

// Mock Weber.OpenURL()
mockWeber := &mocks.Weber{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's an integration test, but we can still mock at useful boundaries (like, for opening web pages)

mockWeber.On("OpenURL", "https://dce.example.com/api/auth")

// Enter the API Token (IRL, would be provided by the web page)
cli.AnswerBasic("Enter API Token: ", "my-api-token")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote a stub Prompter, that will allow you to make assertions against CLI prompts, and provide answers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice

cli.AssertAllPrompts()
mockWeber.AssertExpectations(t)

output := cli.Output()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cli.Output() grabs the logs sent to our logger


func TestInitCommand(t *testing.T) {

t.Run("GIVEN custom config path flag is provided", func(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were all migrated from being functional tests (as per a conversation with @joshmarsh )


func NewCLITest(t *testing.T) *cliTest {
// Reset globals, so they don't leak between tests
cmd.Config = &configs.Root{}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bunch of bootstrapping logic here for CLI integration tests.

Most of the complexity here comes from how we're using globals for dependency management,
and that these globals aren't initialized until you execute a command.
This makes it kind of tricky to inject dependencies into our command runner.

We might want to reconsider how we're setup with dependency injection to make this
kind of think easier, but that can be for another day...

// cli.Inject(func(input *injectorInput) {
// input.util.Weber = &mocks.Weber{}
// })
func (test *cliTest) Inject(f injector) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this Inject() method, which you'll need to use to inject mocks into global services
(because the global service references are nil until the command is run)

@@ -1,33 +0,0 @@
package unit
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now covered in integration tests

@@ -1,101 +0,0 @@
package unit
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now covered in integration tests

@eschwartz eschwartz changed the title WIP: dce auth Add dce auth; Refactor Config file mgmt; Add integration tests Nov 22, 2019
@eschwartz eschwartz merged commit a760546 into master Nov 25, 2019
@eschwartz eschwartz deleted the dce-auth branch November 25, 2019 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants