Skip to content

Commit 938511e

Browse files
authored
feat: GCP multi project Terraform (#1233)
1 parent 6a8125d commit 938511e

File tree

6 files changed

+242
-5
lines changed

6 files changed

+242
-5
lines changed

cli/cmd/generate_gcp.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"regexp"
5+
"strings"
56
"time"
67

78
"github.com/imdario/mergo"
@@ -46,7 +47,9 @@ var (
4647

4748
QuestionGcpAnotherAdvancedOpt = "Configure another advanced integration option"
4849
GcpAdvancedOptLocation = "Customize output location"
50+
GcpAdvancedOptProjects = "Configure multiple projects"
4951
QuestionGcpCustomizeOutputLocation = "Provide the location for the output to be written:"
52+
QuestionGcpCustomizeProjects = "Provide comma separated list of project ID"
5053
QuestionGcpCustomFilter = "Specify a custom Audit Log filter which supersedes all other filter options"
5154
GcpAdvancedOptDone = "Done"
5255

@@ -117,6 +120,7 @@ See help output for more details on the parameter value(s) required for Terrafor
117120
gcp.WithPrefix(GenerateGcpCommandState.Prefix),
118121
gcp.WithWaitTime(GenerateGcpCommandState.WaitTime),
119122
gcp.WithEnableUBLA(GenerateGcpCommandState.EnableUBLA),
123+
gcp.WithMultipleProject(GenerateGcpCommandState.Projects),
120124
}
121125

122126
if GenerateGcpCommandState.OrganizationIntegration {
@@ -441,6 +445,11 @@ func initGenerateGcpTfCommandFlags() {
441445
"use_pub_sub",
442446
false,
443447
"use pub/sub for the audit log data rather than bucket")
448+
generateGcpTfCommand.PersistentFlags().StringSliceVar(
449+
&GenerateGcpCommandState.Projects,
450+
"projects",
451+
[]string{},
452+
"list of project IDs to integrate with (project-level integrations)")
444453
}
445454

446455
// survey.Validator for gcp region
@@ -616,6 +625,44 @@ func promptCustomizeGcpOutputLocation(extraState *GcpGenerateCommandExtraState)
616625
return err
617626
}
618627

628+
func promptCustomizeGcpProjects(config *gcp.GenerateGcpTfConfigurationArgs) error {
629+
630+
validation := func(val interface{}) error {
631+
switch value := val.(type) {
632+
case string:
633+
for _, id := range strings.Split(value, ",") {
634+
err := validateGcpProjectId(strings.TrimSpace(id))
635+
if err != nil {
636+
return err
637+
}
638+
}
639+
default:
640+
return errors.New("value must be a string")
641+
}
642+
643+
return nil
644+
}
645+
646+
var projects string
647+
648+
err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
649+
Prompt: &survey.Input{Message: QuestionGcpCustomizeProjects},
650+
Response: &projects,
651+
Opts: []survey.AskOpt{survey.WithValidator(validation)},
652+
Required: true,
653+
})
654+
655+
if err != nil {
656+
return err
657+
}
658+
659+
for _, id := range strings.Split(projects, ",") {
660+
config.Projects = append(config.Projects, strings.TrimSpace(id))
661+
}
662+
663+
return nil
664+
}
665+
619666
func askAdvancedOptions(config *gcp.GenerateGcpTfConfigurationArgs, extraState *GcpGenerateCommandExtraState) error {
620667
answer := ""
621668

@@ -631,7 +678,7 @@ func askAdvancedOptions(config *gcp.GenerateGcpTfConfigurationArgs, extraState *
631678
options = append(options, GcpAdvancedOptAuditLog)
632679
}
633680

634-
options = append(options, GcpAdvancedOptExistingServiceAccount, GcpAdvancedOptIntegrationName, GcpAdvancedOptLocation, GcpAdvancedOptDone)
681+
options = append(options, GcpAdvancedOptExistingServiceAccount, GcpAdvancedOptIntegrationName, GcpAdvancedOptLocation, GcpAdvancedOptProjects, GcpAdvancedOptDone)
635682
if err := SurveyQuestionInteractiveOnly(SurveyQuestionWithValidationArgs{
636683
Prompt: &survey.Select{
637684
Message: "Which options would you like to configure?",
@@ -660,6 +707,10 @@ func askAdvancedOptions(config *gcp.GenerateGcpTfConfigurationArgs, extraState *
660707
if err := promptCustomizeGcpOutputLocation(extraState); err != nil {
661708
return err
662709
}
710+
case GcpAdvancedOptProjects:
711+
if err := promptCustomizeGcpProjects(config); err != nil {
712+
return err
713+
}
663714
}
664715

665716
// Re-prompt if not done

integration/gcp_generation_test.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ func TestGenerationAdvancedOptsDoneGcp(t *testing.T) {
817817
MsgRsp{cmd.QuestionGcpOrganizationIntegration, "n"},
818818
MsgRsp{cmd.QuestionGcpServiceAccountCredsPath, ""},
819819
MsgRsp{cmd.QuestionGcpConfigureAdvanced, "y"},
820-
MsgMenu{cmd.GcpAdvancedOptAuditLog, 4},
820+
MsgMenu{cmd.GcpAdvancedOptAuditLog, 5},
821821
MsgRsp{cmd.QuestionRunTfPlan, "n"},
822822
})
823823
final, _ = c.ExpectEOF()
@@ -849,7 +849,7 @@ func TestGenerationAdvancedOptsDoneGcpConfiguration(t *testing.T) {
849849
MsgRsp{cmd.QuestionGcpOrganizationIntegration, "n"},
850850
MsgRsp{cmd.QuestionGcpServiceAccountCredsPath, ""},
851851
MsgRsp{cmd.QuestionGcpConfigureAdvanced, "y"},
852-
MsgMenu{cmd.GcpAdvancedOptExistingServiceAccount, 3},
852+
MsgMenu{cmd.GcpAdvancedOptDone, 4},
853853
MsgRsp{cmd.QuestionRunTfPlan, "n"},
854854
})
855855
final, _ = c.ExpectEOF()
@@ -1323,6 +1323,85 @@ func TestGenerationGcpLaceworkProfile(t *testing.T) {
13231323
assert.Equal(t, buildTf, tfResult)
13241324
}
13251325

1326+
func TestGenerationGcpMultipleProjects(t *testing.T) {
1327+
os.Setenv("LW_NOCACHE", "true")
1328+
defer os.Setenv("LW_NOCACHE", "")
1329+
var final string
1330+
gcpProjects := []string{"project1", "project2", "project3"}
1331+
1332+
tfResult := runGcpGenerateTest(t,
1333+
func(c *expect.Console) {
1334+
expectsCliOutput(t, c, []MsgRspHandler{
1335+
MsgRsp{cmd.QuestionGcpEnableConfiguration, "y"},
1336+
MsgRsp{cmd.QuestionGcpEnableAuditLog, "y"},
1337+
MsgRsp{cmd.QuestionGcpProjectID, projectId},
1338+
MsgRsp{cmd.QuestionGcpOrganizationIntegration, "n"},
1339+
MsgRsp{cmd.QuestionGcpServiceAccountCredsPath, ""},
1340+
MsgRsp{cmd.QuestionGcpConfigureAdvanced, "n"},
1341+
MsgRsp{cmd.QuestionRunTfPlan, "n"},
1342+
})
1343+
1344+
final, _ = c.ExpectEOF()
1345+
},
1346+
"generate",
1347+
"cloud-account",
1348+
"gcp",
1349+
"--projects",
1350+
"project1",
1351+
"--projects",
1352+
"project2",
1353+
"--projects",
1354+
"project3",
1355+
"--projects",
1356+
"project1",
1357+
)
1358+
1359+
assertTerraformSaved(t, final)
1360+
1361+
buildTf, _ := gcp.NewTerraform(true, true, false,
1362+
gcp.WithProjectId(projectId),
1363+
gcp.WithMultipleProject(gcpProjects),
1364+
).Generate()
1365+
assert.Equal(t, buildTf, tfResult)
1366+
}
1367+
1368+
func TestGenerationGcpMultipleProjectsInteractive(t *testing.T) {
1369+
os.Setenv("LW_NOCACHE", "true")
1370+
defer os.Setenv("LW_NOCACHE", "")
1371+
var final string
1372+
gcpProjects := []string{"project1", "project2", "project3"}
1373+
1374+
tfResult := runGcpGenerateTest(t,
1375+
func(c *expect.Console) {
1376+
expectsCliOutput(t, c, []MsgRspHandler{
1377+
MsgRsp{cmd.QuestionGcpEnableConfiguration, "y"},
1378+
MsgRsp{cmd.QuestionGcpEnableAuditLog, "y"},
1379+
MsgRsp{cmd.QuestionGcpProjectID, projectId},
1380+
MsgRsp{cmd.QuestionGcpOrganizationIntegration, "n"},
1381+
MsgRsp{cmd.QuestionGcpServiceAccountCredsPath, ""},
1382+
MsgRsp{cmd.QuestionGcpConfigureAdvanced, "y"},
1383+
MsgMenu{cmd.GcpAdvancedOptProjects, 4},
1384+
MsgRsp{cmd.QuestionGcpCustomizeProjects, "project1, project2 ,project3"},
1385+
MsgRsp{cmd.QuestionGcpAnotherAdvancedOpt, "n"},
1386+
MsgRsp{cmd.QuestionRunTfPlan, "n"},
1387+
})
1388+
1389+
final, _ = c.ExpectEOF()
1390+
},
1391+
"generate",
1392+
"cloud-account",
1393+
"gcp",
1394+
)
1395+
1396+
assertTerraformSaved(t, final)
1397+
1398+
buildTf, _ := gcp.NewTerraform(true, true, false,
1399+
gcp.WithProjectId(projectId),
1400+
gcp.WithMultipleProject(gcpProjects),
1401+
).Generate()
1402+
assert.Equal(t, buildTf, tfResult)
1403+
}
1404+
13261405
func runGcpGenerateTest(t *testing.T, conditions func(*expect.Console), args ...string) string {
13271406
os.Setenv("HOME", tfPath)
13281407

integration/test_resources/help/generate_cloud-account_gcp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Flags:
4545
--output string location to write generated content (default is ~/lacework/gcp)
4646
--prefix string prefix that will be used at the beginning of every generated resource
4747
--project_id string specify the project id to be used to provision lacework resources (required)
48+
--projects strings list of project IDs to integrate with (project-level integrations)
4849
--service_account_credentials string specify service account credentials JSON file path (leave blank to make use of google credential ENV vars)
4950
--use_pub_sub use pub/sub for the audit log data rather than bucket
5051
--wait_time string amount of time to wait before the next resource is provisioned

lwgenerate/gcp/gcp.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gcp
22

33
import (
4+
"fmt"
45
"sort"
56

67
"github.com/hashicorp/hcl/v2/hclwrite"
@@ -126,6 +127,8 @@ type GenerateGcpTfConfigurationArgs struct {
126127
Prefix string
127128

128129
WaitTime string
130+
131+
Projects []string
129132
}
130133

131134
// Ensure all combinations of inputs are valid for supported spec
@@ -376,6 +379,12 @@ func WithWaitTime(waitTime string) GcpTerraformModifier {
376379
}
377380
}
378381

382+
func WithMultipleProject(projects []string) GcpTerraformModifier {
383+
return func(c *GenerateGcpTfConfigurationArgs) {
384+
c.Projects = projects
385+
}
386+
}
387+
379388
// Generate new Terraform code based on the supplied args.
380389
func (args *GenerateGcpTfConfigurationArgs) Generate() (string, error) {
381390
// Validate inputs
@@ -515,6 +524,14 @@ func createConfiguration(args *GenerateGcpTfConfigurationArgs) ([]*hclwrite.Bloc
515524
attributes["wait_time"] = args.WaitTime
516525
}
517526

527+
if len(args.Projects) > 0 {
528+
value := make(map[string]string, len(args.Projects))
529+
for _, p := range args.Projects {
530+
value[p] = p
531+
}
532+
moduleDetails = append(moduleDetails, lwgenerate.HclModuleWithForEach("project_id", value))
533+
}
534+
518535
moduleDetails = append(moduleDetails,
519536
lwgenerate.HclModuleWithAttributes(attributes),
520537
)
@@ -617,11 +634,18 @@ func createAuditLog(args *GenerateGcpTfConfigurationArgs) (*hclwrite.Block, erro
617634

618635
if args.ExistingServiceAccount == nil && args.Configuration {
619636
attributes["use_existing_service_account"] = true
637+
638+
cfgModuleName := configurationModuleName
639+
640+
if len(args.Projects) > 0 {
641+
cfgModuleName = fmt.Sprintf("%s[each.key]", cfgModuleName)
642+
}
643+
620644
attributes["service_account_name"] = lwgenerate.CreateSimpleTraversal(
621-
[]string{"module", configurationModuleName, "service_account_name"},
645+
[]string{"module", cfgModuleName, "service_account_name"},
622646
)
623647
attributes["service_account_private_key"] = lwgenerate.CreateSimpleTraversal(
624-
[]string{"module", configurationModuleName, "service_account_private_key"},
648+
[]string{"module", cfgModuleName, "service_account_private_key"},
625649
)
626650
}
627651

@@ -657,6 +681,14 @@ func createAuditLog(args *GenerateGcpTfConfigurationArgs) (*hclwrite.Block, erro
657681
attributes["wait_time"] = args.WaitTime
658682
}
659683

684+
if len(args.Projects) > 0 {
685+
value := make(map[string]string)
686+
for _, p := range args.Projects {
687+
value[p] = p
688+
}
689+
moduleDetails = append(moduleDetails, lwgenerate.HclModuleWithForEach("project_id", value))
690+
}
691+
660692
moduleDetails = append(moduleDetails,
661693
lwgenerate.HclModuleWithAttributes(attributes),
662694
)

lwgenerate/gcp/gcp_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,26 @@ func TestGenerateGcpTfConfigurationArgs_Generate_AuditLog(t *testing.T) {
455455
version = "~> 3.0"
456456
wait_time = "30s"
457457
}
458+
`),
459+
},
460+
{
461+
"TestGenerationMultipleProject",
462+
gcp.NewTerraform(false, true, false,
463+
gcp.WithGcpServiceAccountCredentials("/path/to/credentials"),
464+
gcp.WithProjectId(projectName),
465+
gcp.WithMultipleProject([]string{"project1", "project2", "project3"}),
466+
),
467+
ReqProvider(projectName, `module "gcp_project_audit_log" {
468+
source = "lacework/audit-log/gcp"
469+
version = "~> 3.0"
470+
471+
for_each = {
472+
project1 = "project1"
473+
project2 = "project2"
474+
project3 = "project3"
475+
}
476+
project_id = each.key
477+
}
458478
`),
459479
},
460480
}
@@ -656,6 +676,26 @@ func TestGenerateGcpTfConfigurationArgs_Generate_Configuration(t *testing.T) {
656676
version = "~> 2.3"
657677
wait_time = "30s"
658678
}
679+
`),
680+
},
681+
{
682+
"TestGenerationMultipleProject",
683+
gcp.NewTerraform(true, false, false,
684+
gcp.WithGcpServiceAccountCredentials("/path/to/credentials"),
685+
gcp.WithProjectId(projectName),
686+
gcp.WithMultipleProject([]string{"project1", "project2", "project3"}),
687+
),
688+
ReqProvider(projectName, `module "gcp_project_level_config" {
689+
source = "lacework/config/gcp"
690+
version = "~> 2.3"
691+
692+
for_each = {
693+
project1 = "project1"
694+
project2 = "project2"
695+
project3 = "project3"
696+
}
697+
project_id = each.key
698+
}
659699
`),
660700
},
661701
}

0 commit comments

Comments
 (0)