🌱🏠 a cloud service to enable your own server (owned by you and running on your computer) to be accessible on the internet in seconds, no credit card required https://greenhouse.server.garden/
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.
 
 
 
 

239 lines
6.1 KiB

package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"time"
"git.sequentialread.com/forest/greenhouse/pki"
errors "git.sequentialread.com/forest/pkg-errors"
)
type Config struct {
FrontendPort int
FrontendDomain string
FrontendTLSCertificate string
FrontendTLSKey string
AdminTenantId int
DigitalOceanAPIKey string
DigitalOceanRegion string
DigitalOceanImage string
DigitalOceanSSHAuthorizedKeys []ConfigSSHKey
GandiAPIKey string
SSHPrivateKeyFile string
BackblazeBucketName string
BackblazeKeyId string
BackblazeSecretKey string
ThresholdPort int
ThresholdManagementPort int
DatabaseConnectionString string
DatabaseType string
DatabaseSchema string
SMTP EmailService
}
type ConfigSSHKey struct {
Name string
PublicKey string
}
const isProduction = false
func main() {
_, err := time.LoadLocation("UTC")
if err != nil {
panic(errors.Wrap(err, "can't start the app because can't load UTC location"))
}
workingDirectory := determineWorkingDirectoryByLocatingConfigFile()
config := getConfig(workingDirectory)
model := initDatabase(config)
emailService := &config.SMTP
err = emailService.Initialize()
if err != nil {
panic(err)
}
easypkiInstance := NewGreenhouseEasyPKI(model)
pkiService := pki.NewPKIService(easypkiInstance)
backendApp := initBackend(workingDirectory, config, pkiService, model, emailService)
frontendApp := initFrontend(workingDirectory, config, model, backendApp, emailService)
AddAPIRoutesToFrontend(&frontendApp)
go (func(backendApp *BackendApp) {
defer (func() {
if r := recover(); r != nil {
fmt.Printf("backendApp: panic: %+v\n", r)
debug.PrintStack()
}
})()
for {
err := backendApp.ConsumeMetrics()
if err != nil {
log.Printf("metric collection failed: %+v\n", err)
}
time.Sleep(time.Second * 6)
}
})(backendApp)
// TODO disable this for prod
if !isProduction {
go (func() {
for {
time.Sleep(time.Second)
frontendApp.reloadTemplates()
}
})()
// go (func() {
// var appUrl = fmt.Sprintf("http://localhost:%d", config.FrontendPort)
// var serverIsRunning = false
// var attempts = 0
// for !serverIsRunning && attempts < 15 {
// attempts++
// time.Sleep(time.Millisecond * 500)
// client := &http.Client{
// Timeout: time.Millisecond * 500,
// }
// response, err := client.Get(appUrl)
// if err == nil && response.StatusCode == 200 {
// serverIsRunning = true
// }
// }
// openUrl(appUrl)
// })()
}
err = frontendApp.ListenAndServe()
panic(err)
}
func getConfig(workingDirectory string) *Config {
configBytes, err := ioutil.ReadFile(filepath.Join(workingDirectory, "config.json"))
if err != nil {
log.Fatalf("getConfig(): can't ioutil.ReadFile(\"config.json\") because %+v \n", err)
}
var config Config
err = json.Unmarshal(configBytes, &config)
if err != nil {
log.Fatalf("runServer(): can't json.Unmarshal(configBytes, &config) because %+v \n", err)
}
if config.AdminTenantId == 0 {
config.AdminTenantId = 1
}
configToLog, _ := json.MarshalIndent(config, "", " ")
configToLogString := string(configToLog)
configToLogString = regexp.MustCompile(
`("DigitalOceanAPIKey": "[^"]{2})[^"]+([^"]{2}",)`,
).ReplaceAllString(
configToLogString,
"$1******$2",
)
configToLogString = regexp.MustCompile(
`("DatabaseConnectionString": "[^"]+ password=)[^" ]+( [^"]+",)`,
).ReplaceAllString(
configToLogString,
"$1******$2",
)
configToLogString = regexp.MustCompile(
`("Password": ")[^"]+(",)`,
).ReplaceAllString(
configToLogString,
"$1******$2",
)
configToLogString = regexp.MustCompile(
`("GandiAPIKey": ")[^"]+(",)`,
).ReplaceAllString(
configToLogString,
"$1******$2",
)
configToLogString = regexp.MustCompile(
`("BackblazeSecretKey": ")[^"]+(",)`,
).ReplaceAllString(
configToLogString,
"$1******$2",
)
log.Printf("🌱🏠 greenhouse is starting up using config:\n%s\n", configToLogString)
return &config
}
func determineWorkingDirectoryByLocatingConfigFile() string {
workingDirectory, err := os.Getwd()
if err != nil {
log.Fatalf("determineWorkingDirectoryByLocatingConfigFile(): can't os.Getwd(): %+v", err)
}
executableDirectory, err := getCurrentExecDir()
if err != nil {
log.Fatalf("determineWorkingDirectoryByLocatingConfigFile(): can't getCurrentExecDir(): %+v", err)
}
configFileLocation1 := filepath.Join(executableDirectory, "config.json")
configFileLocation2 := filepath.Join(workingDirectory, "config.json")
configFileLocation := configFileLocation1
configFileStat, err := os.Stat(configFileLocation)
workingDirectoryToReturn := executableDirectory
if err != nil || !configFileStat.Mode().IsRegular() {
configFileLocation = configFileLocation2
configFileStat, err = os.Stat(configFileLocation)
workingDirectoryToReturn = workingDirectory
}
if err != nil || !configFileStat.Mode().IsRegular() {
log.Fatalf("determineWorkingDirectoryByLocatingConfigFile(): no config file. checked %s and %s", configFileLocation1, configFileLocation2)
}
return workingDirectoryToReturn
}
func getCurrentExecDir() (dir string, err error) {
path, err := exec.LookPath(os.Args[0])
if err != nil {
fmt.Printf("exec.LookPath(%s) returned %s\n", os.Args[0], err)
return "", err
}
absPath, err := filepath.Abs(path)
if err != nil {
fmt.Printf("filepath.Abs(%s) returned %s\n", path, err)
return "", err
}
dir = filepath.Dir(absPath)
return dir, nil
}
func openUrl(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
fmt.Printf("can't open app in web browser because '%s'\n", err)
}
}