helpers/foundation-deployer/main.go (245 lines of code) (raw):
// Copyright 2023 Google LLC
//
// 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 (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
gotest "testing"
"time"
"github.com/mitchellh/go-testing-interface"
"github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/gcp"
"github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/msg"
"github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/stages"
"github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/steps"
"github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/utils"
)
var (
validatorApis = []string{
"securitycenter.googleapis.com",
"accesscontextmanager.googleapis.com",
}
)
type cfg struct {
tfvarsFile string
stepsFile string
resetStep string
quiet bool
help bool
listSteps bool
disablePrompt bool
validate bool
destroy bool
}
func parseFlags() cfg {
var c cfg
flag.StringVar(&c.tfvarsFile, "tfvars_file", "", "Full path to the Terraform .tfvars `file` with the configuration to be used.")
flag.StringVar(&c.stepsFile, "steps_file", ".steps.json", "Path to the steps `file` to be used to save progress.")
flag.StringVar(&c.resetStep, "reset_step", "", "Name of a `step` to be reset. The step will be marked as pending.")
flag.BoolVar(&c.quiet, "quiet", false, "If true, additional output is suppressed.")
flag.BoolVar(&c.help, "help", false, "Prints this help text and exits.")
flag.BoolVar(&c.listSteps, "list_steps", false, "List the existing steps.")
flag.BoolVar(&c.disablePrompt, "disable_prompt", false, "Disable interactive prompt.")
flag.BoolVar(&c.validate, "validate", false, "Validate tfvars file inputs.")
flag.BoolVar(&c.destroy, "destroy", false, "Destroy the deployment.")
flag.Parse()
return c
}
func main() {
cfg := parseFlags()
if cfg.help {
fmt.Println("Deploys the Terraform Example Foundation")
flag.PrintDefaults()
return
}
// load tfvars
globalTFVars, err := stages.ReadGlobalTFVars(cfg.tfvarsFile)
if err != nil {
fmt.Printf("# Failed to read GlobalTFVars file. Error: %s\n", err.Error())
os.Exit(1)
}
// validate Directories
err = stages.ValidateDirectories(globalTFVars)
if err != nil {
fmt.Printf("# Failed validating directories. Error: %s\n", err.Error())
os.Exit(1)
}
// init infra
gotest.Init()
t := &testing.RuntimeT{}
conf := stages.CommonConf{
FoundationPath: globalTFVars.FoundationCodePath,
CheckoutPath: globalTFVars.CodeCheckoutPath,
PolicyPath: filepath.Join(globalTFVars.FoundationCodePath, "policy-library"),
EnableHubAndSpoke: globalTFVars.EnableHubAndSpoke,
DisablePrompt: cfg.disablePrompt,
Logger: utils.GetLogger(cfg.quiet),
}
// only enable services if they are not already enabled
if globalTFVars.HasValidatorProj() {
conf.ValidatorProject = *globalTFVars.ValidatorProjectId
var apis []string
gcpConf := gcp.NewGCP()
for _, a := range validatorApis {
if !gcpConf.IsApiEnabled(t, *globalTFVars.ValidatorProjectId, a) {
apis = append(apis, a)
}
}
if len(apis) > 0 {
fmt.Printf("# Enabling APIs: %s in validator project '%s'\n", strings.Join(apis, ", "), *globalTFVars.ValidatorProjectId)
gcpConf.EnableApis(t, *globalTFVars.ValidatorProjectId, apis)
fmt.Println("# waiting for API propagation")
for i := 0; i < 20; i++ {
time.Sleep(10 * time.Second)
fmt.Println("# waiting for API propagation")
}
}
}
// validate inputs
if cfg.validate {
stages.ValidateBasicFields(t, globalTFVars)
stages.ValidateDestroyFlags(t, globalTFVars)
return
}
s, err := steps.LoadSteps(cfg.stepsFile)
if err != nil {
fmt.Printf("# failed to load state file %s. Error: %s\n", cfg.stepsFile, err.Error())
os.Exit(2)
}
if cfg.listSteps {
fmt.Println("# Executed steps:")
e := s.ListSteps()
if len(e) == 0 {
fmt.Println("# No steps executed")
return
}
for _, step := range e {
fmt.Println(step)
}
return
}
if cfg.resetStep != "" {
if err := s.ResetStep(cfg.resetStep); err != nil {
fmt.Printf("# Reset step failed. Error: %s\n", err.Error())
os.Exit(3)
}
return
}
// destroy stages
if cfg.destroy {
// Note: destroy is only terraform destroy, local directories are not deleted.
// 5-app-infra
msg.PrintStageMsg("Destroying 5-app-infra stage")
err = s.RunDestroyStep("bu1-example-app", func() error {
io := stages.GetInfraPipelineOutputs(t, conf.CheckoutPath, "bu1-example-app")
return stages.DestroyExampleAppStage(t, s, io, conf)
})
if err != nil {
fmt.Printf("# Example app step destroy failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 4-projects
msg.PrintStageMsg("Destroying 4-projects stage")
err = s.RunDestroyStep("gcp-projects", func() error {
bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath)
return stages.DestroyProjectsStage(t, s, bo, conf)
})
if err != nil {
fmt.Printf("# Projects step destroy failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 3-networks
msg.PrintStageMsg("Destroying 3-networks stage")
err = s.RunDestroyStep("gcp-networks", func() error {
bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath)
return stages.DestroyNetworksStage(t, s, bo, conf)
})
if err != nil {
fmt.Printf("# Networks step destroy failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 2-environments
msg.PrintStageMsg("Destroying 2-environments stage")
err = s.RunDestroyStep("gcp-environments", func() error {
bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath)
return stages.DestroyEnvStage(t, s, bo, conf)
})
if err != nil {
fmt.Printf("# Environments step destroy failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 1-org
msg.PrintStageMsg("Destroying 1-org stage")
err = s.RunDestroyStep("gcp-org", func() error {
bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath)
return stages.DestroyOrgStage(t, s, bo, conf)
})
if err != nil {
fmt.Printf("# Org step destroy failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 0-bootstrap
msg.PrintStageMsg("Destroying 0-bootstrap stage")
err = s.RunDestroyStep("gcp-bootstrap", func() error {
return stages.DestroyBootstrapStage(t, s, conf)
})
if err != nil {
fmt.Printf("# Bootstrap step destroy failed. Error: %s\n", err.Error())
os.Exit(3)
}
// clean up the steps file
err = steps.DeleteStepsFile(cfg.stepsFile)
if err != nil {
fmt.Printf("# failed to delete state file %s. Error: %s\n", cfg.stepsFile, err.Error())
os.Exit(3)
}
return
}
// deploy stages
// 0-bootstrap
msg.PrintStageMsg("Deploying 0-bootstrap stage")
skipInnerBuildMsg := s.IsStepComplete("gcp-bootstrap")
err = s.RunStep("gcp-bootstrap", func() error {
return stages.DeployBootstrapStage(t, s, globalTFVars, conf)
})
if err != nil {
fmt.Printf("# Bootstrap step failed. Error: %s\n", err.Error())
os.Exit(3)
}
bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath)
if skipInnerBuildMsg {
msg.PrintBuildMsg(bo.CICDProject, bo.DefaultRegion, conf.DisablePrompt)
}
msg.PrintQuotaMsg(bo.ProjectsSA, conf.DisablePrompt)
if globalTFVars.HasGroupsCreation() {
msg.PrintAdminGroupPermissionMsg(bo.BootstrapSA, conf.DisablePrompt)
}
// 1-org
msg.PrintStageMsg("Deploying 1-org stage")
err = s.RunStep("gcp-org", func() error {
return stages.DeployOrgStage(t, s, globalTFVars, bo, conf)
})
if err != nil {
fmt.Printf("# Org step failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 2-environments
msg.PrintStageMsg("Deploying 2-environments stage")
err = s.RunStep("gcp-environments", func() error {
return stages.DeployEnvStage(t, s, globalTFVars, bo, conf)
})
if err != nil {
fmt.Printf("# Environments step failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 3-networks
msg.PrintStageMsg("Deploying 3-networks stage")
err = s.RunStep("gcp-networks", func() error {
return stages.DeployNetworksStage(t, s, globalTFVars, bo, conf)
})
if err != nil {
fmt.Printf("# Networks step failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 4-projects
msg.PrintStageMsg("Deploying 4-projects stage")
msg.ConfirmQuota(bo.ProjectsSA, conf.DisablePrompt)
err = s.RunStep("gcp-projects", func() error {
return stages.DeployProjectsStage(t, s, globalTFVars, bo, conf)
})
if err != nil {
fmt.Printf("# Projects step failed. Error: %s\n", err.Error())
os.Exit(3)
}
// 5-app-infra
msg.PrintStageMsg("Deploying 5-app-infra stage")
io := stages.GetInfraPipelineOutputs(t, conf.CheckoutPath, "bu1-example-app")
io.RemoteStateBucket = bo.RemoteStateBucketProjects
msg.PrintBuildMsg(io.InfraPipeProj, io.DefaultRegion, conf.DisablePrompt)
err = s.RunStep("bu1-example-app", func() error {
return stages.DeployExampleAppStage(t, s, globalTFVars, io, conf)
})
if err != nil {
fmt.Printf("# Example app step failed. Error: %s\n", err.Error())
os.Exit(3)
}
}