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.
 
 
 
 
 
 

225 lines
7.2 KiB

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
errors "git.sequentialread.com/forest/pkg-errors"
"git.sequentialread.com/forest/rootsystem/automation"
"git.sequentialread.com/forest/rootsystem/configuration"
"git.sequentialread.com/forest/rootsystem/objectStorage"
"git.sequentialread.com/forest/rootsystem/pki"
)
type applicationState struct {
workingDirectory string
storage objectStorage.ObjectStorager
}
var global applicationState
func main() {
config, workingDirectory, err := configuration.LoadConfiguration()
if err != nil {
panic(fmt.Sprintf("%+v", errors.Wrap(err, "rootsystem can't start because loadConfiguration() returned")))
}
global.workingDirectory = workingDirectory
storage, err := objectStorage.InitializeObjectStorage(config, true)
if err != nil {
panic(fmt.Sprintf("%+v", errors.Wrap(err, "rootsystem can't start because failed to initialize object storage")))
}
global.storage = storage
go terraformStateServer()
// This creates an access key that the gateway cloud instance can use to upload its SSH public key
// to our object storage. the host-key-poller will download this SSH host public key and add it to our known_hosts
// so that we can SSH to the gateway instance securely
hostKeysAccessSpec := objectStorage.ObjectStorageKey{
Name: "rootsystem-known-hosts",
PathPrefix: "rootsystem/known-hosts",
Read: true,
Write: true,
Delete: false,
List: false,
}
knownHostsCredentials, err := global.storage.CreateAccessKeyIfNotExists(hostKeysAccessSpec)
if err != nil {
panic(fmt.Sprintf("rootsystem can't start because can't create object storage access key for known_hosts: %+v", err))
}
// BuildTLSCertsForThreshold fills in the CAs, Keys, and Certificates in the Threshold ansible roles.
// So when terraform invokes ansible to install threshold client/server, it will install working
// certificates and keys
err = pki.BuildTLSCertsForThreshold(
global.workingDirectory,
config.Terraform.Variables["domain_name"],
config.Host.Name,
global.storage,
)
if err != nil {
panic(fmt.Sprintf("rootsystem can't start because can't create certs for threshold: %+v", err))
}
// Add 1 to the build number, each time rootsystem runs is a different build.
file, notFound, err := global.storage.Get("rootsystem/terraform-logs/build-number.txt")
if err != nil && !notFound {
panic(fmt.Sprintf("rootsystem can't start because can't download build-number.txt: %+v", err))
}
buildNumber := 1
if !notFound {
n, err := strconv.Atoi(string(file.Content))
if err != nil {
log.Printf("warning: build-number.txt did not contain a number. defaulting to build number 1. contents: %s\n", string(file.Content))
} else {
buildNumber = n + 1
}
}
err = global.storage.Put("rootsystem/terraform-logs/build-number.txt", []byte(strconv.Itoa(buildNumber)))
if err != nil {
panic(fmt.Sprintf("rootsystem can't start because can't upload build-number.txt: %+v", err))
}
// First, run the terraform build for the GLOBAL components, meaning the components
// that exist in the cloud, independent of how many server.garden nodes are being used.
outputVariables, success, err := terraformBuild(
config,
automation.TerraformConfiguration{
BuildNumber: buildNumber,
TargetedModules: config.Terraform.GlobalModules,
TerraformProject: configuration.GLOBAL_TERRAFORM_PROJECT,
HostKeysObjectStorageCredentials: knownHostsCredentials,
},
)
if err != nil {
log.Printf("rootsystem %s build errored out (exception): %+v", configuration.GLOBAL_TERRAFORM_PROJECT, err)
// Don't crash the app if the TF build failed, just sit there and do nothing. User has to do something to
// fix the build before we run again.
//panic(fmt.Sprintf("%+v", err))
} else if !success {
log.Printf("rootsystem %s build failed", configuration.GLOBAL_TERRAFORM_PROJECT)
} else {
// Next, we run a separate LOCAL terraform build which is specific to THIS server.garden node,
// this build will be responsible for installing software on this node & registering this node with the
// cloud resources
projectName := fmt.Sprintf("%s-%s", configuration.LOCAL_TERRAFORM_PROJECT, config.Host.Name)
_, success, err = terraformBuild(
config,
automation.TerraformConfiguration{
BuildNumber: buildNumber,
TargetedModules: config.Terraform.LocalModules,
TerraformProject: projectName,
RemoteState: configuration.GLOBAL_TERRAFORM_PROJECT,
RemoteStateVariables: outputVariables,
},
)
if err != nil {
log.Printf("rootsystem %s build errored out (exception): %+v", projectName, err)
//panic(fmt.Sprintf("%+v", err))
} else if !success {
log.Printf("rootsystem %s build failed", projectName)
}
}
// sit and do nothing forever.
a := make(chan bool)
<-a
}
func terraformBuild(
config *configuration.Configuration,
terraformConfig automation.TerraformConfiguration,
) ([]string, bool, error) {
outputVariables, err := automation.WriteTerraformCodeForTargetedModules(
config,
global.workingDirectory,
terraformConfig,
)
if err != nil {
return []string{}, false, err
}
fmt.Println("WriteTerraformCodeForTargetedModules done")
svg, statusChannel, err := automation.TerraformPlanAndApply(config, global.workingDirectory, terraformConfig.TerraformProject)
if err != nil {
return []string{}, false, err
}
fmt.Println("TerraformPlanAndApply kicked off")
diagramPath := fmt.Sprintf(
"rootsystem/terraform-logs/%s/%d/diagram.svg",
terraformConfig.TerraformProject, terraformConfig.BuildNumber,
)
err = global.storage.Put(diagramPath, svg)
if err != nil {
return []string{}, false, err
}
var terraformPlanAndApplyError error = nil
lastLog := ""
success := false
for status := range statusChannel {
statusJson, err := json.MarshalIndent(status, "", " ")
if err != nil {
return []string{}, false, err
}
newLog := strings.TrimPrefix(status.Log, lastLog)
lastLog = status.Log
log.Println(newLog)
//log.Printf("len(newLog): %d\n", len(newLog))
// status1 := automation.TerraformApplyResult{
// Error: status.Error,
// Success: status.Success,
// Complete: status.Complete,
// Status: status.Status,
// }
// statusJson1, err := json.MarshalIndent(status1, "", " ")
// if err != nil {
// return []string{}, false, err
// }
// log.Println(string(statusJson1))
statusPath := fmt.Sprintf(
"rootsystem/terraform-logs/%s/%d/status.json",
terraformConfig.TerraformProject, terraformConfig.BuildNumber,
)
err = global.storage.Put(statusPath, statusJson)
if err != nil {
log.Printf("can't upload terraform status update to object storage: %+v", err)
}
if status.Complete {
terraformPlanAndApplyError = status.Error
success = status.Success
}
}
if terraformPlanAndApplyError != nil {
return outputVariables, false, terraformPlanAndApplyError
}
return outputVariables, success, nil
}
func terraformStateServer() error {
// Make sure to only listen on localhost.
// TODO change this to HTTPS or unix socket
server := http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", configuration.TERRAFORM_STATE_SERVER_PORT_NUMBER),
Handler: terraformStateHandler{},
}
return server.ListenAndServe()
}