server.garden privileged automation agent (mirror of https://git.sequentialread.com/forest/rootsystem)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

931 lines
31 KiB

package automation
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
"git.sequentialread.com/forest/rootsystem/configuration"
)
type TerraformShow struct {
PlannedValues TerraformShowPlannedValues `json:"planned_values"`
Configuraton TerraformShowConfiguration `json:"configuration"`
ResourceChanges []*TerraformShowResourceChange `json:"resource_changes"`
}
type TerraformShowPlannedValues struct {
RootModule *TerraformShowPlannedValuesRoot `json:"root_module"`
}
type TerraformShowPlannedValuesRoot struct {
ChildModules []*TerraformShowModule `json:"child_modules"`
}
type TerraformShowConfiguration struct {
RootModule TerraformShowRootModule `json:"root_module"`
}
type TerraformShowRootModule struct {
ModuleCalls map[string]*TerraformShowModuleCall `json:"module_calls"`
}
type TerraformShowModule struct {
Resources []*TerraformShowResource `json:"resources"`
Address string `json:"address"`
}
type TerraformShowResource struct {
Address string `json:"address"`
Type string `json:"type"`
Name string `json:"name"`
Index int `json:"index"`
Values map[string]interface{} `json:"values"`
}
type ResourceChangeFilter struct {
ResourceType string
Action string
NotAction string
}
type TerraformShowResourceChange struct {
TerraformShowResource
ModuleAddress string `json:"module_address"`
Change TerraformShowChangeSpec `json:"change"`
}
type TerraformShowChangeSpec struct {
Actions []string `json:"actions"`
}
type TerraformShowModuleCall struct {
Expressions map[string]*TerraformShowExpression `json:"expressions"`
Module *TerraformShowModule `json:"module"`
}
type TerraformShowExpression struct {
References []string `json:"references"`
}
type SimplifiedTerraformStatus struct {
Modules map[string]*SimplifiedTerraformModule
Variables map[string]string
Connections []SimplifiedTerraformConnection
}
type SimplifiedTerraformModule struct {
DisplayName string
IsAnsible bool
Resources []*SimplifiedTerraformResource
}
type SimplifiedTerraformResource struct {
ResourceType string
DisplayName string
Plan string
State string
Progress int
ProgressTotal int
}
type SimplifiedTerraformConnection struct {
From string
To string
DisplayName string
}
type AnsibleTaskResult struct {
Name string
Success bool
Skipped bool
Mode string
Role string
Changed bool
}
type TerraformApplyResult struct {
Error error
Success bool
Log string
Complete bool
Status *SimplifiedTerraformStatus
}
const data_template_file = "data.template_file"
func TerraformPlanAndApply(
config *configuration.Configuration,
workingDirectory string,
terraformProject string,
) ([]byte, chan TerraformApplyResult, error) {
terraformDirectory := filepath.Join(workingDirectory, terraformProject)
// Under normal conditions you would not init every time. But I ran into some issues and decided doing this
// every time was the best course of action.
exitCode, initStdout, initStderr, err := shellExec(terraformDirectory, "terraform", "init")
err = errorFromShellExecResult("terraform init", exitCode, initStdout, initStderr, err)
if err != nil {
return []byte{}, nil, err
}
// Convenience function so we can plan multiple times if needed
doPlan := func(terraformDirectory string) (*SimplifiedTerraformStatus, *TerraformShow, string, error) {
exitCode, planStdout, planStderr, err := shellExec(terraformDirectory, "terraform", "plan", "-out", configuration.TERRAFORM_PLAN_FILE_NAME)
err = errorFromShellExecResult("terraform plan", exitCode, planStdout, planStderr, err)
if err != nil {
return nil, nil, "", err
}
exitCode, tfJson, showJsonStderr, err := shellExec(terraformDirectory, "terraform", "show", "-json", configuration.TERRAFORM_PLAN_FILE_NAME)
err = errorFromShellExecResult("terraform show", exitCode, tfJson, showJsonStderr, err)
if err != nil {
return nil, nil, "", err
}
// log.Println("--------------------------")
// log.Println(string(tfJson))
// log.Println("--------------------------")
var tfShow TerraformShow
err = json.Unmarshal([]byte(tfJson), &tfShow)
if err != nil {
return nil, nil, "", errors.Wrap(err, "can't TerraformPlanAndApply because can't json.Unmarshal the output of `terraform show`")
}
// json, err := json.MarshalIndent(tfShow, "", " ")
// if err != nil {
// return nil, nil, "", errors.Wrap(err, "can't GenerateTerraformPlan because can't json.Marshal the output of `terraform show`")
// }
// log.Println(string(json))
// log.Println("--------------------------")
// Copy some values over from PlannedValues to ResourceChanges for convenience access later
changedResources := map[string]*TerraformShowResource{}
for _, module := range tfShow.PlannedValues.RootModule.ChildModules {
for _, resource := range module.Resources {
changedResources[resource.Address] = resource
}
}
for _, changedResource := range tfShow.ResourceChanges {
resourceWithValues := changedResources[changedResource.Address]
if resourceWithValues != nil {
changedResource.Index = resourceWithValues.Index
changedResource.Type = resourceWithValues.Type
changedResource.Name = resourceWithValues.Name
changedResource.Values = resourceWithValues.Values
} else {
// TODO this can happen when a resource is deleted ( like an extra ssh key being deleted )
// for example:
// "planned_values": { "root_module": { "child_modules": [
// ...,
// {
// "resources": [ { "address": "module.ssh-keys-digitalocean.digitalocean_ssh_key.default[0]", ... } ],
// "address": "module.ssh-keys-digitalocean"
// }
// ] } }
// "resource_changes": [
// ...
// {
// "address": "module.ssh-keys-digitalocean.digitalocean_ssh_key.default[0]",
// "change": { "actions": [ "no-op" ] }
// },
// {
// "address": "module.ssh-keys-digitalocean.digitalocean_ssh_key.default[1]",
// "change": { "actions": [ "delete" ] }
// }
// ]
// do we care??
log.Printf(
"TerraformPlanAndApply(): doPlan(): found tfShow.ResourceChanges entry %s which is not in changedResources\n",
changedResource.Address,
)
}
}
simpleStatus, err := makeSimplifiedTerraformStatus(config, workingDirectory, tfShow)
if err != nil {
return nil, nil, "", errors.Wrap(err, "can't TerraformPlanAndApply because can't makeSimplifiedTerraformStatus")
}
// json, err := json.MarshalIndent(simpleStatus, "", " ")
// if err != nil {
// return nil, errors.Wrap(err, "can't GenerateTerraformPlan because can't json.Marshal the simpleStatus")
// }
// log.Println(string(json))
return &simpleStatus, &tfShow, string(planStdout), nil
}
simpleStatus, tfShow, planStdout, err := doPlan(terraformDirectory)
if err != nil {
return []byte{}, nil, errors.Wrap(err, "can't TerraformPlanAndApply because can't doPlan()")
}
// After plan but before apply, check over the plan and fix any known issues with cloud providers
stateModified := false
// DigitalOcean
// TODO remove/import any orphaned server.garden tagged instances?
mod, err := handleDigitalOceanSSHKeyAlreadyExists(config, workingDirectory, terraformDirectory, tfShow)
if mod {
stateModified = true
}
// Gandi.net
//
mod, err = handleGandiLiveDNSRecordAlreadyExists(config, workingDirectory, terraformDirectory, tfShow)
if mod {
stateModified = true
}
// After we try to fix any known issues, we may have to plan again if the terraform state was changed
// for example, with terraform import or state commands.
if stateModified {
simpleStatus, tfShow, planStdout, err = doPlan(terraformDirectory)
if err != nil {
return []byte{}, nil, errors.Wrap(err, "can't TerraformPlanAndApply because can't doPlan()")
}
}
svg, err := makeSVGFromSimpleStatus(simpleStatus)
if err != nil {
return []byte{}, nil, errors.Wrap(err, "can't TerraformPlanAndApply because can't makeSVGFromSimpleStatus")
}
for moduleName, module := range simpleStatus.Modules {
if module.IsAnsible {
err := linkAnsibleWrapperToModule(strings.TrimPrefix(moduleName, "module."), workingDirectory, terraformProject, true)
if err != nil {
return []byte{}, nil, errors.Wrap(err, "can't TerraformPlanAndApply because can't linkAnsibleWrapperToModule")
}
}
}
process := exec.Command("terraform", "apply", "-auto-approve", configuration.TERRAFORM_PLAN_FILE_NAME)
process.Dir = terraformDirectory
logLinesChannel := make(chan string)
stdoutPipe, err := process.StdoutPipe()
if err != nil {
return []byte{}, nil, errors.Wrap(err, "can't TerraformPlanAndApply because can't process.StdoutPipe() terraform apply process")
}
stderrPipe, err := process.StderrPipe()
if err != nil {
return []byte{}, nil, errors.Wrap(err, "can't TerraformPlanAndApply because can't process.StderrPipe() terraform apply process")
}
err = process.Start()
if err != nil {
return []byte{}, nil, errors.Wrap(err, "can't TerraformPlanAndApply because can't process.Start() terraform apply process")
}
scanAllOutput := func(reader io.ReadCloser) {
defer func() {
if err := recover(); err != nil {
log.Println("scanAllOutput panic:")
log.Println(err)
}
}()
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
logLinesChannel <- scanner.Text()
}
}
go scanAllOutput(stdoutPipe)
go scanAllOutput(stderrPipe)
toReturn := make(chan TerraformApplyResult)
logSoFar := strings.Join(
[]string{
fmt.Sprintf("\n(%s) $ terraform init\n", terraformDirectory),
string(initStdout),
fmt.Sprintf("\n(%s) $ terraform plan -out %s\n", terraformDirectory, configuration.TERRAFORM_PLAN_FILE_NAME),
planStdout,
fmt.Sprintf("\n(%s) $ terraform apply -auto-approve %s\n", terraformDirectory, configuration.TERRAFORM_PLAN_FILE_NAME),
},
"\n",
)
go monitorTerraformApplyProgress(workingDirectory, terraformProject, simpleStatus, process, logSoFar, logLinesChannel, toReturn)
return svg, toReturn, nil
}
func monitorTerraformApplyProgress(
workingDirectory string,
terraformProject string,
simpleStatus *SimplifiedTerraformStatus,
applyProcess *exec.Cmd,
logSoFar string,
logLinesChannel chan string,
outputChannel chan TerraformApplyResult,
) {
//ansibleModules := map[string]bool
logLines := []string{}
logLinesWithoutAnsiEscapes := []string{}
terraformIsRunning := true
terraformDirectory := filepath.Join(workingDirectory, terraformProject)
ansibleModuleLevelErrors := map[string]bool{}
ansibleModulesThatHaveAtLeastOneSuccessfulTask := map[string]bool{}
// https://github.com/acarl005/stripansi/blob/master/stripansi.go
ansiEscapeRegex := regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
go (func() {
terraformApplyLogRegex := regexp.MustCompile(`^(([a-zA-Z0-9_-]+\.)+[a-zA-Z0-9_-]+)(\[([0-9]+)\])?( \([a-zA-Z0-9_-]+\))?: (.*)$`)
terraformExecErrorRegex := regexp.MustCompile(`Error running command '(.*)': exit status [0-9]+\. Output:.*$`)
for logLine := range logLinesChannel {
logLines = append(logLines, logLine)
lineWithoutAnsiEscapes := ansiEscapeRegex.ReplaceAllString(logLine, "")
logLinesWithoutAnsiEscapes = append(logLinesWithoutAnsiEscapes, lineWithoutAnsiEscapes)
matches := terraformApplyLogRegex.FindStringSubmatch(lineWithoutAnsiEscapes)
if matches != nil {
address := matches[1]
//index := matches[4]
message := matches[6]
// address = strings.Trim(address, ".")
// address = regexp.MustCompile(`\[[0-9]+\]$`).ReplaceAllString(address, "")
foundModule := false
for moduleName, module := range simpleStatus.Modules {
if strings.HasPrefix(address, moduleName) {
foundModule = true
if module.IsAnsible {
// TODO test this strings.HasPrefix(message, "Executing:") instead
//if strings.HasPrefix(message, "Creating...") || strings.HasPrefix(message, "Modifying...") {
if strings.HasPrefix(message, "Executing:") {
//ansibleModules[moduleName] = true
//fmt.Printf("__INCLUDE_ANSIBLE__%s\n", module.DisplayName)
logLines = append(logLines, fmt.Sprintf("__INCLUDE_ANSIBLE__%s", module.DisplayName))
}
} else {
addressInsideModule := strings.TrimPrefix(address, moduleName)
addressInsideModule = strings.Trim(addressInsideModule, ".")
foundResource := false
for _, resource := range module.Resources {
if addressInsideModule == resource.DisplayName {
foundResource = true
// https://github.com/hashicorp/terraform/blob/5d0b75df7ae65e9f6a8314961c253ca1085c6534/command/hook_ui.go#L78
// https://github.com/hashicorp/terraform/blob/5d0b75df7ae65e9f6a8314961c253ca1085c6534/command/hook_ui.go#L243
if strings.HasPrefix(message, "Creating...") {
resource.State = "creating"
}
if strings.HasPrefix(message, "Creation complete") {
resource.State = "ok"
}
if strings.HasPrefix(message, "Modifying...") {
resource.State = "modifying"
}
if strings.HasPrefix(message, "Modifications complete") {
resource.State = "ok"
}
if strings.HasPrefix(message, "Destroying...") {
resource.State = "destroying"
}
if strings.HasPrefix(message, "Destruction complete") {
resource.State = "destroyed"
}
//fmt.Printf("non-ansible: %s %s %s %s\n", moduleName, resource.DisplayName, resource.State, message)
}
}
if !foundResource {
//fmt.Printf("!foundResource %s %s: %s\n", address, addressInsideModule, message)
}
}
}
}
if !foundModule {
//fmt.Printf("!foundModule %s: %s\n", address, message)
}
} else {
//fmt.Printf("NOT PARSE %s\n", lineWithoutAnsiEscapes)
// Check if there is a line which contains an exec error.
// If there is such a line, it won't contain the module that failed, just the command that failed.
// so we have to look back in the log to find the last time that command was mentioned, which should hit the
// line which has the module / resource name on it.
// So for example we could find a line:
//
// Error: Error running command './ansible-playbook-wrapper --private-key '/usr/lib/rootsystem/ssh/servergarden_builtin_ed22519' -i '167.71.175.207,' -u root -e 'domain=greenhouseusers.com arch=amd64' playbook.yml': exit status 4. Output:
//
// Then we look back in the log for any line which contains that command, and we find:
//
// module.ansible-threshold-server.null_resource.ansible_playbook[0] (local-exec): Executing: ["/bin/sh" "-c" "./ansible-playbook-wrapper --private-key '/usr/lib/rootsystem/ssh/servergarden_builtin_ed22519' -i '167.71.175.207,' -u root -e 'domain=greenhouseusers.com arch=amd64' playbook.yml"]
//
matches := terraformExecErrorRegex.FindStringSubmatch(lineWithoutAnsiEscapes)
if matches != nil {
commandThatFailed := matches[1]
log.Printf("terraformExecErrorRegex matched %s", lineWithoutAnsiEscapes)
log.Printf("terraformExecErrorRegex matched %s", commandThatFailed)
previousLineContainingCommandThatFailed := ""
for i, previousLine := range logLinesWithoutAnsiEscapes {
if i < len(logLinesWithoutAnsiEscapes)-2 && strings.Contains(previousLine, commandThatFailed) {
previousLineContainingCommandThatFailed = previousLine
}
}
if previousLineContainingCommandThatFailed != "" {
log.Printf("previousLineContainingCommandThatFailed: %s", previousLineContainingCommandThatFailed)
matches = terraformApplyLogRegex.FindStringSubmatch(previousLineContainingCommandThatFailed)
if matches != nil {
address := matches[1]
log.Printf("address of previousLineContainingCommandThatFailed: %s", address)
for moduleName, module := range simpleStatus.Modules {
if strings.HasPrefix(address, moduleName) {
if module.IsAnsible {
if !ansibleModulesThatHaveAtLeastOneSuccessfulTask[module.DisplayName] {
log.Printf("setting ansible module %s resources to state error because ansible-playbook failed to start/connect", module.DisplayName)
ansibleModuleLevelErrors[module.DisplayName] = true
for _, resource := range module.Resources {
resource.State = "error"
}
} else {
log.Printf("at least one task succeeded in ansible module %s, skipping cuz ansible-playbook error handling will take care of it.", module.DisplayName)
}
} else {
addressInsideModule := strings.TrimPrefix(address, moduleName)
addressInsideModule = strings.Trim(addressInsideModule, ".")
log.Printf("looking for module %s resource %s...", moduleName, addressInsideModule)
for _, resource := range module.Resources {
if addressInsideModule == resource.DisplayName {
log.Printf("setting module %s resource %s to state error", moduleName, addressInsideModule)
resource.State = "error"
}
}
}
}
}
}
}
}
}
}
})()
joinAnsibleLogsAndUpdateAnsibleStatus := func() string {
processedLogLines := []string{}
for _, line := range logLines {
if strings.HasPrefix(line, "__INCLUDE_ANSIBLE__") {
moduleName := strings.TrimPrefix(line, "__INCLUDE_ANSIBLE__")
logBytes, err := ioutil.ReadFile(filepath.Join(terraformDirectory, "modules", moduleName, "ansible.log"))
if err == nil {
processedLogLines = append(processedLogLines, string(logBytes))
} else {
fmt.Printf("error trying to read ansible log: %s\n", err)
}
// if the entire ansible playbook errored out (like, it couldn't connect or something)
// then the status of the individual roles has already been set to error, we don't want to overwrite that.
// so just exit early
if ansibleModuleLevelErrors[moduleName] {
continue
}
jsonBytes, err := ioutil.ReadFile(filepath.Join(terraformDirectory, "modules", moduleName, "ansible-log.json"))
if err == nil {
var ansibleLog []AnsibleTaskResult
//log.Printf("%s\n", string(jsonBytes))
err = json.Unmarshal(jsonBytes, &ansibleLog)
if err == nil {
module, has := simpleStatus.Modules[fmt.Sprintf("module.%s", moduleName)]
if has {
ansibleRoles := map[string]int{}
ansibleRoleErrors := map[string]int{}
//ansibleRolesErrors := map[string]int{}
for _, ansibleResult := range ansibleLog {
if ansibleResult.Role == "" {
continue
}
if ansibleResult.Success && !ansibleResult.Skipped {
ansibleModulesThatHaveAtLeastOneSuccessfulTask[moduleName] = true
}
if ansibleResult.Success || ansibleResult.Skipped {
ansibleRoles[ansibleResult.Role]++
}
if !ansibleResult.Success && !ansibleResult.Skipped {
ansibleRoleErrors[ansibleResult.Role]++
}
}
for _, resource := range module.Resources {
resource.Progress = ansibleRoles[resource.DisplayName]
if ansibleRoleErrors[resource.DisplayName] > 0 {
resource.State = "error"
} else if resource.Progress < resource.ProgressTotal {
resource.State = "creating"
} else {
resource.State = "ok"
}
// fmt.Printf(
// "ansible %s %s %s (%d/%d)\n",
// fmt.Sprintf("module.%s", module.DisplayName), resource.DisplayName, resource.State, resource.Progress, resource.ProgressTotal,
// )
}
} else {
//fmt.Printf("module %s not found\n", fmt.Sprintf("module.%s", module))
}
}
} else {
//fmt.Printf("ansible-log.json: %s\n", err)
}
} else {
processedLogLines = append(processedLogLines, line)
}
}
return fmt.Sprintf("%s%s", logSoFar, strings.Join(processedLogLines, "\n"))
}
go (func() {
for terraformIsRunning {
time.Sleep(time.Second * configuration.TERRAFORM_APPLY_STATUS_UPDATE_INTERVAL_SECONDS)
if terraformIsRunning {
log := joinAnsibleLogsAndUpdateAnsibleStatus()
outputChannel <- TerraformApplyResult{
Error: nil,
Log: log,
Status: simpleStatus,
}
}
}
})()
err := applyProcess.Wait()
_, isExitError := err.(*exec.ExitError)
if err != nil && !isExitError {
err = errors.Wrap(err, "error waiting for terraform to finish running: ")
} else {
err = nil
}
// once upon a time I got an error
// panic: send on closed channel @ terraformActions.go:204 (logLinesChannel <- blahblah; in scanAllOutput())
// I assumed it was a race condition so I put this sleep here to try to fix it, lets see if it ever happens again...
go (func() {
time.Sleep(time.Millisecond * 100)
close(logLinesChannel)
})()
terraformIsRunning = false
terraformSuccess := true
if err == nil && applyProcess.ProcessState.ExitCode() != 0 {
// If the apply fails, its not really an exceptional case, we don't want to exit the application.
terraformSuccess = false
//err = errors.New(fmt.Sprintf("terraform apply failed: exit code %d", applyProcess.ProcessState.ExitCode()))
}
log := joinAnsibleLogsAndUpdateAnsibleStatus()
outputChannel <- TerraformApplyResult{
Error: err,
Complete: true,
Success: terraformSuccess,
Log: log,
Status: simpleStatus,
}
close(outputChannel)
// cleanUpSymlinksInTerraformProjects
for moduleName, module := range simpleStatus.Modules {
if module.IsAnsible {
linkAnsibleWrapperToModule(strings.TrimPrefix(moduleName, "module."), workingDirectory, terraformProject, false)
rolesFolder := filepath.Join(workingDirectory, configuration.TERRAFORM_MODULES, moduleName, "roles")
os.Remove(rolesFolder)
}
}
}
func makeSimplifiedTerraformStatus(
config *configuration.Configuration,
workingDirectory string,
tfShow TerraformShow,
) (SimplifiedTerraformStatus, error) {
omitFromDiagram := []string{
post_to_object_storage_shell_script,
data_template_file,
}
shouldOmit := func(name string) bool {
toReturn := false
for _, omit := range omitFromDiagram {
if strings.Contains(name, omit) {
toReturn = true
}
}
return toReturn
}
simpleModules := map[string]*SimplifiedTerraformModule{}
for name, module := range tfShow.Configuraton.RootModule.ModuleCalls {
if shouldOmit(name) {
continue
}
resources := []*SimplifiedTerraformResource{}
isAnsible := strings.HasPrefix(name, "ansible-")
if isAnsible {
rolesMap, err := getAnsibleRolesFromModule(name, workingDirectory)
if err != nil {
return SimplifiedTerraformStatus{}, errors.Wrapf(err, "cant getAnsibleRoles(%s) because", name)
}
for roleName, tasks := range rolesMap {
if shouldOmit(roleName) {
continue
}
resource := SimplifiedTerraformResource{
ResourceType: "ansible_role",
DisplayName: roleName,
Plan: "none",
State: "ok",
Progress: 0,
ProgressTotal: len(tasks),
}
resources = append(resources, &resource)
}
} else {
for _, resource := range module.Module.Resources {
if shouldOmit(resource.Address) {
continue
}
resource := SimplifiedTerraformResource{
ResourceType: resource.Type,
DisplayName: resource.Address,
Plan: "none",
State: "ok",
}
resources = append(resources, &resource)
}
}
if len(resources) == 0 {
resource := SimplifiedTerraformResource{
DisplayName: "none",
}
resources = append(resources, &resource)
}
simpleModules[fmt.Sprintf("module.%s", name)] = &SimplifiedTerraformModule{
DisplayName: name,
IsAnsible: isAnsible,
Resources: resources,
}
}
resourceIndexRegexp := regexp.MustCompile(`\[([0-9]+)\]$`)
for _, resourceChange := range tfShow.ResourceChanges {
// resourceChange.Values should have come from the corresponding resource.
// if there is no corresponding resource, I think it should be nil and that means it was
// a deposed resource or something... lets omit it
if resourceChange.Values != nil {
continue
}
module, has := simpleModules[resourceChange.ModuleAddress]
if has {
address := strings.TrimPrefix(resourceChange.Address, resourceChange.ModuleAddress)
address = strings.Trim(address, ".")
// TODO it looks like the simplified terraform status wraps up repeated resources into one
// and it takes the "Plan" and "State" from whichever was the last one
// Is that ok? do we need to change that ?
//indexMatches = resourceIndexRegexp.FindStringSubmatch(address)
address = resourceIndexRegexp.ReplaceAllString(address, "")
if shouldOmit(address) {
continue
}
foundResource := false
for _, resource := range module.Resources {
if resource.DisplayName == address || module.IsAnsible {
foundResource = true
create := false
delete := false
update := false
for _, action := range resourceChange.Change.Actions {
if action == "create" {
create = true
}
if action == "delete" {
delete = true
}
if action == "update" {
update = true
}
}
if create && delete {
resource.Plan = "recreate"
resource.State = "tainted"
} else if create {
resource.Plan = "create"
resource.State = "planned"
} else if delete {
resource.Plan = "delete"
resource.State = "tainted"
} else if update {
resource.Plan = "update"
resource.State = "tainted"
}
//fmt.Printf("init: %s %s %s %s\n", resourceChange.ModuleAddress, address, strings.Join(resourceChange.Change.Actions, ","), resource.State)
}
}
if !foundResource {
err := fmt.Errorf("no resource \"%s\" found in module \"%s\" when processing a resource change", address, resourceChange.ModuleAddress)
return SimplifiedTerraformStatus{}, err
}
} else {
err := fmt.Errorf("no module \"%s\" found when processing a resource change", resourceChange.ModuleAddress)
return SimplifiedTerraformStatus{}, err
}
}
simpleConnectionsMap := map[string]*SimplifiedTerraformConnection{}
for moduleName, module := range tfShow.Configuraton.RootModule.ModuleCalls {
if shouldOmit(moduleName) {
continue
}
for expressionName, expression := range module.Expressions {
if shouldOmit(expressionName) {
continue
}
for _, reference := range expression.References {
if shouldOmit(reference) {
continue
}
refSlice := strings.Split(reference, ".")
if len(refSlice) > 1 {
reference = strings.Join(refSlice[:2], ".")
simpleConnectionsMap[fmt.Sprintf("%s%s%s", reference, moduleName, expressionName)] = &SimplifiedTerraformConnection{
From: reference,
To: fmt.Sprintf("module.%s", moduleName),
DisplayName: expressionName,
}
}
}
}
}
simpleConnections := []SimplifiedTerraformConnection{}
for _, simpleConnection := range simpleConnectionsMap {
simpleConnections = append(simpleConnections, *simpleConnection)
}
simpleVariables := map[string]string{}
simpleVariables[ssh_public_keys] = "<secure shell public keys>"
simpleVariables[ssh_private_key_filepath] = ssh_private_key_filepath_value
for variableName, value := range config.Terraform.Variables {
if shouldOmit(variableName) {
continue
}
simpleVariables[variableName] = value
}
return SimplifiedTerraformStatus{
Modules: simpleModules,
Connections: simpleConnections,
Variables: simpleVariables,
}, nil
}
func linkAnsibleWrapperToModule(moduleName, workingDirectory, terraformProject string, createLink bool) error {
ansibleDirectory := filepath.Join(workingDirectory, terraformProject, "modules", moduleName)
for _, toLink := range configuration.GET_ANSIBLE_WRAPPER_FILES() {
inModule := filepath.Join(ansibleDirectory, toLink)
inAnsibleWrapper := filepath.Join(workingDirectory, configuration.ANSIBLE_WRAPPER_PATH, toLink)
os.Remove(inModule)
if createLink {
err := os.Symlink(inAnsibleWrapper, inModule)
if err != nil {
return errors.Wrapf(err, "could not create symbolic link for ansible wrapper")
}
}
}
return nil
}
func getAnsibleRolesFromModule(moduleName, workingDirectory string) (map[string][]string, error) {
ansibleDirectory := filepath.Join(workingDirectory, configuration.TERRAFORM_MODULES, moduleName)
rolesFolder := filepath.Join(ansibleDirectory, "roles")
os.Remove(rolesFolder)
err := os.Symlink(
filepath.Join(workingDirectory, configuration.ANSIBLE_ROLES),
rolesFolder,
)
if err != nil {
return nil, errors.Wrapf(err, "could not create symbolic link to ansible roles folder")
}
exitCode, ansibleStdout, ansibleStderr, err := shellExec(ansibleDirectory, "ansible-playbook", "--list-tasks", configuration.ANSIBLE_PLAYBOOK_FILE_NAME)
err = errorFromShellExecResult("ansible-playbook --list-tasks", exitCode, ansibleStdout, ansibleStderr, err)
if err != nil {
return nil, err
}
// ansibleStdout looks like:
// playbook: provision.yml
//
// play #1 (localhost): This is a hello-world example TAGS: []
// tasks:
// test-role : Create a file called '/tmp/testfile.txt' with the content 'hello world'. TAGS: []
// .....
matchTask := regexp.MustCompile(`\n\s+(( *[^ :]+)+) : (.*?)\s*TAGS: \[.+`)
taskMatches := matchTask.FindAllStringSubmatch(string(ansibleStdout), -1)
ansibleRoles := map[string][]string{}
for _, match := range taskMatches {
role := match[1]
task := match[3]
if _, has := ansibleRoles[role]; !has {
ansibleRoles[role] = []string{}
}
ansibleRoles[role] = append(ansibleRoles[role], task)
}
return ansibleRoles, nil
}
// TODO which user to run this command as ??
// Dont run as root. (this process probably runs as root)
func shellExec(workingDirectory string, executable string, arguments ...string) (int, []byte, []byte, error) {
return shellExecInputPipe(workingDirectory, nil, executable, arguments...)
}
func shellExecInputPipe(workingDirectory string, input *string, executable string, arguments ...string) (int, []byte, []byte, error) {
process := exec.Command(executable, arguments...)
process.Dir = workingDirectory
if input != nil {
stdin, err := process.StdinPipe()
if err != nil {
return -1, []byte{}, []byte{}, errors.Wrap(err, "process.StdinPipe() returned")
}
go func() {
defer stdin.Close()
io.WriteString(stdin, *input)
}()
}
var processStdoutBuffer, processStderrBuffer bytes.Buffer
process.Stdout = &processStdoutBuffer
process.Stderr = &processStderrBuffer
err := process.Start()
if err != nil {
err = errors.Wrapf(err, "can't ShellExec(%s %s), process.Start() returned", executable, strings.Join(arguments, " "))
return process.ProcessState.ExitCode(), []byte(""), []byte(""), err
}
err = process.Wait()
if err != nil {
err = errors.Wrapf(err, "can't ShellExec(%s %s), process.Wait() returned", executable, strings.Join(arguments, " "))
}
return process.ProcessState.ExitCode(), processStdoutBuffer.Bytes(), processStderrBuffer.Bytes(), err
}
func errorFromShellExecResult(command string, exitCode int, stdout []byte, stderr []byte, err error) error {
if exitCode != 0 || err != nil {
errorString := "nil"
if err != nil {
errorString = err.Error()
}
return fmt.Errorf(
"%s failed with exit code %d, stdout: \n----\n%s\n----\nstderr: \n----\n%s\n----\nerror: %s",
command, exitCode, stdout, stderr, errorString,
)
}
return nil
}