Skip to content

Commit

Permalink
Merge pull request #57 from ministryofjustice/bulk-plan
Browse files Browse the repository at this point in the history
Add Terraform plan to CLI commands
  • Loading branch information
Alejandro Garrido Mota authored Nov 5, 2020
2 parents 42e351e + cf53042 commit 76f4158
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 19 deletions.
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ RUN \
coreutils \
curl \
findutils \
git \
git-crypt \
git \
gnupg \
grep \
openssl \
Expand All @@ -57,4 +57,3 @@ RUN curl -sLo /usr/local/bin/aws-iam-authenticator https://amazon-eks.s3-us-west
RUN chmod +x /usr/local/bin/*

CMD /bin/bash

19 changes: 15 additions & 4 deletions pkg/commands/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,22 @@ func addTerraformCmd(topLevel *cobra.Command) {
Run: func(cmd *cobra.Command, args []string) {
contextLogger := log.WithFields(log.Fields{"subcommand": "plan"})

contextLogger.Info("Executing terraform plan")
err := options.Plan()
if options.BulkTfPlanPaths == "" {
contextLogger.Info("Executing terraform plan")
err := options.Plan()

if err != nil {
contextLogger.Fatal("Error executing terraform plan - check the outputs")
}
} else {
err := options.BulkPlan()

if err != nil {
contextLogger.Fatal(err)
}

if err != nil {
contextLogger.Fatal("Error executing terraform plan - check the outputs")
}

},
}

Expand All @@ -86,6 +96,7 @@ func addCommonFlags(cmd *cobra.Command, o *terraform.Commander) {
cmd.PersistentFlags().StringVarP(&o.Workspace, "workspace", "w", "default", "Default workspace where terraform is going to be executed")
cmd.PersistentFlags().BoolVarP(&o.DisplayTfOutput, "display-tf-output", "d", true, "Display or not terraform plan output")
cmd.PersistentFlags().StringVarP(&o.VarFile, "var-file", "v", "", "tfvar to be used by terraform")
cmd.PersistentFlags().StringVar(&o.BulkTfPlanPaths, "dirs-file", "", "Required for bulk-plans, file path which holds directories where terraform plan is going to be executed")

cmd.MarkPersistentFlagRequired("aws-access-key-id")
cmd.MarkPersistentFlagRequired("aws-secret-access-key")
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

// This MUST match the number of the latest release on github
var Version = "1.5.2"
var Version = "1.6.0"

const owner = "ministryofjustice"
const repoName = "cloud-platform-cli"
Expand Down
121 changes: 109 additions & 12 deletions pkg/terraform/main.go → pkg/terraform/terraform.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
// Package terraform implements methods and functions for running
// Terraform commands, such as terraform init/plan/apply.
//
// The intention of this package is to call and run inside a CI/CD
// pipeline.
package terraform

import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"syscall"

log "github.com/sirupsen/logrus"
)

// Commander empty struct which methods to execute terraform
// Commander struct holds all data required to execute terraform.
type Commander struct {
action string
cmd []string
cmdDir string
cmdEnv []string
AccessKeyID string
SecretAccessKey string
Workspace string
VarFile string
DisplayTfOutput bool
BulkTfPlanPaths string
}

// Terraform creates terraform command to be executed
Expand All @@ -32,10 +42,19 @@ func (s *Commander) Terraform(args ...string) (*CmdOutput, error) {
"err": err,
"stdout": stdoutBuf.String(),
"stderr": stderrBuf.String(),
"dir": s.cmdDir,
})

cmd := exec.Command("terraform", args...)

if s.cmdDir != "" {
cmd.Dir = s.cmdDir
}

if s.cmdEnv != nil {
cmd.Env = s.cmdEnv
}

cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf

Expand All @@ -46,6 +65,7 @@ func (s *Commander) Terraform(args ...string) (*CmdOutput, error) {
exitCode = ws.ExitStatus()
}
contextLogger.Error("cmd.Run() failed")
return nil, err
} else {
ws := cmd.ProcessState.Sys().(syscall.WaitStatus)
exitCode = ws.ExitStatus()
Expand All @@ -64,21 +84,23 @@ func (s *Commander) Terraform(args ...string) (*CmdOutput, error) {
}
}

// Init is mandatory almost always before doing anything with terraform
func (s *Commander) Init() error {
// Init executes terraform init.
func (s *Commander) Init(p bool) error {

output, err := s.Terraform("init")
if err != nil {
log.Error(output.Stderr)
return err
}

log.Info(output.Stdout)
if p {
log.Info(output.Stdout)
}

return nil
}

// SelectWs is used to select certain workspace
// SelectWs is used to select certain workspace.
func (s *Commander) SelectWs(ws string) error {

output, err := s.Terraform("workspace", "select", ws)
Expand All @@ -91,9 +113,10 @@ func (s *Commander) SelectWs(ws string) error {
return nil
}

// CheckDivergence is used to select certain workspace
// CheckDivergence check that there are not changes within certain state, if there are
// it will return non-zero and pipeline will fail.
func (s *Commander) CheckDivergence() error {
err := s.Init()
err := s.Init(true)
if err != nil {
return err
}
Expand All @@ -105,7 +128,7 @@ func (s *Commander) CheckDivergence() error {

var cmd []string

// Check if user provided a terraform var-file
// Check if user provided a terraform var-file.
if s.VarFile != "" {
cmd = append([]string{fmt.Sprintf("-var-file=%s", s.VarFile)})
}
Expand Down Expand Up @@ -137,9 +160,9 @@ func (s *Commander) CheckDivergence() error {
return err
}

// Apply executes terraform apply
// Apply executes terraform apply.
func (s *Commander) Apply() error {
err := s.Init()
err := s.Init(true)
if err != nil {
return err
}
Expand Down Expand Up @@ -184,9 +207,9 @@ func (s *Commander) Apply() error {

}

// Plan executes terraform apply
// Plan executes terraform plan
func (s *Commander) Plan() error {
err := s.Init()
err := s.Init(false)
if err != nil {
return err
}
Expand Down Expand Up @@ -229,3 +252,77 @@ func (s *Commander) Plan() error {
return err

}

// Workspaces return the workspaces within the state.
func (c *Commander) workspaces() ([]string, error) {
arg := []string{
"workspace",
"list",
}

output, err := c.Terraform(arg...)
if err != nil {
log.Error(output.Stderr)
return nil, err
}

ws := strings.Split(output.Stdout, "\n")

return ws, nil
}

// BulkPlan executes plan against all directories that changed in the PR.
func (c *Commander) BulkPlan() error {
dirs, err := targetDirs(c.BulkTfPlanPaths)
if err != nil {
return err
}

for _, dir := range dirs {
fmt.Printf("\n")
fmt.Println("#########################################################################")
fmt.Printf("PLAN FOR DIRECTORY: %v\n", dir)
fmt.Println("#########################################################################")
fmt.Printf("\n")
c.cmdDir = dir
err := c.Init(false)
if err != nil {
return err
}

ws, err := c.workspaces()
if err != nil {
return err
}

if contains(ws, " live-1") {
log.WithFields(log.Fields{"dir": dir}).Info("Using live-1 context with: KUBE_CTX=live-1.cloud-platform.service.justice.gov.uk")

c.cmdEnv = append(os.Environ(), "KUBE_CTX=live-1.cloud-platform.service.justice.gov.uk")
c.Workspace = "live-1"

err := c.Plan()
if err != nil {
return err
}
} else if contains(ws, " manager") {
log.WithFields(log.Fields{"dir": dir}).Info("Using manager context with: KUBE_CTX=manager.cloud-platform.service.justice.gov.uk")

c.cmdEnv = append(os.Environ(), "KUBE_CTX=manager.cloud-platform.service.justice.gov.uk")
c.Workspace = "manager"

err := c.Plan()
if err != nil {
return err
}
} else {
log.WithFields(log.Fields{"dir": dir}).Info("No context, normal terraform plan")
err := c.Plan()
if err != nil {
return err
}
}
}

return nil
}
52 changes: 52 additions & 0 deletions pkg/terraform/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package terraform

import (
"bufio"
"os"
"path/filepath"
)

// targetDirs return the directories where terraform plan is going to be executed
func targetDirs(file string) ([]string, error) {
var dirs []string // Directories where tf plan is going to be executed

dirsWhitelist := []string{
"terraform/cloud-platform-components",
"terraform/cloud-platform",
"terraform/cloud-platform-eks/components",
"terraform/cloud-platform-eks",
}

f, err := os.Open(file)
if err != nil {
return nil, err
}

defer f.Close()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
// The first condition evaluates if the element already exists in the slice (why to execute
// plan twice against the same dir?). The second condition evaluates if the element is in
// the desired list to execute Plan (we don't want to execute Plan against everything)
if contains(dirs, filepath.Dir(scanner.Text())) != true &&
contains(dirsWhitelist, filepath.Dir(scanner.Text())) == true {
dirs = append(dirs, filepath.Dir(scanner.Text()))
}
}

if err := scanner.Err(); err != nil {
return nil, err
}

return dirs, nil
}

func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
47 changes: 47 additions & 0 deletions pkg/terraform/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package terraform

import (
"fmt"
"os"
"strings"
"testing"
)

// TestTargetDirs confirms given a file directory (string) in a text file,
// the targetDirs function will extract the directory path successfully.
func TestTargetDirs(t *testing.T) {
fileName := "changedFiles"
fileString := "/this/is/a/test/dir"

// A temp file is created with a string inside
file, err := os.Create(fileName)
if err != nil {
fmt.Println(err)
}
_, err = file.WriteString(fileString)
if err != nil {
fmt.Println(err)
}
err = file.Close()
if err != nil {
fmt.Println(err)
}

// The temp file is passed to the targetDirs function so it can extract the string containing
// a directory path.
target, err := targetDirs(fileName)
if err != nil {
fmt.Println(err)
}

// The value extracted from the text file is compared to its expected value. If false, the
// test will fail.
for _, v := range target {
if strings.Contains(fileString, v) {
fmt.Println("Test passes")
} else {
t.Error("Files do not match, test fails")
}
}
os.Remove(fileName)
}

0 comments on commit 76f4158

Please sign in to comment.