package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
"git.sequentialread.com/forest/greenhouse/pki"
errors "git.sequentialread.com/forest/pkg-errors"
"golang.org/x/sys/unix"
)
type Config struct {
FrontendPort int
FrontendDomain string
FrontendTLSCertificate string
FrontendTLSKey string
HomepageMarkdownURL string
LokiURL string
EnableRegistration bool
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 {
go postTelemetry ( "greenhouse-web" , "" , "admin" , "" , fmt . Sprintf ( "load UTC location failed: %s" , err ) )
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 {
go postTelemetry ( "greenhouse-web" , "" , "admin" , strconv . Itoa ( config . AdminTenantId ) , fmt . Sprintf ( "emailService.Initialize() failed: %s" , err ) )
panic ( err )
}
easypkiInstance := NewGreenhouseEasyPKI ( model )
pkiService := pki . NewPKIService ( easypkiInstance )
backendApp := initBackend ( workingDirectory , config , pkiService , model , emailService )
ingressService := NewIngressService ( config , backendApp , model )
frontendApp := initFrontend ( workingDirectory , config , model , backendApp , emailService , ingressService )
scheduledTasks := NewScheduledTasks ( ingressService , backendApp , model , config . AdminTenantId )
err = scheduledTasks . Initialize ( )
if err != nil {
// TODO should this be Fatalf??
go postTelemetry ( "greenhouse-web" , "" , "admin" , strconv . Itoa ( config . AdminTenantId ) , fmt . Sprintf ( "initialization process failed: %s" , err ) )
log . Printf ( "Greenhouse's initialization process failed: \n%+v\n\n" , err )
}
go ( func ( backendApp * BackendApp ) {
defer ( func ( ) {
if r := recover ( ) ; r != nil {
go postTelemetry ( "greenhouse-web" , "" , "admin" , strconv . Itoa ( config . AdminTenantId ) , fmt . Sprintf ( "metric collection panic: %s" , r ) )
fmt . Printf ( "backendApp: panic: %+v\n" , r )
debug . PrintStack ( )
}
} ) ( )
for {
err := backendApp . ConsumeMetrics ( )
if err != nil {
firstLineOfError := strings . Split ( fmt . Sprintf ( "metric collection failed: %s" , err ) , "\n" ) [ 0 ]
go postTelemetry ( "greenhouse-web" , "" , "admin" , strconv . Itoa ( config . AdminTenantId ) , firstLineOfError )
log . Printf ( "metric collection failed: %+v\n" , err )
}
time . Sleep ( time . Second * 6 )
}
} ) ( backendApp )
AddAPIRoutesToFrontend ( & frontendApp )
// 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)
// })()
}
go postTelemetry ( "greenhouse-web" , "" , "admin" , strconv . Itoa ( config . AdminTenantId ) , "greenhouse started up!" )
go func ( ) {
err = frontendApp . ListenAndServe ( )
go postTelemetry ( "greenhouse-web" , "" , "admin" , strconv . Itoa ( config . AdminTenantId ) , fmt . Sprintf ( "frontendApp.ListenAndServe(): %s" , err ) )
panic ( err )
} ( )
sigs := make ( chan os . Signal , 10 )
signal . Notify ( sigs , unix . SIGTERM , unix . SIGINT , unix . SIGHUP )
done := make ( chan bool , 1 )
go func ( ) {
sig := <- sigs
log . Printf ( "Greenhouse recieved signal: %s\n" , sig )
go postTelemetry ( "greenhouse-web" , "" , "admin" , strconv . Itoa ( config . AdminTenantId ) , fmt . Sprintf ( "greenhouse recieved signal %s" , sig . String ( ) ) )
ingressService . StopGreenhouseDaemon ( )
attempts := 0
for attempts < 20 {
attempts ++
time . Sleep ( 500 * time . Millisecond )
log . Println ( "Greenhouse is waiting greenhouse-daemon to stop..." )
if ! ingressService . GetGreenhouseDaemonProcessRunning ( ) {
log . Printf ( "greenhouse-daemon has stopped\n" )
done <- true
return
}
}
postTelemetry ( "greenhouse-web" , "" , "admin" , strconv . Itoa ( config . AdminTenantId ) , "greenhouse timed out waiting for greenhouse-daemon to stop! " )
done <- true
} ( )
<- done
fmt . Println ( "exiting" )
}
func getConfig ( workingDirectory string ) * Config {
configBytes , err := ioutil . ReadFile ( filepath . Join ( workingDirectory , "config.json" ) )
if err != nil {
configBytes , err = ioutil . ReadFile ( filepath . Join ( workingDirectory , "config/config.json" ) )
if err != nil {
log . Fatalf ( "getConfig(): can't ioutil.ReadFile(\"config.json\" or \"config/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
}
lokiURL := os . Getenv ( "GREENHOUSE_LOKI_URL" )
homepageMarkdownURL := os . Getenv ( "GREENHOUSE_HOMEPAGE_MARKDOWN_URL" )
enableRegistrationString := os . Getenv ( "GREENHOUSE_ENABLE_REGISTRATION" )
digitalOceanAPIKey := os . Getenv ( "GREENHOUSE_DIGITALOCEAN_API_KEY" )
databaseConnectionString := os . Getenv ( "GREENHOUSE_DATABASE_CONNECTION_STRING" )
databaseSchema := os . Getenv ( "GREENHOUSE_DATABASE_SCHEMA" )
gandiAPIKey := os . Getenv ( "GREENHOUSE_GANDI_API_KEY" )
sshPrivateKeyFile := os . Getenv ( "GREENHOUSE_SSH_PRIVATE_KEY_FILE" )
backblazeBucketName := os . Getenv ( "GREENHOUSE_BACKBLAZE_BUCKET_NAME" )
backblazeKeyId := os . Getenv ( "GREENHOUSE_BACKBLAZE_KEY_ID" )
backblazeSecretKey := os . Getenv ( "GREENHOUSE_BACKBLAZE_SECRET_KEY" )
smtpHost := os . Getenv ( "GREENHOUSE_SMTP_HOST" )
smtpPortString := os . Getenv ( "GREENHOUSE_SMTP_PORT" )
smtpUsername := os . Getenv ( "GREENHOUSE_SMTP_USERNAME" )
smtpPassword := os . Getenv ( "GREENHOUSE_SMTP_PASSWORD" )
smtpEncryption := os . Getenv ( "GREENHOUSE_SMTP_ENCRYPTION" )
if lokiURL != "" {
config . LokiURL = lokiURL
}
if homepageMarkdownURL != "" {
config . HomepageMarkdownURL = homepageMarkdownURL
}
if enableRegistrationString != "" {
v , err := strconv . ParseBool ( enableRegistrationString )
if err != nil {
log . Fatalf ( "getConfig(): can't convert enableRegistrationString '%s' to bool: %+v \n" , enableRegistrationString , err )
}
config . EnableRegistration = v
}
if digitalOceanAPIKey != "" {
config . DigitalOceanAPIKey = digitalOceanAPIKey
}
if databaseConnectionString != "" {
config . DatabaseConnectionString = databaseConnectionString
}
if databaseSchema != "" {
config . DatabaseSchema = databaseSchema
}
if gandiAPIKey != "" {
config . GandiAPIKey = gandiAPIKey
}
if sshPrivateKeyFile != "" {
config . SSHPrivateKeyFile = sshPrivateKeyFile
}
if backblazeBucketName != "" {
config . BackblazeBucketName = backblazeBucketName
}
if backblazeKeyId != "" {
config . BackblazeKeyId = backblazeKeyId
}
if backblazeSecretKey != "" {
config . BackblazeSecretKey = backblazeSecretKey
}
if smtpHost != "" {
config . SMTP . Host = smtpHost
}
if smtpPortString != "" {
smtpPort , err := strconv . Atoi ( smtpPortString )
if err != nil {
log . Fatalf ( "getConfig(): can't convert smtpPortString '%s' to int: %+v \n" , smtpPortString , err )
}
config . SMTP . Port = smtpPort
}
if smtpUsername != "" {
config . SMTP . Username = smtpUsername
}
if smtpPassword != "" {
config . SMTP . Password = smtpPassword
}
if smtpEncryption != "" {
config . SMTP . Encryption = smtpEncryption
}
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 )
}
}