2020-10-01 01:07:35 +00:00
|
|
|
package automation
|
|
|
|
|
|
|
|
import (
|
2020-10-21 18:36:28 +00:00
|
|
|
"encoding/json"
|
2020-10-01 20:51:46 +00:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2020-10-03 04:07:47 +00:00
|
|
|
"log"
|
2020-10-01 20:51:46 +00:00
|
|
|
"os"
|
2020-10-03 04:07:47 +00:00
|
|
|
"os/exec"
|
2020-10-01 20:51:46 +00:00
|
|
|
"path"
|
2020-10-03 05:15:40 +00:00
|
|
|
"regexp"
|
2020-10-01 20:51:46 +00:00
|
|
|
"strings"
|
2020-10-03 04:07:47 +00:00
|
|
|
"time"
|
2020-10-01 20:51:46 +00:00
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
errors "git.sequentialread.com/forest/pkg-errors"
|
2020-10-01 01:07:35 +00:00
|
|
|
"git.sequentialread.com/forest/rootsystem/configuration"
|
2020-10-01 20:51:46 +00:00
|
|
|
composeloader "github.com/docker/cli/cli/compose/loader"
|
|
|
|
composeschema "github.com/docker/cli/cli/compose/schema"
|
|
|
|
composetypes "github.com/docker/cli/cli/compose/types"
|
2020-10-01 01:07:35 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func DockerComposeUp(
|
|
|
|
config *configuration.Configuration,
|
|
|
|
workingDirectory string,
|
|
|
|
) ([]byte, chan TerraformApplyResult, error) {
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
existingModuleFolders := map[string]bool{}
|
2020-10-01 20:51:46 +00:00
|
|
|
applicationModulesMap := map[string]*composetypes.ConfigDetails{}
|
|
|
|
applicationModulesPath := path.Join(workingDirectory, configuration.APPLICATION_MODULES_PATH)
|
|
|
|
fileInfos, err := ioutil.ReadDir(applicationModulesPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.Wrap(err, "can't list application modules directory")
|
|
|
|
}
|
|
|
|
for _, fileInfo := range fileInfos {
|
|
|
|
if fileInfo.IsDir() {
|
2020-10-21 18:36:28 +00:00
|
|
|
existingModuleFolders[fileInfo.Name()] = true
|
2020-10-01 20:51:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, moduleName := range config.ApplicationModules {
|
2020-10-21 18:36:28 +00:00
|
|
|
_, has := existingModuleFolders[moduleName]
|
|
|
|
if !has {
|
2020-10-01 20:51:46 +00:00
|
|
|
return nil, nil, errors.New(fmt.Sprintf("unknown application module '%s'", moduleName))
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
workingDir := path.Join(applicationModulesPath, moduleName)
|
|
|
|
dockerComposePath := path.Join(workingDir, "docker-compose.yml")
|
2020-10-01 20:51:46 +00:00
|
|
|
|
|
|
|
fileInfo, err := os.Stat(dockerComposePath)
|
|
|
|
if err != nil || fileInfo.IsDir() {
|
|
|
|
return nil, nil, errors.New(
|
|
|
|
fmt.Sprintf("application module '%s' doesn't contain a docker-compose.yml file", moduleName),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
bytes, err := ioutil.ReadFile(dockerComposePath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.Wrap(err, "can't read application module compose file")
|
|
|
|
}
|
|
|
|
config, err := composeloader.ParseYAML(bytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.New(
|
|
|
|
fmt.Sprintf("can't ParseYAML(docker-compose.yml) in application module '%s'", moduleName),
|
|
|
|
)
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
|
|
|
|
configDetails := composetypes.ConfigDetails{
|
|
|
|
WorkingDir: workingDir,
|
|
|
|
Version: composeschema.Version(config),
|
|
|
|
ConfigFiles: []composetypes.ConfigFile{
|
|
|
|
composetypes.ConfigFile{
|
|
|
|
Filename: dockerComposePath,
|
|
|
|
Config: config,
|
|
|
|
},
|
2020-10-01 20:51:46 +00:00
|
|
|
},
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
applicationModulesMap[moduleName] = &configDetails
|
2020-10-01 20:51:46 +00:00
|
|
|
// TODO set details.Environment ?
|
|
|
|
}
|
|
|
|
|
|
|
|
configs := map[string]*composetypes.Config{}
|
|
|
|
moduleByNetwork := map[string]string{}
|
2020-10-03 04:07:47 +00:00
|
|
|
connectionsMap := map[string]map[string]SimplifiedTerraformConnection{}
|
2020-10-01 20:51:46 +00:00
|
|
|
|
|
|
|
for _, moduleName := range config.ApplicationModules {
|
2020-10-21 18:36:28 +00:00
|
|
|
dockerCompose, err := composeloader.Load(*(applicationModulesMap[moduleName]))
|
2020-10-01 20:51:46 +00:00
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return nil, nil, errors.Wrapf(
|
|
|
|
err, "can't composeloader.Load(docker-compose.yml) for application module '%s'", moduleName,
|
2020-10-01 20:51:46 +00:00
|
|
|
)
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
configs[moduleName] = dockerCompose
|
2020-10-01 20:51:46 +00:00
|
|
|
createsDefaultNetwork := false
|
2020-10-21 18:36:28 +00:00
|
|
|
for _, service := range dockerCompose.Services {
|
2020-10-01 20:51:46 +00:00
|
|
|
for networkName := range service.Networks {
|
|
|
|
if networkName == "default" {
|
|
|
|
createsDefaultNetwork = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(service.Networks) == 0 {
|
|
|
|
createsDefaultNetwork = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if createsDefaultNetwork {
|
|
|
|
moduleByNetwork[fmt.Sprintf("%s_default", moduleName)] = moduleName
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
for networkId, network := range dockerCompose.Networks {
|
|
|
|
|
2020-10-01 20:51:46 +00:00
|
|
|
if !network.External.External {
|
|
|
|
networkName := fmt.Sprintf("%s_%s", moduleName, networkId)
|
|
|
|
if network.Name != "" {
|
|
|
|
networkName = network.Name
|
|
|
|
}
|
|
|
|
moduleByNetwork[networkName] = moduleName
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
// bytes2, _ := json.MarshalIndent(dockerCompose, "", " ")
|
|
|
|
// log.Printf("\ndockerCompose %s: %s\n\n", moduleName, string(bytes2))
|
2020-10-01 20:51:46 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
bytes1, _ := json.MarshalIndent(moduleByNetwork, "", " ")
|
|
|
|
log.Printf("\nmoduleByNetwork: %s\n\n", string(bytes1))
|
|
|
|
|
2020-10-01 20:51:46 +00:00
|
|
|
for _, moduleName := range config.ApplicationModules {
|
|
|
|
config := configs[moduleName]
|
|
|
|
for _, network := range config.Networks {
|
2020-10-21 18:36:28 +00:00
|
|
|
dependsOnModule := moduleByNetwork[network.Name]
|
|
|
|
|
2020-10-02 03:31:22 +00:00
|
|
|
if network.External.External && dependsOnModule != "" {
|
2020-10-03 04:07:47 +00:00
|
|
|
if connectionsMap[moduleName] == nil {
|
|
|
|
connectionsMap[moduleName] = map[string]SimplifiedTerraformConnection{}
|
|
|
|
}
|
|
|
|
connectionsMap[moduleName][dependsOnModule] = SimplifiedTerraformConnection{
|
|
|
|
From: dependsOnModule,
|
|
|
|
To: moduleName,
|
2020-10-21 18:36:28 +00:00
|
|
|
DisplayName: network.Name,
|
2020-10-02 03:31:22 +00:00
|
|
|
}
|
2020-10-01 20:51:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
//TODO external volumes as well..?
|
|
|
|
//TODO depends_on.. ?
|
2020-10-03 04:07:47 +00:00
|
|
|
//TODO dis-allow links? other validation?
|
2020-10-02 03:31:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sortedApplicationModulesMap := map[string]bool{}
|
|
|
|
sortedApplicationModules := []string{}
|
|
|
|
iterations := 0
|
|
|
|
|
|
|
|
for len(sortedApplicationModules) < len(config.ApplicationModules) && iterations < 100 {
|
|
|
|
iterations++
|
|
|
|
for _, moduleName := range config.ApplicationModules {
|
|
|
|
if !sortedApplicationModulesMap[moduleName] {
|
|
|
|
allDependenciesMet := true
|
2020-10-03 04:07:47 +00:00
|
|
|
dependencies := connectionsMap[moduleName]
|
2020-10-02 03:31:22 +00:00
|
|
|
if dependencies != nil {
|
2020-10-03 04:07:47 +00:00
|
|
|
for dependsOnModule := range dependencies {
|
2020-10-02 03:31:22 +00:00
|
|
|
if !sortedApplicationModulesMap[dependsOnModule] {
|
|
|
|
allDependenciesMet = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if allDependenciesMet {
|
|
|
|
sortedApplicationModulesMap[moduleName] = true
|
|
|
|
sortedApplicationModules = append(sortedApplicationModules, moduleName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-01 20:51:46 +00:00
|
|
|
}
|
|
|
|
|
2020-10-02 03:31:22 +00:00
|
|
|
if len(sortedApplicationModules) < len(config.ApplicationModules) {
|
|
|
|
return nil, nil, errors.New(
|
|
|
|
"circular dependency detected: can't sort application modules in dependency order",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
connections := []SimplifiedTerraformConnection{}
|
|
|
|
for _, dependsOn := range connectionsMap {
|
|
|
|
for _, connection := range dependsOn {
|
|
|
|
connections = append(connections, connection)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
simpleStatus, err := dockerComposePlan(configs, applicationModulesMap, connections)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.Wrap(err, "can't create docker-compose plan")
|
|
|
|
}
|
|
|
|
|
|
|
|
svgBytes, err := makeSVGFromSimpleStatus(simpleStatus)
|
|
|
|
//svg, err := makeSVGFromDockerCompose(moduleDependencies, applicationModulesMap, configs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, errors.Wrap(err, "can't create svg diagram from docker-compose plan document")
|
|
|
|
}
|
|
|
|
|
|
|
|
outputChannel := make(chan TerraformApplyResult)
|
|
|
|
|
|
|
|
go (func() {
|
|
|
|
logLinesChannel := make(chan string)
|
|
|
|
logLines := []string{}
|
|
|
|
dockerComposeIsRunning := true
|
|
|
|
go (func() {
|
|
|
|
for logLine := range logLinesChannel {
|
|
|
|
logLines = append(logLines, logLine)
|
2020-10-03 05:15:40 +00:00
|
|
|
split := strings.Split(logLine, " ... ")
|
|
|
|
if len(split) == 2 {
|
|
|
|
leftSide := strings.Split(strings.TrimSpace(split[0]), " ")
|
|
|
|
status := split[1]
|
|
|
|
if len(leftSide) == 2 {
|
|
|
|
action := leftSide[0]
|
|
|
|
containerName := leftSide[1]
|
|
|
|
for moduleName, _ := range configs {
|
|
|
|
moduleNameUnderscore := fmt.Sprintf("%s_", moduleName)
|
|
|
|
if strings.HasPrefix(containerName, moduleNameUnderscore) {
|
|
|
|
serviceName := strings.TrimPrefix(containerName, moduleNameUnderscore)
|
|
|
|
serviceName = regexp.MustCompile("_[0-9]$").ReplaceAllString(serviceName, "")
|
|
|
|
if simpleStatus.Modules[moduleName] != nil {
|
|
|
|
for _, service := range simpleStatus.Modules[moduleName].Resources {
|
|
|
|
if service.DisplayName == serviceName {
|
|
|
|
if status == "done" {
|
|
|
|
service.State = "ok"
|
|
|
|
} else if action == "Recreating" {
|
|
|
|
service.State = "modifying"
|
|
|
|
} else if action == "Creating" {
|
|
|
|
service.State = "creating"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-10-03 04:07:47 +00:00
|
|
|
}
|
|
|
|
})()
|
|
|
|
|
|
|
|
abort := func(err error) {
|
|
|
|
dockerComposeIsRunning = false
|
|
|
|
outputChannel <- TerraformApplyResult{
|
|
|
|
Error: err,
|
|
|
|
Complete: true,
|
|
|
|
Success: false,
|
|
|
|
Log: strings.Join(logLines, "\n"),
|
|
|
|
Status: simpleStatus,
|
|
|
|
}
|
|
|
|
close(outputChannel)
|
|
|
|
}
|
|
|
|
|
|
|
|
go (func() {
|
|
|
|
for dockerComposeIsRunning {
|
|
|
|
time.Sleep(time.Second * configuration.TERRAFORM_APPLY_STATUS_UPDATE_INTERVAL_SECONDS)
|
|
|
|
if dockerComposeIsRunning {
|
|
|
|
outputChannel <- TerraformApplyResult{
|
|
|
|
Error: nil,
|
|
|
|
Log: strings.Join(logLines, "\n"),
|
|
|
|
Status: simpleStatus,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})()
|
|
|
|
|
|
|
|
dockerComposeSuccess := true
|
|
|
|
|
|
|
|
for _, moduleName := range sortedApplicationModules {
|
|
|
|
// TODO handle panics in here with abort
|
|
|
|
details := applicationModulesMap[moduleName]
|
|
|
|
|
2020-10-03 05:15:40 +00:00
|
|
|
process := exec.Command("docker-compose", "--no-ansi", "up", "-d", "--remove-orphans")
|
|
|
|
logLinesChannel <- fmt.Sprintf("\n(%s) $ docker-compose --no-ansi up -d --remove-orphans\n", details.WorkingDir)
|
2020-10-03 04:07:47 +00:00
|
|
|
process.Dir = details.WorkingDir
|
|
|
|
|
|
|
|
stdoutPipe, err := process.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
abort(errors.Wrap(err, "can't DockerComposeUp because can't process.StdoutPipe() docker-compose process"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
stderrPipe, err := process.StderrPipe()
|
|
|
|
if err != nil {
|
|
|
|
abort(errors.Wrap(err, "can't DockerComposeUp because can't process.StderrPipe() docker-compose process"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = process.Start()
|
|
|
|
if err != nil {
|
|
|
|
abort(errors.Wrap(err, "can't DockerComposeUp because can't process.Start() docker-compose process"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
go scanAllOutput(stdoutPipe, logLinesChannel)
|
|
|
|
go scanAllOutput(stderrPipe, logLinesChannel)
|
|
|
|
|
|
|
|
err = process.Wait()
|
|
|
|
_, isExitError := err.(*exec.ExitError)
|
|
|
|
if err != nil && !isExitError {
|
|
|
|
abort(errors.Wrap(err, "can't DockerComposeUp, error occurred while waiting for docker-compose to finish"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if process.ProcessState.ExitCode() != 0 {
|
|
|
|
dockerComposeSuccess = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
dockerComposeIsRunning = false
|
2020-10-02 03:31:22 +00:00
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
if !dockerComposeSuccess {
|
|
|
|
for _, module := range simpleStatus.Modules {
|
|
|
|
for _, resource := range module.Resources {
|
|
|
|
if resource.Plan != "none" {
|
|
|
|
resource.State = "error"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-03 05:15:40 +00:00
|
|
|
time.Sleep(time.Millisecond * 100)
|
|
|
|
close(logLinesChannel)
|
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
outputChannel <- TerraformApplyResult{
|
|
|
|
Error: nil,
|
|
|
|
Complete: true,
|
|
|
|
Success: dockerComposeSuccess,
|
|
|
|
Log: strings.Join(logLines, "\n"),
|
|
|
|
Status: simpleStatus,
|
|
|
|
}
|
|
|
|
|
|
|
|
close(outputChannel)
|
|
|
|
|
|
|
|
})()
|
|
|
|
|
|
|
|
return svgBytes, outputChannel, nil
|
2020-10-01 20:51:46 +00:00
|
|
|
}
|
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
func dockerComposePlan(
|
2020-10-02 03:31:22 +00:00
|
|
|
configs map[string]*composetypes.Config,
|
2020-10-03 04:07:47 +00:00
|
|
|
configDetails map[string]*composetypes.ConfigDetails,
|
|
|
|
connections []SimplifiedTerraformConnection,
|
|
|
|
) (*SimplifiedTerraformStatus, error) {
|
2020-10-01 20:51:46 +00:00
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
tasks := []func() TaskResult{
|
|
|
|
func() TaskResult {
|
2020-11-04 07:24:31 +00:00
|
|
|
containers, err := ListDockerContainers()
|
2020-10-01 20:51:46 +00:00
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return TaskResult{
|
2020-10-03 04:07:47 +00:00
|
|
|
Name: "list_containers_task",
|
2020-10-21 18:36:28 +00:00
|
|
|
Err: errors.Wrap(err, "can't list docker containers"),
|
2020-10-03 04:07:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
return TaskResult{
|
2020-10-03 04:07:47 +00:00
|
|
|
Name: "list_containers_task",
|
|
|
|
Result: containers,
|
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for moduleName, details := range configDetails {
|
2020-10-21 18:36:28 +00:00
|
|
|
|
|
|
|
// https://stackoverflow.com/questions/26692844/captured-closure-for-loop-variable-in-go
|
|
|
|
// golang way of doing a closure over the CURRENT VALUE of a variable (not the variable itself)
|
|
|
|
moduleName := moduleName
|
|
|
|
details := *details
|
|
|
|
|
|
|
|
tasks = append(tasks, func() TaskResult {
|
|
|
|
taskName := fmt.Sprintf("%s_get_compose_config_hash", moduleName)
|
2020-10-03 04:07:47 +00:00
|
|
|
exitCode, stdout, stderr, err := shellExec(details.WorkingDir, "docker-compose", "config", "--hash=*")
|
|
|
|
err = errorFromShellExecResult("docker-compose config --hash=*", exitCode, stdout, stderr, err)
|
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return TaskResult{
|
2020-10-03 04:07:47 +00:00
|
|
|
Name: taskName,
|
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// $ sudo docker-compose config --hash=*
|
|
|
|
// threshold 5f6deb1fa7dfc76fa0dc04e96d22cb4a2cc05a02060a4a3e77d38b87ff1d7f82
|
|
|
|
// caddy 95b3516b900089a3d04376b2635bfbde5596ec3769c50f12ce53b65f2d2e62a0
|
|
|
|
lines := strings.Split(string(stdout), "\n")
|
|
|
|
configHashByServiceName := map[string]string{}
|
|
|
|
for _, line := range lines {
|
|
|
|
elements := strings.Split(line, " ")
|
|
|
|
if len(elements) == 2 {
|
|
|
|
configHashByServiceName[elements[0]] = elements[1]
|
|
|
|
}
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
return TaskResult{
|
2020-10-03 04:07:47 +00:00
|
|
|
Name: taskName,
|
|
|
|
Result: configHashByServiceName,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
results := DoInParallel(tasks...)
|
2020-10-03 04:07:47 +00:00
|
|
|
|
|
|
|
for _, result := range results {
|
|
|
|
if result.Err != nil {
|
|
|
|
return nil, errors.Wrapf(result.Err, "can't dockerComposePlan because %s failed", result.Name)
|
2020-10-01 20:51:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
containers := results["list_containers_task"].Result.([]DockerContainer)
|
|
|
|
containersByModuleService := map[string]DockerContainer{}
|
2020-10-03 04:07:47 +00:00
|
|
|
planModules := map[string]*SimplifiedTerraformModule{}
|
2020-10-02 03:31:22 +00:00
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
// First we go through all the containers returned by `docker ps -a` api command.
|
|
|
|
// we fill out the containersByModuleService map, and
|
|
|
|
// mark to be deleted any docker-compose containers that aren't in the current docker-compose configs.
|
|
|
|
for _, container := range containers {
|
|
|
|
containersModuleName := container.Labels["com.docker.compose.project"]
|
|
|
|
containersServiceName := container.Labels["com.docker.compose.service"]
|
|
|
|
if containersModuleName == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
containersConfig, has := configs[containersModuleName]
|
|
|
|
foundInConfig := false
|
|
|
|
if has {
|
|
|
|
if containersServiceName != "" {
|
|
|
|
containersByModuleService[fmt.Sprintf("%s_%s", containersModuleName, containersServiceName)] = container
|
|
|
|
}
|
|
|
|
var containersService composetypes.ServiceConfig
|
|
|
|
for _, service := range containersConfig.Services {
|
|
|
|
if service.Name == containersServiceName {
|
|
|
|
containersService = service
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if containersService.Name != "" {
|
|
|
|
foundInConfig = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !foundInConfig {
|
|
|
|
if _, has := planModules[containersModuleName]; !has {
|
|
|
|
planModules[containersModuleName] = &SimplifiedTerraformModule{
|
|
|
|
DisplayName: containersModuleName,
|
|
|
|
Resources: []*SimplifiedTerraformResource{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
planModules[containersModuleName].Resources = append(
|
|
|
|
planModules[containersModuleName].Resources,
|
|
|
|
&SimplifiedTerraformResource{
|
|
|
|
DisplayName: containersServiceName,
|
|
|
|
Plan: "delete",
|
|
|
|
},
|
2020-10-02 03:31:22 +00:00
|
|
|
)
|
2020-10-03 04:07:47 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-01 20:51:46 +00:00
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
// Now we go through the current docker-compose configs and mark any services
|
|
|
|
// that have no containers as to be created,
|
|
|
|
// plus mark any that have differing configHash as to be updated
|
|
|
|
for moduleName, config := range configs {
|
|
|
|
if _, has := planModules[moduleName]; !has {
|
|
|
|
planModules[moduleName] = &SimplifiedTerraformModule{
|
|
|
|
DisplayName: moduleName,
|
|
|
|
Resources: []*SimplifiedTerraformResource{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, service := range config.Services {
|
|
|
|
container := containersByModuleService[fmt.Sprintf("%s_%s", moduleName, service.Name)]
|
|
|
|
plan := "none"
|
2020-11-04 07:24:31 +00:00
|
|
|
if container.Id != "" {
|
2020-10-03 04:07:47 +00:00
|
|
|
existingConfigHash := container.Labels["com.docker.compose.config-hash"]
|
|
|
|
getConfigHashTaskName := fmt.Sprintf("%s_get_compose_config_hash", moduleName)
|
|
|
|
newConfigHash := results[getConfigHashTaskName].Result.(map[string]string)[service.Name]
|
|
|
|
if existingConfigHash != newConfigHash {
|
|
|
|
plan = "recreate"
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
plan = "create"
|
|
|
|
}
|
|
|
|
planModules[moduleName].Resources = append(
|
|
|
|
planModules[moduleName].Resources,
|
|
|
|
&SimplifiedTerraformResource{
|
|
|
|
DisplayName: service.Name,
|
|
|
|
Plan: plan,
|
|
|
|
},
|
|
|
|
)
|
2020-10-01 20:51:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
return &SimplifiedTerraformStatus{
|
|
|
|
Modules: planModules,
|
|
|
|
Variables: map[string]string{},
|
|
|
|
Connections: connections,
|
|
|
|
}, nil
|
2020-10-02 03:31:22 +00:00
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
}
|
2020-10-02 03:31:22 +00:00
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
func DoInParallel(actions ...func() TaskResult) map[string]TaskResult {
|
2020-10-03 04:07:47 +00:00
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
resultsChannel := make(chan TaskResult)
|
|
|
|
results := map[string]TaskResult{}
|
|
|
|
|
|
|
|
log.Printf("do %d actions in parallel", len(actions))
|
2020-10-03 04:07:47 +00:00
|
|
|
|
|
|
|
for _, action := range actions {
|
2020-10-21 18:36:28 +00:00
|
|
|
// this is how you do closures over the VALUE of a variable (not the variable itself) in golang
|
|
|
|
// https://stackoverflow.com/questions/26692844/captured-closure-for-loop-variable-in-go
|
|
|
|
action := action
|
2020-10-03 04:07:47 +00:00
|
|
|
go (func() {
|
|
|
|
result := action()
|
|
|
|
resultsChannel <- result
|
|
|
|
})()
|
2020-10-01 20:51:46 +00:00
|
|
|
}
|
|
|
|
|
2020-10-03 04:07:47 +00:00
|
|
|
for range actions {
|
2020-10-21 18:36:28 +00:00
|
|
|
result := <-resultsChannel
|
|
|
|
results[result.Name] = result
|
|
|
|
log.Printf("task '%s' completed", result.Name)
|
|
|
|
if result.Err != nil {
|
|
|
|
break
|
2020-10-03 04:07:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return results
|
|
|
|
}
|
2020-10-01 20:51:46 +00:00
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
type TaskResult struct {
|
2020-10-03 04:07:47 +00:00
|
|
|
Name string
|
|
|
|
Err error
|
|
|
|
Result interface{}
|
2020-10-01 01:07:35 +00:00
|
|
|
}
|
2020-10-03 04:07:47 +00:00
|
|
|
|
|
|
|
// func makeSVGFromDockerCompose(
|
|
|
|
// moduleDependencies map[string][]string,
|
|
|
|
// configs map[string]*composetypes.Config,
|
|
|
|
// ) ([]byte, error) {
|
|
|
|
|
|
|
|
// moduleDots := []string{}
|
|
|
|
// for moduleName := range configs {
|
|
|
|
// serviceDots := []string{}
|
|
|
|
// config := configs[moduleName]
|
|
|
|
|
|
|
|
// for _, service := range config.Services {
|
|
|
|
// id := fmt.Sprintf(`service_%s_%s`, moduleName, service.Name)
|
|
|
|
// id = strings.ReplaceAll(strings.ReplaceAll(id, ".", "_"), "-", "_")
|
|
|
|
// serviceDot := fmt.Sprintf(`
|
|
|
|
// "%s" [label = "%s", tooltip="%s", margin = 0.1, shape = "box3d]`,
|
|
|
|
// id, padStringForDot(service.Name), id,
|
|
|
|
// )
|
|
|
|
// serviceDots = append(serviceDots, serviceDot)
|
|
|
|
// }
|
|
|
|
// i := 0
|
|
|
|
// for _, service := range config.Services {
|
|
|
|
// if i != 0 {
|
|
|
|
// id0 := fmt.Sprintf(`service_%s_%s`, moduleName, config.Services[i-1].Name)
|
|
|
|
// id0 = strings.ReplaceAll(strings.ReplaceAll(id0, ".", "_"), "-", "_")
|
|
|
|
// id1 := fmt.Sprintf(`service_%s_%s`, moduleName, service.Name)
|
|
|
|
// id1 = strings.ReplaceAll(strings.ReplaceAll(id1, ".", "_"), "-", "_")
|
|
|
|
// serviceDot := fmt.Sprintf(`
|
|
|
|
// "%s" -> "%s" [style=invis]`, id0, id1,
|
|
|
|
// )
|
|
|
|
// serviceDots = append(serviceDots, serviceDot)
|
|
|
|
// }
|
|
|
|
// i++
|
|
|
|
// }
|
|
|
|
|
|
|
|
// moduleDot := fmt.Sprintf(`
|
|
|
|
// subgraph cluster_%s {
|
|
|
|
// bgcolor = lightgrey;
|
|
|
|
// tooltip = "%s";
|
|
|
|
// label = "%s";
|
|
|
|
// %s
|
|
|
|
// }`,
|
|
|
|
// strings.ReplaceAll(strings.ReplaceAll(moduleName, ".", "_"), "-", "_"),
|
|
|
|
// moduleName,
|
|
|
|
// padStringForDot(moduleName),
|
|
|
|
// strings.Join(serviceDots, ""),
|
|
|
|
// )
|
|
|
|
// moduleDots = append(moduleDots, moduleDot)
|
|
|
|
// }
|
|
|
|
|
|
|
|
// dependencyDots := []string{}
|
|
|
|
// for to, fromList := range moduleDependencies {
|
|
|
|
// for _, from := range fromList {
|
|
|
|
// fromServiceName := configs[from].Services[len(configs[from].Services)-1].Name
|
|
|
|
// toServiceName := configs[to].Services[0].Name
|
|
|
|
|
|
|
|
// fromService := fmt.Sprintf(`service_%s_%s`, from, fromServiceName)
|
|
|
|
// toService := fmt.Sprintf(`service_%s_%s`, to, toServiceName)
|
|
|
|
|
|
|
|
// dependencyDot := fmt.Sprintf(`
|
|
|
|
// "%s" -> "%s" [ ltail="cluster_%s", lhead="cluster_%s" ];`,
|
|
|
|
// strings.ReplaceAll(strings.ReplaceAll(fromService, ".", "_"), "-", "_"),
|
|
|
|
// strings.ReplaceAll(strings.ReplaceAll(toService, ".", "_"), "-", "_"),
|
|
|
|
// strings.ReplaceAll(strings.ReplaceAll(from, ".", "_"), "-", "_"),
|
|
|
|
// strings.ReplaceAll(strings.ReplaceAll(to, ".", "_"), "-", "_"),
|
|
|
|
// )
|
|
|
|
|
|
|
|
// dependencyDots = append(dependencyDots, dependencyDot)
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// dot := fmt.Sprintf(`
|
|
|
|
// digraph {
|
|
|
|
// compound = "true"
|
|
|
|
// newrank = "true"
|
|
|
|
// ranksep = 0.1;
|
|
|
|
|
|
|
|
// %s
|
|
|
|
// %s
|
|
|
|
// }`,
|
|
|
|
// strings.Join(moduleDots, ""),
|
|
|
|
// strings.Join(dependencyDots, ""),
|
|
|
|
// )
|
|
|
|
|
|
|
|
// exitCode, dotStdout, dotStderr, err := shellExecInputPipe(".", &dot, "dot", "-Tsvg")
|
|
|
|
// err = errorFromShellExecResult("dot -Tsvg", exitCode, dotStdout, dotStderr, err)
|
|
|
|
// if err != nil {
|
|
|
|
// return []byte{}, err
|
|
|
|
// }
|
|
|
|
|
|
|
|
// svgBytes, err := modifyDotSVGAsXML(dotStdout, func(svgDoc *XMLNode) error {
|
|
|
|
|
|
|
|
// // correctly set the dot property for services
|
|
|
|
// svgDoc.WithQuerySelector(
|
|
|
|
// // 'g.node > title'
|
|
|
|
// []XMLQuery{XMLQuery{NodeType: "g", Class: "node"}, XMLQuery{NodeType: "title", DirectChild: true}},
|
|
|
|
// func(node *XMLNode) {
|
|
|
|
// if node.Parent != nil {
|
|
|
|
// title := html.UnescapeString(string(node.Content))
|
|
|
|
// node.Parent.SetAttr("data-dot", title)
|
|
|
|
// if strings.HasPrefix(title, "service_") {
|
|
|
|
// node.Parent.SetAttr("class", "resource")
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// },
|
|
|
|
// )
|
|
|
|
|
|
|
|
// // correctly set the dot property for the "cluster"s (aka modules)
|
|
|
|
// moduleNames := []string{}
|
|
|
|
// svgDoc.WithQuerySelector(
|
|
|
|
// // 'a[xlink:title]'
|
|
|
|
// []XMLQuery{XMLQuery{NodeType: "a", Attr: "xlink:title"}},
|
|
|
|
// func(node *XMLNode) {
|
|
|
|
// if node.Parent != nil && node.Parent.Parent != nil {
|
|
|
|
// title := html.UnescapeString(node.GetAttr("xlink:title"))
|
|
|
|
// title = regexp.MustCompile(`[.-]`).ReplaceAllString(title, "_")
|
|
|
|
// if node.Parent.Parent.GetAttr("data-dot") == "" {
|
|
|
|
// moduleNames = append(moduleNames, title)
|
|
|
|
// node.Parent.Parent.SetAttr("data-dot", title)
|
|
|
|
// node.Parent.Parent.SetAttr("class", "module")
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }
|
|
|
|
// },
|
|
|
|
// )
|
|
|
|
|
|
|
|
// // correctly set the dot property for edges
|
|
|
|
// // in order to get dot to render it correctly, the connections are actually between resources,
|
|
|
|
// // not between modules. So we have to trim the resource name off of the dot attribute
|
|
|
|
// // on the connections to make them match what we have in the status JSON object.
|
|
|
|
// svgDoc.WithQuerySelector(
|
|
|
|
// // 'g.edge title'
|
|
|
|
// []XMLQuery{XMLQuery{NodeType: "g", Class: "edge"}, XMLQuery{NodeType: "title"}},
|
|
|
|
// func(node *XMLNode) {
|
|
|
|
// if node.Parent != nil {
|
|
|
|
// title := html.UnescapeString(string(node.Content))
|
|
|
|
// fromTo := strings.Split(title, "->")
|
|
|
|
// if len(fromTo) != 2 {
|
|
|
|
// fmt.Printf("malformed dot string on svg edge: fromTo.length != 2: %s", title)
|
|
|
|
// }
|
|
|
|
// // if this edge starts or ends at a resource inside a module, override the dot string
|
|
|
|
// // to make it start or end at that module instead.
|
|
|
|
// for _, moduleName := range moduleNames {
|
|
|
|
|
|
|
|
// if strings.HasPrefix(strings.TrimPrefix(fromTo[0], "service_"), moduleName) {
|
|
|
|
// fromTo[0] = moduleName
|
|
|
|
// }
|
|
|
|
// if strings.HasPrefix(strings.TrimPrefix(fromTo[1], "service_"), moduleName) {
|
|
|
|
// fromTo[1] = moduleName
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// node.Parent.SetAttr("data-dot", html.EscapeString(fmt.Sprintf("%s->%s", fromTo[0], fromTo[1])))
|
|
|
|
// }
|
|
|
|
// },
|
|
|
|
// )
|
|
|
|
|
|
|
|
// // two last things to do:
|
|
|
|
// // 1. center each resource horizontally within the bounding box of its parent module
|
|
|
|
// // because dot is dumb and it just kinda throws them wherever
|
|
|
|
// // 2. create little create/delete/modify icons next to each changed resource
|
|
|
|
// // similar to how terraform displays -, +, +/-, or ~ on each line-item in the plan
|
|
|
|
|
|
|
|
// for moduleName, config := range configs {
|
|
|
|
// moduleId := regexp.MustCompile(`[.-]`).ReplaceAllString(moduleName, "_")
|
|
|
|
// svgDoc.WithQuerySelector(
|
|
|
|
// []XMLQuery{XMLQuery{Attr: "data-dot", AttrValue: moduleId}},
|
|
|
|
// func(node *XMLNode) {
|
|
|
|
// moduleRect := node.GetBoundingBox()
|
|
|
|
|
|
|
|
// for _, service := range config.Services {
|
|
|
|
// resourceId := fmt.Sprintf("%s_%s", moduleId, regexp.MustCompile(`[.-]`).ReplaceAllString(service.Name, "_"))
|
|
|
|
// svgDoc.WithQuerySelector(
|
|
|
|
// // '[data-dot=module_dns_gandi_gandi_livedns_record_dns_entries]' for example
|
|
|
|
// []XMLQuery{XMLQuery{Attr: "data-dot", AttrValue: resourceId}},
|
|
|
|
// func(node *XMLNode) {
|
|
|
|
// // TODO pass the plan here
|
|
|
|
// centerResourceAndAddPlannedAction(moduleRect, node, resourceId, "create")
|
|
|
|
// },
|
|
|
|
// )
|
|
|
|
// }
|
|
|
|
// },
|
|
|
|
// )
|
|
|
|
// }
|
|
|
|
|
|
|
|
// return nil
|
|
|
|
// })
|
|
|
|
|
|
|
|
// return svgBytes, err
|
|
|
|
// }
|