Skip to content

Commit

Permalink
refactor(cli): Refactor generate & generate_aws (#689)
Browse files Browse the repository at this point in the history
* refactor(cli): Refactor generate & generate_aws

Signed-off-by: Ross <[email protected]>

* refactor(cli): Refactor to use type switch rather than reflect

Signed-off-by: Ross <[email protected]>
  • Loading branch information
rmoles authored Feb 8, 2022
1 parent 6adfeeb commit c2d4241
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 124 deletions.
70 changes: 54 additions & 16 deletions cli/cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,22 @@ func (a *AwsGenerateCommandExtraState) isEmpty() bool {
return a.Output == "" && !a.UseExistingCloudtrail && len(a.AwsSubAccounts) == 0 && !a.TerraformApply
}

// Flush current state of the struct to disk, provided its not empty
// Flush current state of the struct to disk, provided it's not empty
func (a *AwsGenerateCommandExtraState) writeCache() {
if !a.isEmpty() {
cli.WriteAssetToCache(CachedAssetAwsExtraState, time.Now().Add(time.Hour*1), a)
}
}

var (
QuestionRunTfPlan = "Run Terraform plan now?"
QuestionUsePreviousCache = "Previous IaC generation detected, load cached values?"
QuestionRunTfPlan = "Run Terraform plan now?"
QuestionUsePreviousCache = "Previous IaC generation detected, load cached values?"

GenerateAwsCommandState = &aws.GenerateAwsTfConfigurationArgs{}
GenerateAwsExistingRoleState = &aws.ExistingIamRoleDetails{}
GenerateAwsCommandExtraState = &AwsGenerateCommandExtraState{}
ValidateSubAccountFlagRegex = fmt.Sprintf(`%s:%s`, AwsProfileRegex, AwsRegionRegex)
CachedAssetIacParams = "iac-aws-generate-params"
CachedAwsAssetIacParams = "iac-aws-generate-params"
CachedAssetAwsExtraState = "iac-aws-extra-state"

// iac-generate command is used to create IaC code for various environments
Expand Down Expand Up @@ -118,7 +119,7 @@ This command can also be run in noninteractive mode. See help output for more de
return errors.Wrap(err, "failed to parse output location")
}

ok, err := writeHclOutputPrecheck(dirname)
ok, err := writeHclOutputPreCheck(dirname)
if err != nil {
return errors.Wrap(err, "failed to validate output location")
}
Expand All @@ -129,7 +130,7 @@ This command can also be run in noninteractive mode. See help output for more de

location, err := writeHclOutput(hcl, dirname)
if err != nil {
return errors.Wrap(err, "failed to write terrraform code to disk")
return errors.Wrap(err, "failed to write terraform code to disk")
}

// Prompt to execute
Expand All @@ -139,7 +140,7 @@ This command can also be run in noninteractive mode. See help output for more de
})

if err != nil {
return errors.Wrap(err, "failed to promopt for terraform execution")
return errors.Wrap(err, "failed to prompt for terraform execution")
}

// Execute
Expand Down Expand Up @@ -208,7 +209,7 @@ This command can also be run in noninteractive mode. See help output for more de
// Load any cached inputs if interactive
if cli.InteractiveMode() {
cachedOptions := &aws.GenerateAwsTfConfigurationArgs{}
iacParamsExpired := cli.ReadCachedAsset(CachedAssetIacParams, &cachedOptions)
iacParamsExpired := cli.ReadCachedAsset(CachedAwsAssetIacParams, &cachedOptions)
if iacParamsExpired {
cli.Log.Debug("loaded previously set values for AWS iac generation")
}
Expand All @@ -223,7 +224,7 @@ This command can also be run in noninteractive mode. See help output for more de
answer := false
if !iacParamsExpired || !extraStateParamsExpired {
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
Prompt: &survey.Confirm{Message: QuestionUsePreviousCache, Default: answer},
Prompt: &survey.Confirm{Message: QuestionUsePreviousCache, Default: false},
Response: &answer,
}); err != nil {
return errors.Wrap(err, "failed to load saved options")
Expand All @@ -232,7 +233,7 @@ This command can also be run in noninteractive mode. See help output for more de

// If the user decides NOT to use the previous values; we won't load them. However, every time the command runs
// we are going to write out new cached values, so if they run it - bail out - and run it again they'll get
// reprompted.
// re-prompted.
if answer {
// Merge cached inputs to current options (current options win)
if err := mergo.Merge(GenerateAwsCommandState, cachedOptions); err != nil {
Expand Down Expand Up @@ -353,7 +354,7 @@ type SurveyQuestionWithValidationArgs struct {
Required bool
}

// Prompt use for question, only if the CLI is in interactive mode
// SurveyQuestionInteractiveOnly Prompt use for question, only if the CLI is in interactive mode
func SurveyQuestionInteractiveOnly(question SurveyQuestionWithValidationArgs) error {
// Do validations pass?
ok := true
Expand Down Expand Up @@ -387,7 +388,7 @@ func SurveyQuestionInteractiveOnly(question SurveyQuestionWithValidationArgs) er
return nil
}

// Prompt for many values at once
// SurveyMultipleQuestionWithValidation Prompt for many values at once
//
// checks: If supplied check(s) are true, questions will be asked
func SurveyMultipleQuestionWithValidation(questions []SurveyQuestionWithValidationArgs, checks ...bool) error {
Expand Down Expand Up @@ -427,8 +428,8 @@ func determineOutputDirPath(location string) (string, error) {
return filepath.FromSlash(fmt.Sprintf("%s/%s", dirname, "lacework")), nil
}

// Prompt for confirmation if main.tf already exists; return true to continue
func writeHclOutputPrecheck(outputLocation string) (bool, error) {
// writeHclOutputPreCheck Prompt for confirmation if main.tf already exists; return true to continue
func writeHclOutputPreCheck(outputLocation string) (bool, error) {
// If noninteractive, continue
if !cli.InteractiveMode() {
return true, nil
Expand Down Expand Up @@ -458,7 +459,7 @@ func writeHclOutputPrecheck(outputLocation string) (bool, error) {
return answer, nil
}

// Write HCL output
// writeHclOutput Write HCL output
func writeHclOutput(hcl string, location string) (string, error) {
// Determine write location
dirname, err := determineOutputDirPath(location)
Expand Down Expand Up @@ -492,7 +493,7 @@ func writeHclOutput(hcl string, location string) (string, error) {
return outputLocation, nil
}

// This function used to validate provided output location exists and is a directory
// validateOutputLocation This function used to validate provided output location exists and is a directory
func validateOutputLocation(dirname string) error {
// If output location was supplied, validate it exists
if dirname != "" {
Expand Down Expand Up @@ -522,3 +523,40 @@ func validateAwsSubAccounts(subaccounts []string) error {

return nil
}

// create survey.Validator for string with regex
func validateStringWithRegex(val interface{}, regex string, errorString string) error {
switch value := val.(type) {
case string:
// if value doesn't match regex, return invalid arn
ok, err := regexp.MatchString(regex, value)
if err != nil {
return errors.Wrap(err, "failed to validate input")
}

if !ok {
return errors.New(errorString)
}
default:
// if the value passed is not a string
return errors.New("value must be a string")
}

return nil
}

// Used to test if path supplied for output exists
func validPathExists(val interface{}) error {
switch value := val.(type) {
case string:
// Test if supplied path exists
if err := validateOutputLocation(value); err != nil {
return err
}
default:
// if the value passed is not a string
return errors.New("value must be a string")
}

return nil
}
89 changes: 23 additions & 66 deletions cli/cmd/generate_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package cmd

import (
"fmt"
"reflect"
"regexp"
"strings"
"time"

Expand All @@ -14,7 +12,7 @@ import (

var (
// Define question text here so they can be reused in testing
QuestionEnableConfig = "Enable Config Integration?"
QuestionAwsEnableConfig = "Enable Config Integration?"
QuestionEnableCloudtrail = "Enable Cloudtrail Integration?"
QuestionAwsRegion = "Specify the AWS region to be used by Cloudtrail, SNS, and S3:"
QuestionConsolidatedCloudtrail = "Use consolidated Cloudtrail?"
Expand All @@ -29,47 +27,24 @@ var (
QuestionSubAccountRegion = "What region should be used for this account?"
QuestionSubAccountAddMore = "Add another AWS account?"
QuestionSubAccountReplace = "Currently configured AWS subaccounts: %s, replace?"
QuestionConfigAdvanced = "Configure advanced integration options?"
QuestionAnotherAdvancedOpt = "Configure another advanced integration option"
QuestionCustomizeOutputLocation = "Provide the location for the output to be written:"
QuestionAwsConfigAdvanced = "Configure advanced integration options?"
QuestionAwsAnotherAdvancedOpt = "Configure another advanced integration option"
QuestionAwsCustomizeOutputLocation = "Provide the location for the output to be written:"

// select options
AdvancedOptDone = "Done"
AwsAdvancedOptDone = "Done"
AdvancedOptCloudTrail = "Additional Cloudtrail options"
AdvancedOptIamRole = "Configure Lacework integration with an existing IAM role"
AdvancedOptAwsAccounts = "Add additional AWS Accounts to Lacework"
AdvancedOptLocation = "Customize output location"
AwsAdvancedOptLocation = "Customize output location"

// original source: https://regex101.com/r/pOfxYN/1
// AwsArnRegex original source: https://regex101.com/r/pOfxYN/1
AwsArnRegex = `^arn:(?P<Partition>[^:\n]*):(?P<Service>[^:\n]*):(?P<Region>[^:\n]*):(?P<AccountID>[^:\n]*):(?P<Ignore>(?P<ResourceType>[^:\/\n]*)[:\/])?(?P<Resource>.*)$`
// regex used for validating region input; note intentionally does not match gov cloud
// AwsRegionRegex regex used for validating region input; note intentionally does not match gov cloud
AwsRegionRegex = `(us|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d`
AwsProfileRegex = `([A-Za-z_0-9-]+)`
)

// create survey.Validator for string with regex
func validateStringWithRegex(val interface{}, regex string, errorString string) error {
// the reflect value of the result
value := reflect.ValueOf(val)

// if the value passed is not a string
if value.Kind() != reflect.String {
return errors.New("value must be a string")
}

// if value doesn't match regex, return invalid arn
ok, err := regexp.MatchString(regex, value.String())
if err != nil {
return errors.Wrap(err, "failed to validate input")
}

if !ok {
return errors.New(errorString)
}

return nil
}

// survey.Validator for aws ARNs
//
// This isn't service/type specific but rather just validates that an ARN was entered that matches valid ARN formats
Expand Down Expand Up @@ -228,27 +203,9 @@ func promptAwsAdditionalAccountQuestions(config *aws.GenerateAwsTfConfigurationA
return nil
}

// Used to test if path supplied for output exists
func validPathExists(val interface{}) error {
// the reflect value of the result
value := reflect.ValueOf(val)

// if the value passed is not a string
if value.Kind() != reflect.String {
return errors.New("value must be a string")
}

// Test if supplied path exists
if err := validateOutputLocation(value.String()); err != nil {
return err
}

return nil
}

func promptCustomizeOutputLocation(config *aws.GenerateAwsTfConfigurationArgs, extraState *AwsGenerateCommandExtraState) error {
func promptCustomizeAwsOutputLocation(extraState *AwsGenerateCommandExtraState) error {
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
Prompt: &survey.Input{Message: QuestionCustomizeOutputLocation, Default: extraState.Output},
Prompt: &survey.Input{Message: QuestionAwsCustomizeOutputLocation, Default: extraState.Output},
Response: &extraState.Output,
Opts: []survey.AskOpt{survey.WithValidator(validPathExists)},
Required: true,
Expand All @@ -263,7 +220,7 @@ func askAdvancedOptions(config *aws.GenerateAwsTfConfigurationArgs, extraState *
answer := ""

// Prompt for options
for answer != AdvancedOptDone {
for answer != AwsAdvancedOptDone {
// Construction of this slice is a bit strange at first look, but the reason for that is because we have to do string
// validation to know which option was selected due to how survey works; and doing it by index (also supported) is
// difficult when the options are dynamic (which they are)
Expand All @@ -273,7 +230,7 @@ func askAdvancedOptions(config *aws.GenerateAwsTfConfigurationArgs, extraState *
if config.ConsolidatedCloudtrail {
options = append(options, AdvancedOptAwsAccounts)
}
options = append(options, AdvancedOptLocation, AdvancedOptDone)
options = append(options, AwsAdvancedOptLocation, AwsAdvancedOptDone)
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
Prompt: &survey.Select{
Message: "Which options would you like to enable?",
Expand All @@ -298,28 +255,28 @@ func askAdvancedOptions(config *aws.GenerateAwsTfConfigurationArgs, extraState *
if err := promptAwsAdditionalAccountQuestions(config); err != nil {
return err
}
case AdvancedOptLocation:
if err := promptCustomizeOutputLocation(config, extraState); err != nil {
case AwsAdvancedOptLocation:
if err := promptCustomizeAwsOutputLocation(extraState); err != nil {
return err
}
}

// Re-prompt if not done
innerAskAgain := true
if answer == AdvancedOptDone {
if answer == AwsAdvancedOptDone {
innerAskAgain = false
}

if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
Checks: []*bool{&innerAskAgain},
Prompt: &survey.Confirm{Message: QuestionAnotherAdvancedOpt, Default: false},
Prompt: &survey.Confirm{Message: QuestionAwsAnotherAdvancedOpt, Default: false},
Response: &innerAskAgain,
}); err != nil {
return err
}

if !innerAskAgain {
answer = AdvancedOptDone
answer = AwsAdvancedOptDone
}
}

Expand All @@ -332,7 +289,7 @@ func configOrCloudtrailEnabled(config *aws.GenerateAwsTfConfigurationArgs) *bool
}

func awsConfigIsEmpty(g *aws.GenerateAwsTfConfigurationArgs) bool {
return (!g.Cloudtrail &&
return !g.Cloudtrail &&
!g.Config &&
!g.ConsolidatedCloudtrail &&
g.AwsProfile == "default" &&
Expand All @@ -342,7 +299,7 @@ func awsConfigIsEmpty(g *aws.GenerateAwsTfConfigurationArgs) bool {
g.ExistingSnsTopicArn == "" &&
g.LaceworkProfile == "" &&
!g.ForceDestroyS3Bucket &&
g.SubAccounts == nil)
g.SubAccounts == nil
}

func writeAwsGenerationArgsCache(a *aws.GenerateAwsTfConfigurationArgs) {
Expand All @@ -351,7 +308,7 @@ func writeAwsGenerationArgsCache(a *aws.GenerateAwsTfConfigurationArgs) {
if a.ExistingIamRole.IsPartial() {
a.ExistingIamRole = nil
}
cli.WriteAssetToCache(CachedAssetIacParams, time.Now().Add(time.Hour*1), a)
cli.WriteAssetToCache(CachedAwsAssetIacParams, time.Now().Add(time.Hour*1), a)
}
}

Expand All @@ -374,11 +331,11 @@ func promptAwsGenerate(
config.ExistingIamRole = existingIam
}

// This are the core questions that should be asked. Region required for provider block
// These are the core questions that should be asked. Region required for provider block
if err := SurveyMultipleQuestionWithValidation(
[]SurveyQuestionWithValidationArgs{
{
Prompt: &survey.Confirm{Message: QuestionEnableConfig, Default: config.Config},
Prompt: &survey.Confirm{Message: QuestionAwsEnableConfig, Default: config.Config},
Response: &config.Config,
},
{
Expand Down Expand Up @@ -406,7 +363,7 @@ func promptAwsGenerate(
// Find out if the customer wants to specify more advanced features
askAdvanced := false
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
Prompt: &survey.Confirm{Message: QuestionConfigAdvanced, Default: askAdvanced},
Prompt: &survey.Confirm{Message: QuestionAwsConfigAdvanced, Default: askAdvanced},
Response: &askAdvanced,
}); err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions cli/cmd/generate_aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func TestGenerationCache(t *testing.T) {

args := aws.GenerateAwsTfConfigurationArgs{AwsProfile: "default"} // Profile is set automatically by the CLI defaults
writeAwsGenerationArgsCache(&args)
assert.NoFileExists(t, filepath.FromSlash(fmt.Sprintf("%s/cache/standalone/%s", dir, CachedAssetIacParams)))
assert.NoFileExists(t, filepath.FromSlash(fmt.Sprintf("%s/cache/standalone/%s", dir, CachedAwsAssetIacParams)))
})
t.Run("iac params should be cached when not empty", func(t *testing.T) {
dir, err := ioutil.TempDir("", "lacework-cli-cache")
Expand All @@ -135,6 +135,6 @@ func TestGenerationCache(t *testing.T) {

args := aws.GenerateAwsTfConfigurationArgs{AwsProfile: "default", AwsRegion: "us-east-2"} // Profile is set automatically by the CLI defaults
writeAwsGenerationArgsCache(&args)
assert.FileExists(t, filepath.FromSlash(fmt.Sprintf("%s/cache/standalone/%s", dir, CachedAssetIacParams)))
assert.FileExists(t, filepath.FromSlash(fmt.Sprintf("%s/cache/standalone/%s", dir, CachedAwsAssetIacParams)))
})
}
Loading

0 comments on commit c2d4241

Please sign in to comment.