2020-02-15 15:09:23 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-09-19 06:02:18 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
2020-10-21 18:36:28 +00:00
|
|
|
"os"
|
2020-11-04 07:24:31 +00:00
|
|
|
"path/filepath"
|
2020-09-24 03:12:27 +00:00
|
|
|
"strconv"
|
2020-09-22 17:40:54 +00:00
|
|
|
"strings"
|
2020-11-04 07:24:31 +00:00
|
|
|
"time"
|
2020-09-19 06:02:18 +00:00
|
|
|
|
|
|
|
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"
|
2020-02-15 15:09:23 +00:00
|
|
|
)
|
|
|
|
|
2020-02-24 19:20:16 +00:00
|
|
|
type applicationState struct {
|
2020-09-19 06:02:18 +00:00
|
|
|
workingDirectory string
|
|
|
|
storage objectStorage.ObjectStorager
|
2020-02-24 19:20:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var global applicationState
|
|
|
|
|
2020-09-18 21:06:49 +00:00
|
|
|
func main() {
|
2020-09-19 06:02:18 +00:00
|
|
|
config, workingDirectory, err := configuration.LoadConfiguration()
|
|
|
|
if err != nil {
|
2020-09-22 17:40:54 +00:00
|
|
|
panic(fmt.Sprintf("%+v", errors.Wrap(err, "rootsystem can't start because loadConfiguration() returned")))
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
|
|
|
global.workingDirectory = workingDirectory
|
|
|
|
|
|
|
|
storage, err := objectStorage.InitializeObjectStorage(config, true)
|
|
|
|
if err != nil {
|
2020-09-22 17:40:54 +00:00
|
|
|
panic(fmt.Sprintf("%+v", errors.Wrap(err, "rootsystem can't start because failed to initialize object storage")))
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
|
|
|
global.storage = storage
|
|
|
|
|
|
|
|
go terraformStateServer()
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
results := automation.DoInParallel(
|
|
|
|
func() automation.TaskResult {
|
2020-09-28 06:06:09 +00:00
|
|
|
// 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 {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "knownHostsCredentials",
|
|
|
|
Err: errors.Wrap(err, "can't create object storage access key for known_hosts"),
|
|
|
|
}
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "knownHostsCredentials",
|
|
|
|
Result: knownHostsCredentials,
|
|
|
|
}
|
|
|
|
},
|
2020-09-19 06:02:18 +00:00
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
func() automation.TaskResult {
|
2020-09-28 06:06:09 +00:00
|
|
|
// 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 {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "buildTLSCertsForThreshold",
|
|
|
|
Err: errors.Wrap(err, "can't create certs for threshold"),
|
|
|
|
}
|
|
|
|
}
|
2020-11-01 21:08:29 +00:00
|
|
|
return automation.TaskResult{Name: "buildTLSCertsForThreshold"}
|
2020-09-28 06:06:09 +00:00
|
|
|
},
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
func() automation.TaskResult {
|
2020-09-28 06:06:09 +00:00
|
|
|
sshPort := 2201
|
|
|
|
hostSSHPortFilename := fmt.Sprintf("rootsystem/ssh/%s.txt", config.Host.Name)
|
|
|
|
file, notFound, err := global.storage.Get(hostSSHPortFilename)
|
|
|
|
if err != nil && !notFound {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "sshPort",
|
|
|
|
Err: errors.Wrapf(err, "can't download %s", hostSSHPortFilename),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !notFound {
|
|
|
|
sshPort, err = strconv.Atoi(string(file.Content))
|
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "sshPort",
|
|
|
|
Err: errors.Wrapf(err, "can't read %s as a number", hostSSHPortFilename),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
file, notFound, err := global.storage.Get("rootsystem/ssh/next-port.txt")
|
|
|
|
if err != nil && !notFound {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "sshPort",
|
|
|
|
Err: errors.Wrap(err, "can't download next-port.txt"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !notFound {
|
|
|
|
sshPort, err = strconv.Atoi(string(file.Content))
|
|
|
|
if err != nil {
|
|
|
|
sshPort = 2201
|
|
|
|
log.Printf("warning: next-port.txt did not contain a number. defaulting to %d. contents: %s\n", sshPort, string(file.Content))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
err = global.storage.Put(hostSSHPortFilename, []byte(strconv.Itoa(sshPort)))
|
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "sshPort",
|
|
|
|
Err: errors.Wrapf(err, "can't can't upload %s", hostSSHPortFilename),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
err = global.storage.Put("rootsystem/ssh/next-port.txt", []byte(strconv.Itoa(sshPort+1)))
|
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "sshPort",
|
|
|
|
Err: errors.Wrap(err, "can't can't upload next-port.txt"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "sshPort",
|
|
|
|
Result: sshPort,
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
func() automation.TaskResult {
|
2020-09-28 06:06:09 +00:00
|
|
|
// Add 1 to the build number, each time rootsystem runs is a different build.
|
2020-10-31 22:46:49 +00:00
|
|
|
file, notFound, err := global.storage.Get("rootsystem/automation/build-number.txt")
|
2020-09-28 06:06:09 +00:00
|
|
|
if err != nil && !notFound {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "buildNumber",
|
|
|
|
Err: errors.Wrap(err, "can't download build-number.txt"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-10-31 22:46:49 +00:00
|
|
|
err = global.storage.Put("rootsystem/automation/build-number.txt", []byte(strconv.Itoa(buildNumber)))
|
2020-09-28 06:06:09 +00:00
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "buildNumber",
|
|
|
|
Err: errors.Wrap(err, "can't can't upload build-number.txt"),
|
|
|
|
}
|
|
|
|
}
|
2020-10-21 18:36:28 +00:00
|
|
|
return automation.TaskResult{
|
2020-09-28 06:06:09 +00:00
|
|
|
Name: "buildNumber",
|
|
|
|
Result: buildNumber,
|
|
|
|
}
|
|
|
|
},
|
2020-09-19 06:02:18 +00:00
|
|
|
)
|
|
|
|
|
2020-09-28 06:06:09 +00:00
|
|
|
for _, result := range results {
|
|
|
|
if result.Err != nil {
|
|
|
|
panic(fmt.Sprintf("can't start rootsystem because %s: %+v", result.Name, result.Err))
|
2020-09-24 03:12:27 +00:00
|
|
|
}
|
|
|
|
}
|
2020-09-28 06:06:09 +00:00
|
|
|
|
|
|
|
//sshPort := results["sshPort"].Result.(int)
|
|
|
|
buildNumber := results["buildNumber"].Result.(int)
|
|
|
|
knownHostsCredentials := results["knownHostsCredentials"].Result.([]configuration.Credential)
|
2020-09-24 03:12:27 +00:00
|
|
|
|
2020-09-19 06:02:18 +00:00
|
|
|
// 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.
|
2020-09-22 17:40:54 +00:00
|
|
|
outputVariables, success, err := terraformBuild(
|
2020-09-19 06:02:18 +00:00
|
|
|
config,
|
|
|
|
automation.TerraformConfiguration{
|
2020-09-24 03:12:27 +00:00
|
|
|
BuildNumber: buildNumber,
|
2020-09-19 06:02:18 +00:00
|
|
|
TargetedModules: config.Terraform.GlobalModules,
|
|
|
|
TerraformProject: configuration.GLOBAL_TERRAFORM_PROJECT,
|
|
|
|
HostKeysObjectStorageCredentials: knownHostsCredentials,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
2020-09-22 20:59:08 +00:00
|
|
|
log.Printf("rootsystem %s build errored out (exception): %+v", configuration.GLOBAL_TERRAFORM_PROJECT, err)
|
2020-09-22 17:40:54 +00:00
|
|
|
// 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 {
|
2020-09-22 20:59:08 +00:00
|
|
|
log.Printf("rootsystem %s build failed", configuration.GLOBAL_TERRAFORM_PROJECT)
|
2020-09-22 17:40:54 +00:00
|
|
|
} 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{
|
2020-09-24 03:12:27 +00:00
|
|
|
BuildNumber: buildNumber,
|
2020-09-22 17:40:54 +00:00
|
|
|
TargetedModules: config.Terraform.LocalModules,
|
|
|
|
TerraformProject: projectName,
|
|
|
|
RemoteState: configuration.GLOBAL_TERRAFORM_PROJECT,
|
|
|
|
RemoteStateVariables: outputVariables,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
2020-09-22 20:59:08 +00:00
|
|
|
log.Printf("rootsystem %s build errored out (exception): %+v", projectName, err)
|
2020-09-22 17:40:54 +00:00
|
|
|
//panic(fmt.Sprintf("%+v", err))
|
|
|
|
} else if !success {
|
2020-09-22 20:59:08 +00:00
|
|
|
log.Printf("rootsystem %s build failed", projectName)
|
2020-10-21 18:36:28 +00:00
|
|
|
} else {
|
2020-11-04 07:24:31 +00:00
|
|
|
|
|
|
|
os.MkdirAll(filepath.Dir(configuration.THRESHOLD_SOCKET), 0o700)
|
|
|
|
os.MkdirAll(filepath.Dir(configuration.CADDY_SOCKET), 0o700)
|
|
|
|
os.MkdirAll(configuration.CADDY_DATA, 0o700)
|
2020-10-21 18:36:28 +00:00
|
|
|
|
|
|
|
svg, statusChannel, err := automation.DockerComposeUp(config, workingDirectory)
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Sprintf("%+v", errors.Wrap(err, "rootsystem can't start because DockerComposeUp() returned")))
|
|
|
|
}
|
|
|
|
// err = ioutil.WriteFile("docker.svg", svg, 0o777)
|
|
|
|
// if err != nil {
|
|
|
|
// panic(fmt.Sprintf("%+v", errors.Wrap(err, "rootsystem can't start because WriteFile(\"docker.svg\") returned")))
|
|
|
|
// }
|
|
|
|
|
|
|
|
fmt.Println("DockerComposeUp kicked off")
|
|
|
|
|
|
|
|
diagramPath := fmt.Sprintf(
|
2020-10-31 22:46:49 +00:00
|
|
|
"rootsystem/automation/%04d/docker-compose-%s/diagram.svg", buildNumber, config.Host.Name,
|
2020-10-21 18:36:28 +00:00
|
|
|
)
|
|
|
|
statusPath := fmt.Sprintf(
|
2020-10-31 22:46:49 +00:00
|
|
|
"rootsystem/automation/%04d/docker-compose-%s/status.json", buildNumber, config.Host.Name,
|
2020-10-21 18:36:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
success, err := streamUpdatesToObjectStorage(diagramPath, svg, statusPath, statusChannel)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("rootsystem docker-compose errored out (exception): %+v", err)
|
|
|
|
} else if !success {
|
|
|
|
log.Printf("rootsystem docker-compose failed")
|
2020-11-04 07:24:31 +00:00
|
|
|
} else {
|
|
|
|
time.Sleep(5 * time.Second)
|
2020-11-05 02:13:37 +00:00
|
|
|
|
|
|
|
for {
|
|
|
|
err = automation.IngressConfig(config)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("rootsystem IngressConfig failed: %+v", err)
|
|
|
|
} else {
|
|
|
|
log.Printf("rootsystem IngressConfig success")
|
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(30 * time.Second)
|
2020-11-04 07:24:31 +00:00
|
|
|
}
|
2020-11-05 02:13:37 +00:00
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
}
|
2020-09-22 17:40:54 +00:00
|
|
|
}
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
|
|
|
|
2020-09-22 17:40:54 +00:00
|
|
|
// sit and do nothing forever.
|
2020-09-19 06:02:18 +00:00
|
|
|
a := make(chan bool)
|
|
|
|
<-a
|
2020-09-18 21:06:49 +00:00
|
|
|
}
|
2020-08-14 21:36:50 +00:00
|
|
|
|
|
|
|
func terraformBuild(
|
2020-09-19 06:02:18 +00:00
|
|
|
config *configuration.Configuration,
|
|
|
|
terraformConfig automation.TerraformConfiguration,
|
2020-09-22 17:40:54 +00:00
|
|
|
) ([]string, bool, error) {
|
2020-08-14 21:36:50 +00:00
|
|
|
|
2020-09-19 06:02:18 +00:00
|
|
|
outputVariables, err := automation.WriteTerraformCodeForTargetedModules(
|
|
|
|
config,
|
|
|
|
global.workingDirectory,
|
|
|
|
terraformConfig,
|
|
|
|
)
|
|
|
|
if err != nil {
|
2020-09-22 17:40:54 +00:00
|
|
|
return []string{}, false, err
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("WriteTerraformCodeForTargetedModules done")
|
|
|
|
|
|
|
|
svg, statusChannel, err := automation.TerraformPlanAndApply(config, global.workingDirectory, terraformConfig.TerraformProject)
|
|
|
|
if err != nil {
|
2020-09-22 17:40:54 +00:00
|
|
|
return []string{}, false, err
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
|
|
|
|
2020-09-22 17:40:54 +00:00
|
|
|
fmt.Println("TerraformPlanAndApply kicked off")
|
2020-09-19 06:02:18 +00:00
|
|
|
|
2020-09-24 03:12:27 +00:00
|
|
|
diagramPath := fmt.Sprintf(
|
2020-10-31 22:46:49 +00:00
|
|
|
"rootsystem/automation/%04d/%s/diagram.svg",
|
|
|
|
terraformConfig.BuildNumber, terraformConfig.TerraformProject,
|
2020-09-24 03:12:27 +00:00
|
|
|
)
|
2020-10-21 18:36:28 +00:00
|
|
|
statusPath := fmt.Sprintf(
|
2020-10-31 22:46:49 +00:00
|
|
|
"rootsystem/automation/%04d/%s/status.json",
|
|
|
|
terraformConfig.BuildNumber, terraformConfig.TerraformProject,
|
2020-10-21 18:36:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
success, err := streamUpdatesToObjectStorage(diagramPath, svg, statusPath, statusChannel)
|
2020-09-24 03:12:27 +00:00
|
|
|
|
2020-09-19 06:02:18 +00:00
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return outputVariables, false, err
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
return outputVariables, success, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func streamUpdatesToObjectStorage(
|
|
|
|
diagramPath string,
|
|
|
|
svg []byte,
|
|
|
|
statusPath string,
|
|
|
|
statusChannel chan automation.TerraformApplyResult,
|
|
|
|
) (bool, error) {
|
|
|
|
|
|
|
|
err := global.storage.Put(diagramPath, svg)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
2020-09-19 06:02:18 +00:00
|
|
|
|
2020-09-22 17:40:54 +00:00
|
|
|
lastLog := ""
|
|
|
|
|
2020-09-19 06:02:18 +00:00
|
|
|
for status := range statusChannel {
|
2020-09-23 05:30:04 +00:00
|
|
|
statusJson, err := json.MarshalIndent(status, "", " ")
|
2020-09-19 06:02:18 +00:00
|
|
|
if err != nil {
|
2020-10-21 18:36:28 +00:00
|
|
|
return false, err
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
2020-09-22 17:40:54 +00:00
|
|
|
|
2020-09-22 18:32:03 +00:00
|
|
|
newLog := strings.TrimPrefix(status.Log, lastLog)
|
2020-09-22 17:40:54 +00:00
|
|
|
lastLog = status.Log
|
|
|
|
log.Println(newLog)
|
2020-09-23 05:30:04 +00:00
|
|
|
//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))
|
2020-10-21 18:36:28 +00:00
|
|
|
|
2020-09-24 03:12:27 +00:00
|
|
|
err = global.storage.Put(statusPath, statusJson)
|
2020-09-19 06:02:18 +00:00
|
|
|
if err != nil {
|
2020-09-22 17:40:54 +00:00
|
|
|
log.Printf("can't upload terraform status update to object storage: %+v", err)
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
|
|
|
if status.Complete {
|
2020-10-21 18:36:28 +00:00
|
|
|
return status.Success, status.Error
|
2020-09-19 06:02:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-21 18:36:28 +00:00
|
|
|
return false, errors.New("streamUpdatesToObjectStorage: statusChannel closed before status was Complete")
|
2020-03-05 15:47:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func terraformStateServer() error {
|
2020-04-01 22:12:08 +00:00
|
|
|
|
2020-09-19 06:02:18 +00:00
|
|
|
// 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{},
|
|
|
|
}
|
2020-02-27 18:55:52 +00:00
|
|
|
|
2020-09-19 06:02:18 +00:00
|
|
|
return server.ListenAndServe()
|
2020-02-27 18:55:52 +00:00
|
|
|
}
|