Browse Source

update to golang 16 and got digitalocean instance spawner working

roughly
master
forest 6 months ago
parent
commit
7c929a69e3
15 changed files with 411 additions and 134 deletions
  1. +3
    -0
      README.md
  2. +1
    -1
      backblaze_b2_service.go
  3. +101
    -12
      backend.go
  4. +97
    -46
      db_model.go
  5. +75
    -21
      digitalocean_service.go
  6. +3
    -3
      email_service.go
  7. +6
    -6
      frontend.go
  8. +1
    -1
      frontend/login.gotemplate.html
  9. +13
    -0
      go.mod
  10. +16
    -0
      go.sum
  11. +31
    -4
      main.go
  12. +2
    -1
      pki_service.go
  13. +6
    -7
      schema_versions/02_up_create_tenants_etc.sql
  14. +9
    -8
      ssh_service.go
  15. +47
    -24
      threshold_provisioning_service.go

+ 3
- 0
README.md View File

@ -56,6 +56,9 @@ Architechture:
![architechture diagram](greenhouse-arch.png)
This diagram was created with https://app.diagrams.net/.
To edit it, download the <a download href="greenhouse-arch.drawio">diagram file</a> and edit it with the https://app.diagrams.net/ web application, or you may run the application from [source](https://github.com/jgraph/drawio) if you wish.
Features:
- Threshold (TCP reverse tunnel) as a service (multi-tenant or dedicated IP address)


+ 1
- 1
backblaze_b2_service.go View File

@ -7,7 +7,7 @@ import (
"strings"
"time"
"github.com/pkg/errors"
errors "git.sequentialread.com/forest/pkg-errors"
)
type BackblazeB2Service struct {


+ 101
- 12
backend.go View File

@ -12,6 +12,8 @@ import (
"math"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
@ -90,7 +92,7 @@ func initBackend(
pkiService *PKIService,
model *DBModel,
emailService *EmailService,
) BackendApp {
) *BackendApp {
toReturn := BackendApp{
WorkingDirectory: workingDirectory,
@ -131,7 +133,7 @@ func initBackend(
}, &expiryTime, nil
}
return toReturn
return &toReturn
}
func (app *BackendApp) Monitor() error {
@ -184,10 +186,10 @@ func (app *BackendApp) Rebalance() (bool, error) {
//TODO
billingYear := 2021
billingMonth := 1
billingMonth := 3
desiredInstancesPerTenant := 2
tenantPinDuration := time.Hour
errors := []string{}
errorStrings := []string{}
tenants, err := app.Model.GetTenants()
if err != nil {
@ -213,25 +215,48 @@ func (app *BackendApp) Rebalance() (bool, error) {
validVpsInstances := map[string]*VPSInstance{}
for k := range digitalOceanVpsInstances {
if _, has := dbVPSInstances[k]; !has {
errors = append(errors, fmt.Sprintf("instance %s is in the provider api, but not in the database", k))
errorStrings = append(errorStrings, fmt.Sprintf("instance %s is in the provider api, but not in the database", k))
}
}
for k, v := range dbVPSInstances {
if _, has := digitalOceanVpsInstances[k]; !has {
errors = append(errors, fmt.Sprintf("instance %s is in the database, but not in the provider api", k))
errorStrings = append(errorStrings, fmt.Sprintf("instance %s is in the database, but not in the provider api", k))
} else if !v.Deleted && !v.Deprecated {
validVpsInstances[k] = v
}
}
if len(errorStrings) > 0 {
return false, errors.Errorf("VPS instances are inconsistent: \n%s\n", strings.Join(errorStrings, "\n"))
}
// aggregate dedicated vps instances count onto tenants
// TODO need to remove these instances from validVpsInstances or something...
// They should not be part of the scaling logic for the multitenant instances
for _, vpsInstance := range validVpsInstances {
keysToRemove := []string{}
for k, vpsInstance := range validVpsInstances {
if vpsInstance.TenantId != 0 {
keysToRemove = append(keysToRemove, k)
tenants[vpsInstance.TenantId].DedicatedVPSCount += 1
}
}
// TODO do we need to do anything else with these besides cutting them from the "validVpsInstances" ?
for _, k := range keysToRemove {
delete(validVpsInstances, k)
}
jsonBytes, err := json.MarshalIndent(tenants, "", " ")
log.Printf("tenants: %s\n\n", string(jsonBytes))
jsonBytes, err = json.MarshalIndent(dbVPSInstances, "", " ")
log.Printf("dbVPSInstances: %s\n\n", string(jsonBytes))
jsonBytes, err = json.MarshalIndent(tenantVpsInstanceRows, "", " ")
log.Printf("tenantVpsInstanceRows: %s\n\n", string(jsonBytes))
jsonBytes, err = json.MarshalIndent(digitalOceanVpsInstances, "", " ")
log.Printf("digitalOceanVpsInstances: %s\n\n", string(jsonBytes))
jsonBytes, err = json.MarshalIndent(validVpsInstances, "", " ")
log.Printf("validVpsInstances: %s\n\n", string(jsonBytes))
// aggregate the bandwidth usage from the tenantVPS relation onto both
// note that the total bandwidth on the tenants may be larger
@ -320,6 +345,29 @@ func (app *BackendApp) Rebalance() (bool, error) {
}
log.Printf("spawning %d instances...", instancesToCreate)
tasks := []func() taskResult{}
for i := 0; i < instancesToCreate; i++ {
tasks = append(tasks, func() taskResult {
instance, err := app.SpawnNewMultitenantInstance()
return taskResult{
Err: err,
Result: instance,
Name: strconv.Itoa(i),
}
})
}
results := doInParallel(false, tasks...)
errors := []string{}
for _, result := range results {
if result.Err != nil {
errors = append(errors, fmt.Sprintf("%+v", result.Err))
}
}
if len(errors) > 0 {
return false, fmt.Errorf("SpawnNewMultitenantInstance failed: \n%s\n", strings.Join(errors, "\n"))
}
return false, nil
}
@ -677,35 +725,73 @@ func (app *BackendApp) Rebalance() (bool, error) {
func (app *BackendApp) SpawnNewMultitenantInstance() (*VPSInstance, error) {
userData := fmt.Sprintf(
"#!/bin/sh\n%s\n\n%s",
`#!/bin/sh
touch /root/.hushlogin
%s
%s`,
app.DigitalOcean.GetSSHHostKeysFileScript(), app.BackblazeB2.GetFileUploadShellScript(),
)
log.Println("Creating new multitenant worker...")
instance, err := app.DigitalOcean.Create("multitenant-worker", userData)
if err != nil {
return nil, errors.Wrap(err, "failed to DigitalOcean.Create")
}
log.Printf("Waiting for %s to get an IPv4...\n", instance.GetId())
waitedSeconds := 0
for instance.IPV4 == "" && waitedSeconds < 300 {
time.Sleep(time.Second * time.Duration(5))
waitedSeconds += 5
instance, err = app.DigitalOcean.Get(instance.ProviderInstanceId)
if err != nil {
return nil, errors.Wrap(err, "failed to DigitalOcean.Get")
}
}
log.Printf("Waiting for %s (%s) to upload its ssh host public keys to backblaze...\n", instance.GetId(), instance.IPV4)
knownHostsFileContent, err := app.pollForSSHHostKeys(instance.GetId())
if err != nil {
return nil, errors.Wrap(err, "failed to PollBackblazeForSSHHostKeys")
}
log.Printf("adding %s (%s) to the user's known-hosts file...\n", instance.GetId(), instance.IPV4)
err = app.SSH.AppendToKnownHostsFile(knownHostsFileContent)
if err != nil {
return nil, errors.Wrap(err, "failed to AppendToSSHKnownHostsFile")
}
log.Printf("Generating threshold install script for %s (%s)...\n", instance.GetId(), instance.IPV4)
provisionThresholdScript, err := app.ThresholdProvisioning.GetMultitenantInstallScript(instance.IPV4)
if err != nil {
return nil, errors.Wrap(err, "failed to ThresholdProvisioning.GetMultitenantInstallScript")
}
// TODO use potentially different username depending on cloud provider
err = app.SSH.RunScriptOnRemoteHost(provisionThresholdScript, "root", instance.IPV4)
log.Printf("Installing threshold on %s (%s)...\n", instance.GetId(), instance.IPV4)
stdout, stderr, err := app.SSH.RunScriptOnRemoteHost(provisionThresholdScript, "root", instance.IPV4)
if err != nil {
return nil, errors.Wrapf(
err, "failed to RunScriptOnRemoteHost(provisionThresholdScript, \"root\", %s)",
instance.IPV4,
)
}
log.Println("stderr:")
log.Println(stderr)
log.Println("")
log.Println("")
log.Println("stdout:")
log.Println(stdout)
log.Println("")
log.Println("")
log.Printf("Saving %s (%s) info to the database...\n", instance.GetId(), instance.IPV4)
err = app.Model.CreateVPSInstance(instance)
if err != nil {
return nil, errors.Wrap(err, "failed to Model.CreateVPSInstance")
}
log.Printf("DONE! %s (%s) is provisioned!\n\n\n", instance.GetId(), instance.IPV4)
return instance, nil
}
@ -815,6 +901,9 @@ func (service *BaseHTTPService) MyHTTP(
}
request, err := http.NewRequest(method, url, body)
if err != nil {
return 0, nil, errors.Wrapf(err, "failed to create HTTP request calling %s %s", method, url)
}
if withRequest != nil {
withRequest(request)
}
@ -839,7 +928,7 @@ func (service *BaseHTTPService) MyHTTP200(
statusCode, bytes, err := service.MyHTTP(method, url, body, withRequest)
if err == nil && statusCode >= 300 {
err = fmt.Errorf("HTTP %d when calling %s %s ", statusCode, method, url)
err = errors.Errorf("HTTP %d when calling %s %s: \n%s\n\n", statusCode, method, url, string(bytes))
}
return bytes, err


+ 97
- 46
db_model.go View File

@ -12,8 +12,8 @@ import (
"strings"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
_ "github.com/lib/pq"
"github.com/pkg/errors"
)
type RowScanner interface {
@ -78,7 +78,12 @@ func initDatabase(config *Config) *DBModel {
log.Fatal(err)
}
if err := db.Ping(); err != nil {
log.Fatalf("failed to open database connection: %v", err)
log.Fatalf("failed to open database connection: %+v", err)
}
_, err = db.Exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
if err != nil {
log.Fatalf("failed to SET TRANSACTION ISOLATION LEVEL %+v", err)
}
var tableName string
@ -96,17 +101,17 @@ func initDatabase(config *Config) *DBModel {
INSERT INTO schema_version(version) VALUES (1);
`)
if err != nil {
log.Fatalf("failed to create schema_version table: %v", err)
log.Fatalf("failed to create schema_version table: %+v", err)
}
} else if err != nil {
log.Fatalf("failed to check whether or not schema_version table exists: %v", err)
log.Fatalf("failed to check whether or not schema_version table exists: %+v", err)
}
readSchemaVersionFromDatabase := func() int {
var currentSchemaVersion int
err = db.QueryRow("SELECT version FROM schema_version").Scan(&currentSchemaVersion)
if err != nil {
log.Fatalf("failed to select currentSchemaVersion: %v", err)
log.Fatalf("failed to select currentSchemaVersion: %+v", err)
}
return currentSchemaVersion
}
@ -115,7 +120,7 @@ func initDatabase(config *Config) *DBModel {
files, err := ioutil.ReadDir("schema_versions")
if err != nil {
log.Fatalf("failed to list schema_versions: %v", err)
log.Fatalf("failed to list schema_versions: %+v", err)
}
getMigrationScript := func(version int, direction string) (filename string, content string) {
@ -126,9 +131,16 @@ func initDatabase(config *Config) *DBModel {
filename = filepath.Join("schema_versions", file.Name())
contentsBytes, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalf("failed to read file '%s': %v", filename, err)
log.Fatalf("failed to read file '%s': %+v", filename, err)
}
content = string(contentsBytes)
content = fmt.Sprintf(`
BEGIN TRANSACTION;
%s
UPDATE schema_version SET version = %d;
COMMIT TRANSACTION;
`, string(contentsBytes), version)
break
}
}
@ -150,7 +162,7 @@ func initDatabase(config *Config) *DBModel {
filename, content = getMigrationScript(expectedSchemaVersion, "up")
_, err = db.Exec(content)
if err != nil {
log.Fatalf("failed to execute database migration script %s: %v", filename, err)
log.Fatalf("failed to execute database migration script %s: %+v", filename, err)
}
} else {
@ -158,7 +170,7 @@ func initDatabase(config *Config) *DBModel {
filename, content = getMigrationScript(currentSchemaVersion, "down")
_, err = db.Exec(content)
if err != nil {
log.Fatalf("failed to execute database migration script %s: %v", filename, err)
log.Fatalf("failed to execute database migration script %s: %+v", filename, err)
}
}
@ -222,7 +234,7 @@ func (model *DBModel) VerifyEmail(token string, tenantId int) error {
// log.Println(expires)
// log.Println(expires.Unix())
if err != nil && err != sql.ErrNoRows {
log.Printf("VerifyEmail(): query error %v", err)
log.Printf("VerifyEmail(): query error %+v", err)
}
if err != nil || time.Now().After(expires) {
return errors.New("email verification token was invalid or expired")
@ -279,7 +291,7 @@ func (model *DBModel) GetSession(id string, cameFromLaxCookie bool) (*Session, e
return nil, nil
}
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "GetSession(id=%s, cameFromLaxCookie=%t): ", id, cameFromLaxCookie)
}
return &Session{
TenantId: loggedInTenantId,
@ -290,16 +302,16 @@ func (model *DBModel) GetSession(id string, cameFromLaxCookie bool) (*Session, e
}
func (model *DBModel) SetSession(id string, session *Session) error {
_, err := model.DB.Exec("UPDATE tenants SET lax_cookie = $1", session.LaxCookie)
_, err := model.DB.Exec("UPDATE tenants SET lax_cookie = $1 WHERE id = $2", session.LaxCookie, session.TenantId)
if err != nil {
return err
return errors.Wrap(err, "SetSession(): ")
}
_, err = model.DB.Exec("DELETE FROM session_cookies WHERE tenant_id = $1", session.TenantId)
if err != nil {
return err
return errors.Wrap(err, "SetSession(): ")
}
_, err = model.DB.Exec("INSERT INTO session_cookies (id, tenant_id, expires) VALUES ($1, $2, $3)", id, session.TenantId, session.Expires.UTC())
return err
return errors.Wrap(err, "SetSession(): ")
}
func (model *DBModel) LogoutTenant(tenantId int) error {
@ -310,19 +322,19 @@ func (model *DBModel) LogoutTenant(tenantId int) error {
func (model *DBModel) GetVPSInstances() (map[string]*VPSInstance, error) {
rows, err := model.DB.Query(`
SELECT service_provider, provider_instance_id, tenant_id, ipv4, ipv6, bytes_monthly, created, deprecated, deleted,
FROM vps_instances WHERE deleted IS NULL
SELECT service_provider, provider_instance_id, tenant_id, ipv4, ipv6, bytes_monthly, created, deprecated, deleted
FROM vps_instances WHERE deleted = FALSE
`,
)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetVPSInstances(): ")
}
toReturn := map[string]*VPSInstance{}
for rows.Next() {
instance, err := model.rowToVPSInstance(rows)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetVPSInstances(): ")
}
toReturn[instance.GetId()] = instance
}
@ -330,11 +342,40 @@ func (model *DBModel) GetVPSInstances() (map[string]*VPSInstance, error) {
return toReturn, nil
}
func (model *DBModel) CreateVPSInstance(toCreate *VPSInstance) error {
_, err := model.DB.Exec(`
INSERT INTO vps_instances (
service_provider, provider_instance_id, created,
ipv4, ipv6, bytes_monthly
)
VALUES($1, $2, $3,
$4, $5, $6)
`, toCreate.ServiceProvider, toCreate.ProviderInstanceId, toCreate.Created,
toCreate.IPV4, toCreate.IPV6, toCreate.BytesMonthly,
)
if err != nil {
return errors.Wrap(err, "CreateVPSInstance(): ")
}
if toCreate.TenantId != 0 {
_, err := model.DB.Exec(
`UPDATE vps_instances SET tenantId = $1 WHERE service_provider = $2 AND provider_instance_id = $3`,
toCreate.TenantId, toCreate.ServiceProvider, toCreate.ProviderInstanceId,
)
if err != nil {
return errors.Wrap(err, "CreateVPSInstance(): ")
}
}
return nil
}
func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
rows, err := model.DB.Query(`SELECT tenant_id, port_start, port_end FROM reserved_ports`)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenants(): ")
}
reservedPorts := map[int][]PortRange{}
@ -344,7 +385,7 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
var portEnd int
err := rows.Scan(&tenantId, &portStart, &portEnd)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenants(): ")
}
portRange := PortRange{Start: portStart, End: portEnd}
@ -357,7 +398,7 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
rows, err = model.DB.Query(`SELECT tenant_id, domain_name, last_verified FROM external_domains`)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenants(): ")
}
verificationCutoff := time.Now().Add(-(DomainVerificationPollingInterval + time.Minute))
@ -368,7 +409,7 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
var lastVerified time.Time
err := rows.Scan(&tenantId, &domainName, &lastVerified)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenants(): ")
}
if lastVerified.After(verificationCutoff) {
@ -383,7 +424,7 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
rows, err = model.DB.Query(`SELECT id, created, subdomain, service_limit_cents FROM tenants`)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenants(): ")
}
toReturn := map[int]*TenantInfo{}
@ -394,7 +435,7 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
var serviceLimitCents int
err := rows.Scan(&tenantId, &tenantCreated, subdomain, &serviceLimitCents)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenants(): ")
}
thisTenantDomains := authorizedDomains[tenantId]
@ -425,7 +466,7 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
func (model *DBModel) GetTenantVPSInstanceRows(billingYear, billingMonth int) ([]*TenantVPSInstance, error) {
// tenantCondition := ""
// if tenantId > 0 {
// tenantCondition = "AND tenant_vps_pin.tenant_id = $3"
// tenantCondition = "AND tenant_vps_instance.tenant_id = $3"
// }
rows, err := model.DB.Query(`
SELECT
@ -436,13 +477,13 @@ func (model *DBModel) GetTenantVPSInstanceRows(billingYear, billingMonth int) ([
bytes,
active,
deactivated_at
FROM tenant_vps_pin
WHERE billing_year == $1 AND billing_month = $2
FROM tenant_vps_instance
WHERE billing_year = $1 AND billing_month = $2
`, billingYear, billingMonth,
)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenantVPSInstanceRows(): ")
}
toReturn := []*TenantVPSInstance{}
@ -456,12 +497,12 @@ func (model *DBModel) GetTenantVPSInstanceRows(billingYear, billingMonth int) ([
var deactivatedAt *time.Time
err := rows.Scan(&tenantId, &serviceProvider, &serviceProviderInstanceId, &shadowConfigString, &bytes, &active, deactivatedAt)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenantVPSInstanceRows(): ")
}
var shadowConfig TunnelSettings
err = json.Unmarshal([]byte(shadowConfigString), &shadowConfig)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "GetTenantVPSInstanceRows(): ")
}
toReturn = append(toReturn, &TenantVPSInstance{
@ -509,7 +550,7 @@ func (model *DBModel) SaveInstanceConfiguration(
)
VALUES($1, $2, $3, $4, $5,
$6, $7)
ON CONFLICT (primary_key)
ON CONFLICT ON CONSTRAINT pk_tenant_vps_instance
DO
UPDATE SET shadow_config = $6, active = $7;
`,
@ -517,7 +558,7 @@ func (model *DBModel) SaveInstanceConfiguration(
shadowConfig, true, time.Now().UTC(),
)
if err != nil {
return err
return errors.Wrap(err, "SaveInstanceConfiguration(): ")
}
}
@ -536,59 +577,69 @@ func (model *DBModel) SaveInstanceConfiguration(
billingYear, billingMonth, instance.ServiceProvider, instance.ProviderInstanceId,
)
return err
return errors.Wrap(err, "SaveInstanceConfiguration(): ")
}
func (model *DBModel) PutKeyPair(caName, name string, key, cert []byte) error {
_, err := model.DB.Exec(`
INSERT INTO pki_key_pairs (ca_name, name, key_bytes, cert_bytes)
VALUES($1, $2, $3, $4)
ON CONFLICT (primary_key)
ON CONFLICT ON CONSTRAINT pk_pki_key_pairs
DO
UPDATE SET key_bytes = $3, cert_bytes = $4;
`,
caName, name, key, cert,
)
return err
return errors.Wrap(err, "PutKeyPair(): ")
}
func (model *DBModel) GetKeyPair(caName, name string) ([]byte, []byte, error) {
var key, cert []byte
err := model.DB.QueryRow(`
rows, err := model.DB.Query(`
SELECT key_bytes, cert_bytes FROM pki_key_pairs
WHERE ca_name = $1 AND name = $2
`,
caName, name,
).Scan(&key, &cert)
)
if err != nil {
return nil, nil, err
return nil, nil, errors.Wrap(err, "GetKeyPair(): ")
}
for rows.Next() {
err = rows.Scan(&key, &cert)
if err != nil {
return nil, nil, errors.Wrap(err, "GetKeyPair(): ")
}
return key, cert, nil
}
return key, cert, nil
return nil, nil, nil
}
func (model *DBModel) rowToVPSInstance(row RowScanner) (*VPSInstance, error) {
var serviceProvider string
var providerInstanceId string
var tenantId int
var tenantId *int
var ipv4 string
var ipv6 string
var bytesMonthly int64
var created time.Time
var deprecated bool
var deleted bool
err := row.Scan(&serviceProvider, &providerInstanceId, &tenantId, &ipv4, &ipv4, bytesMonthly, &created, &deprecated, &deleted)
err := row.Scan(&serviceProvider, &providerInstanceId, &tenantId, &ipv4, &ipv4, &bytesMonthly, &created, &deprecated, &deleted)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "rowToVPSInstance(): ")
}
tenantIdInt := 0
if tenantId != nil {
tenantIdInt = *tenantId
}
return &VPSInstance{
ServiceProvider: serviceProvider,
ProviderInstanceId: providerInstanceId,
TenantId: tenantId,
TenantId: tenantIdInt,
IPV4: ipv4,
IPV6: ipv6,
BytesMonthly: bytesMonthly,


+ 75
- 21
digitalocean_service.go View File

@ -7,12 +7,13 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
errors "git.sequentialread.com/forest/pkg-errors"
)
type DigitalOceanService struct {
@ -21,6 +22,7 @@ type DigitalOceanService struct {
APIKey string
SSHKeys []ConfigSSHKey
Region string
Image string
}
type VPSInstance struct {
@ -42,16 +44,22 @@ func (i *VPSInstance) GetId() string {
}
type DigitalOceanDroplet struct {
Status string `json:"status"`
ID int `json:"id"`
Networks map[string]DigitalOceanNetwork `json:"networks"`
Created string `json:"created_at"`
SizeSlug string `json:"size_slug"`
Status string `json:"status"`
ID int `json:"id"`
Networks DigitalOceanDropletNetworks `json:"networks"`
Created string `json:"created_at"`
SizeSlug string `json:"size_slug"`
}
type DigitalOceanDropletNetworks struct {
IPv4 []DigitalOceanIPv4Network `json:"v4"`
IPv6 []DigitalOceanIPv6Network `json:"v6"`
}
type DigitalOceanCreateDropletRequest struct {
Name string `json:"name"`
Region string `json:"region"`
Image string `json:"image"`
Size string `json:"size"`
SSHKeys []int `json:"ssh_keys"`
Backups bool `json:"backups"`
@ -60,13 +68,20 @@ type DigitalOceanCreateDropletRequest struct {
Tags []string `json:"tags"`
}
type DigitalOceanNetwork struct {
type DigitalOceanIPv4Network struct {
IPAddress string `json:"ip_address"`
Netmask string `json:"netmask"`
Gateway string `json:"gateway"`
Type string `json:"type"`
}
type DigitalOceanIPv6Network struct {
IPAddress string `json:"ip_address"`
Netmask int `json:"netmask"`
Gateway string `json:"gateway"`
Type string `json:"type"`
}
type DigitalOceanSSHKey struct {
Id int `json:"id,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
@ -83,10 +98,11 @@ func NewDigitalOceanService(config *Config) *DigitalOceanService {
APIKey: config.DigitalOceanAPIKey,
SSHKeys: config.DigitalOceanSSHAuthorizedKeys,
Region: config.DigitalOceanRegion,
Image: config.DigitalOceanImage,
}
toReturn.ClientFactory = func() (*http.Client, *time.Time, error) {
return &http.Client{Timeout: 10 * time.Second}, nil, nil
return &http.Client{Timeout: 300 * time.Second}, nil, nil
}
return toReturn
@ -99,13 +115,22 @@ func (service *DigitalOceanService) List() (map[string]*VPSInstance, error) {
return nil, err
}
var responseObject []DigitalOceanDroplet
var responseObject struct {
Droplets []DigitalOceanDroplet `json:"droplets"`
}
err = json.Unmarshal(responseBytes, &responseObject)
if err != nil {
log.Printf("\n\nresponse from /v2/droplets:\n %s\n\n", string(responseBytes))
return nil, errors.Wrap(err, "JSON parse error when GET-ing /v2/droplets on digitalocean API")
}
if responseObject.Droplets == nil {
log.Printf("\n\nresponse from /v2/droplets:\n %s\n\n", string(responseBytes))
return nil, errors.New("response does not have droplets property")
}
toReturn := map[string]*VPSInstance{}
for _, droplet := range responseObject {
for _, droplet := range responseObject.Droplets {
instance, err := service.DropletToInstance(droplet)
if err != nil {
return nil, errors.Wrap(err, "mapping error when GET-ing /v2/droplets on digitalocean API")
@ -116,6 +141,29 @@ func (service *DigitalOceanService) List() (map[string]*VPSInstance, error) {
return toReturn, nil
}
func (service *DigitalOceanService) Get(id string) (*VPSInstance, error) {
responseBytes, err := service.DigitalOceanHTTP("GET", fmt.Sprintf("/v2/droplets/%s", id), nil)
if err != nil {
return nil, err
}
var responseObject struct {
Droplet DigitalOceanDroplet `json:"droplet"`
}
err = json.Unmarshal(responseBytes, &responseObject)
if err != nil {
log.Printf("\n\nresponse from /v2/droplets/:\n %s\n\n", string(responseBytes))
return nil, errors.Wrap(err, "JSON parse error when GET-ing /v2/droplets on digitalocean API")
}
if responseObject.Droplet.ID == 0 {
log.Printf("\n\nresponse from /v2/droplets:\n %s\n\n", string(responseBytes))
return nil, errors.New("response does not have droplet.id property")
}
return service.DropletToInstance(responseObject.Droplet)
}
func (service *DigitalOceanService) Create(roleName, userData string) (*VPSInstance, error) {
keyIds, err := service.EnsureDigitalOceanSSHKeyIds()
@ -131,11 +179,12 @@ func (service *DigitalOceanService) Create(roleName, userData string) (*VPSInsta
Name: fmt.Sprintf("greenhouse-%s-%s", roleName, randomSuffix),
SSHKeys: keyIds,
Region: service.Region,
Image: service.Image,
Size: DEFAULT_INSTANCE_SIZE,
Backups: false,
IPV6: true,
UserData: userData,
Tags: []string{""},
Tags: []string{"greenhouse-worker"},
})
if err != nil {
@ -152,6 +201,7 @@ func (service *DigitalOceanService) Create(roleName, userData string) (*VPSInsta
}
err = json.Unmarshal(responseBytes, &responseObject)
if err != nil {
log.Printf("\n\nresponse from POST /v2/droplets:\n %s\n\n", string(responseBytes))
return nil, errors.Wrap(err, "JSON parse error when POSTing to /v2/droplets on digitalocean API")
}
@ -226,8 +276,7 @@ func (service *DigitalOceanService) CreateDigitalOceanSSHKey(publicKey ConfigSSH
}
func (service *DigitalOceanService) DigitalOceanHTTP(method string, path string, body io.Reader) ([]byte, error) {
responseBytes, err := service.MyHTTP200(
return service.MyHTTP200(
method,
fmt.Sprintf("%s%s", service.APIUrl, path),
body,
@ -238,19 +287,24 @@ func (service *DigitalOceanService) DigitalOceanHTTP(method string, path string,
}
},
)
return responseBytes, err
}
func (service *DigitalOceanService) DropletToInstance(droplet DigitalOceanDroplet) (*VPSInstance, error) {
ipv4 := ""
ipv6 := ""
ipv4Network, hasIpv4 := droplet.Networks["v4"]
ipv6Network, hasIpv6 := droplet.Networks["v6"]
if hasIpv4 {
ipv4 = ipv4Network.IPAddress
if droplet.Networks.IPv4 != nil {
for _, network := range droplet.Networks.IPv4 {
if network.Type == "public" {
ipv4 = network.IPAddress
}
}
}
if hasIpv6 {
ipv6 = ipv6Network.IPAddress
if droplet.Networks.IPv6 != nil {
for _, network := range droplet.Networks.IPv6 {
if network.Type == "public" {
ipv6 = network.IPAddress
}
}
}
created, err := time.Parse(time.RFC3339, droplet.Created)
if err != nil {
@ -258,7 +312,7 @@ func (service *DigitalOceanService) DropletToInstance(droplet DigitalOceanDrople
}
bytesMonthly := DEFAULT_INSTANCE_MONTHLY_BYTES
if droplet.SizeSlug != DEFAULT_INSTANCE_SIZE {
panic(fmt.Errorf("unknown monthly bandwidth for digital ocean instance %s with size_slug %s", droplet.ID, droplet.SizeSlug))
panic(fmt.Errorf("unknown monthly bandwidth for digital ocean instance %d with size_slug %s", droplet.ID, droplet.SizeSlug))
}
return &VPSInstance{


+ 3
- 3
email_service.go View File

@ -5,7 +5,7 @@ import (
"strings"
"time"
"github.com/pkg/errors"
errors "git.sequentialread.com/forest/pkg-errors"
mail "github.com/xhit/go-simple-mail"
)
@ -58,7 +58,7 @@ func (emailService *EmailService) SendEmail(subject, recipient, body string) err
smtpClient.SendTimeout = 10 * time.Second
smtpConn, err := smtpClient.Connect()
if err != nil {
log.Printf("SendEmail(): could not connect to email server: %v", err)
log.Printf("SendEmail(): could not connect to email server: %+v", err)
return errors.New("could not connect to email server")
}
email := mail.NewMSG()
@ -68,7 +68,7 @@ func (emailService *EmailService) SendEmail(subject, recipient, body string) err
email.SetBody(mail.TextPlain, body)
err = email.Send(smtpConn)
if err != nil {
log.Printf("SendEmail(): connected to email server, but could not send mail: %v", err)
log.Printf("SendEmail(): connected to email server, but could not send mail: %+v", err)
return errors.New("connected to email server, but could not send mail")
}
return nil


+ 6
- 6
frontend.go View File

@ -282,7 +282,7 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, email
app.handleWithSession("/profile", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
if !session.EmailVerified {
// anti-XSS: only set returnTo if it matches a basic url pattern
// anti-XSS: only set path into the flash cookie if it matches a basic url pattern
if app.basicURLPathRegex.MatchString(request.URL.Path) {
msg := fmt.Sprintf("Please verify your email address in order to access %s%s", app.Domain, request.URL.Path)
app.setFlash(responseWriter, session, "info", msg)
@ -386,7 +386,7 @@ func (app *FrontendApp) getSession(request *http.Request, domain string) (Sessio
session, err := app.Model.GetSession(cookie.Value, cookie.Name == "sessionIdLax")
if err != nil {
log.Printf("can't getSession because can't query session from database: %v", err)
log.Printf("can't getSession because can't query session from database: %+v", err)
return toReturn, err
}
if session != nil {
@ -409,13 +409,13 @@ func (app *FrontendApp) getSession(request *http.Request, domain string) (Sessio
} else if cookie.Name == "flash" && cookie.Value != "" {
bytes, err := base64.RawURLEncoding.DecodeString(cookie.Value)
if err != nil {
log.Printf("can't getSession because can't base64 decode flash cookie: %v", err)
log.Printf("can't getSession because can't base64 decode flash cookie: %+v", err)
return toReturn, err
}
flash := map[string]string{}
err = json.Unmarshal(bytes, &flash)
if err != nil {
log.Printf("can't getSession because can't json parse the decoded flash cookie: %v", err)
log.Printf("can't getSession because can't json parse the decoded flash cookie: %+v", err)
return toReturn, err
}
toReturn.Flash = &flash
@ -458,7 +458,7 @@ func (app *FrontendApp) setSession(responseWriter http.ResponseWriter, session *
}
func (app *FrontendApp) unhandledError(responseWriter http.ResponseWriter, err error) {
log.Printf("500 internal server error: %v\n", err)
log.Printf("500 internal server error: %+v\n", err)
responseWriter.Header().Add("Content-Type", "text/plain")
responseWriter.WriteHeader(http.StatusInternalServerError)
responseWriter.Write([]byte("500 internal server error"))
@ -549,7 +549,7 @@ func (app *FrontendApp) setFlash(responseWriter http.ResponseWriter, session Ses
(*session.Flash)[key] += value
bytes, err := json.Marshal((*session.Flash))
if err != nil {
log.Printf("can't setFlash because can't json marshal the flash map: %v", err)
log.Printf("can't setFlash because can't json marshal the flash map: %+v", err)
return
}


+ 1
- 1
frontend/login.gotemplate.html View File

@ -24,7 +24,7 @@
const submitForm = () => {
const passwordElement = form.querySelector("input[name=password]");
const salt = form.querySelector("input[name=passwordSalt]").value;
const timestamp = form.querySelector("input[name=timestamp]").value;
const timestamp = String(form.querySelector("input[name=timestamp]").value);
const hashBits = greenhouse.sjcl.hash.sha256.hash(`${salt}${passwordElement.value}`);
const myHMAC = new greenhouse.sjcl.misc.hmac(hashBits, greenhouse.sjcl.hash.sha256);
const hmacBits = myHMAC.mac(greenhouse.sjcl.codec.utf8String.toBits(timestamp));


+ 13
- 0
go.mod View File

@ -0,0 +1,13 @@
module git.sequentialread.com/forest/greenhouse
go 1.16
require (
git.sequentialread.com/forest/easypki.git v1.1.2 // indirect
git.sequentialread.com/forest/pkg-errors v0.9.2 // indirect
github.com/boltdb/bolt v1.3.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/lib/pq v1.10.0 // indirect
github.com/xhit/go-simple-mail v2.2.2+incompatible // indirect
golang.org/x/sys v0.0.0-20210317091845-390168757d9c // indirect
)

+ 16
- 0
go.sum View File

@ -0,0 +1,16 @@
git.sequentialread.com/forest/easypki.git v1.1.1 h1:rpfZe1+2i82OAtVtnjr+5mvdqwh2qiX2LvKyzrAQJ3U=
git.sequentialread.com/forest/easypki.git v1.1.1/go.mod h1:/KtMMypjp/3w8tlYlbrKTZKJHxvm+fCwSzIHaZ1TBAQ=
git.sequentialread.com/forest/easypki.git v1.1.2 h1:6R60tyBS+lXYXd98jIuHlpy7D5qoRaQYNsvYaaUvsnk=
git.sequentialread.com/forest/easypki.git v1.1.2/go.mod h1:/KtMMypjp/3w8tlYlbrKTZKJHxvm+fCwSzIHaZ1TBAQ=
git.sequentialread.com/forest/pkg-errors v0.9.2 h1:j6pwbL6E+TmE7TD0tqRtGwuoCbCfO6ZR26Nv5nest9g=
git.sequentialread.com/forest/pkg-errors v0.9.2/go.mod h1:8TkJ/f8xLWFIAid20aoqgDZcCj9QQt+FU+rk415XO1w=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/xhit/go-simple-mail v2.2.2+incompatible h1:Hm2VGfLqiQJ/NnC8SYsrPOPyVYIlvP2kmnotP4RIV74=
github.com/xhit/go-simple-mail v2.2.2+incompatible/go.mod h1:I8Ctg6vIJZ+Sv7k/22M6oeu/tbFumDY0uxBuuLbtU7Y=
golang.org/x/sys v0.0.0-20210317091845-390168757d9c h1:WGyvPg8lhdtSkb8BiYWdtPlLSommHOmJHFvzWODI7BQ=
golang.org/x/sys v0.0.0-20210317091845-390168757d9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

+ 31
- 4
main.go View File

@ -11,6 +11,7 @@ import (
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"time"
)
@ -21,6 +22,7 @@ type Config struct {
FrontendTLSKey string
DigitalOceanAPIKey string
DigitalOceanRegion string
DigitalOceanImage string
DigitalOceanSSHAuthorizedKeys []ConfigSSHKey
SSHPrivateKeyFile string
BackblazeBucketName string
@ -54,6 +56,31 @@ func main() {
}
frontendApp := initFrontend(workingDirectory, config, model, emailService)
pkiService := NewPKIService(config, model)
backendApp := initBackend(workingDirectory, config, pkiService, model, emailService)
go (func(backendApp *BackendApp) {
defer (func() {
if r := recover(); r != nil {
fmt.Printf("backendApp: panic: %+v\n", r)
debug.PrintStack()
}
})()
log.Println("Starting backendApp.Rebalance()")
completed, err := backendApp.Rebalance()
if err != nil {
log.Printf("Rebalance failed: %+v\n", err)
} else if !completed {
log.Println("Rebalance not complete yet. Running backendApp.Rebalance() again")
_, err := backendApp.Rebalance()
if err != nil {
log.Printf("Rebalance failed: %+v\n", err)
}
}
})(backendApp)
// TODO disable this for prod
if !isProduction {
@ -90,13 +117,13 @@ func main() {
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)
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)
log.Fatalf("runServer(): can't json.Unmarshal(configBytes, &config) because %+v \n", err)
}
configToLog, _ := json.MarshalIndent(config, "", " ")
@ -134,11 +161,11 @@ func getConfig(workingDirectory string) *Config {
func determineWorkingDirectoryByLocatingConfigFile() string {
workingDirectory, err := os.Getwd()
if err != nil {
log.Fatalf("determineWorkingDirectoryByLocatingConfigFile(): can't os.Getwd(): %v", err)
log.Fatalf("determineWorkingDirectoryByLocatingConfigFile(): can't os.Getwd(): %+v", err)
}
executableDirectory, err := getCurrentExecDir()
if err != nil {
log.Fatalf("determineWorkingDirectoryByLocatingConfigFile(): can't getCurrentExecDir(): %v", err)
log.Fatalf("determineWorkingDirectoryByLocatingConfigFile(): can't getCurrentExecDir(): %+v", err)
}
configFileLocation1 := filepath.Join(executableDirectory, "config.json")


+ 2
- 1
pki_service.go View File

@ -99,6 +99,7 @@ func (service *PKIService) GetCACertificate(caName string) (*x509.Certificate, e
func (service *PKIService) getCA(caName string) (*easypkiCertificate.Bundle, error) {
ca, err := service.EasyPKI.GetCA(caName)
if err == ErrDoesNotExist {
err = service.EasyPKI.Sign(
nil,
@ -147,7 +148,7 @@ func (store *GreenhouseEasyPKIStore) Update(
) error {
panic("not implemented")
// if state != certificate.Revoked {
// return fmt.Errorf("unsupported update for certificate state %v", st)
// return fmt.Errorf("unsupported update for certificate state %+v", st)
// }
// store.DB.AddPKIRevocation(caName, serialNumber.Int64())
}


+ 6
- 7
schema_versions/02_up_create_tenants_etc.sql View File

@ -27,7 +27,7 @@ CREATE TABLE vps_instances (
tenant_id INTEGER NULL REFERENCES tenants(id) ON DELETE RESTRICT,
ipv4 TEXT NOT NULL,
ipv6 TEXT NOT NULL,
bytes_monthly INTEGER NOT NULL,
bytes_monthly NUMERIC NOT NULL,
created TIMESTAMP NOT NULL DEFAULT NOW(),
deprecated BOOLEAN NOT NULL DEFAULT FALSE,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
@ -41,20 +41,20 @@ CREATE TABLE tenant_vps_instance (
provider_instance_id TEXT NOT NULL,
tenant_id INTEGER NULL REFERENCES tenants(id) ON DELETE RESTRICT,
shadow_config TEXT NOT NULL,
bytes INTEGER NOT NULL,
bytes NUMERIC NOT NULL,
active BOOLEAN NOT NULL DEFAULT FALSE,
deactivated_at TIMESTAMP NULL,
CONSTRAINT vps_instance
FOREIGN KEY(service_provider, provider_instance_id)
REFERENCES vps_instances(service_provider, provider_instance_id) ON DELETE RESTRICT,
CONSTRAINT primary_key PRIMARY KEY (billing_year, billing_month, tenant_id, service_provider, provider_instance_id)
CONSTRAINT pk_tenant_vps_instance PRIMARY KEY (billing_year, billing_month, tenant_id, service_provider, provider_instance_id)
);
CREATE TABLE tenant_metrics_bandwidth (
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT,
measured TIMESTAMP NOT NULL DEFAULT NOW(),
bytes INTEGER NOT NULL,
PRIMARY KEY (service_provider, provider_instance_id, tenant_id, measured)
bytes NUMERIC NOT NULL,
PRIMARY KEY (tenant_id, measured)
);
CREATE TABLE email_verification_tokens (
@ -88,8 +88,7 @@ CREATE TABLE pki_key_pairs (
name TEXT NOT NULL,
key_bytes BYTEA NOT NULL,
cert_bytes BYTEA NOT NULL,
CONSTRAINT primary_key PRIMARY KEY (ca_name, name)
CONSTRAINT pk_pki_key_pairs PRIMARY KEY (ca_name, name)
);
UPDATE schema_version SET version = 2;

+ 9
- 8
ssh_service.go View File

@ -12,7 +12,7 @@ import (
"regexp"
"strings"
"github.com/pkg/errors"
errors "git.sequentialread.com/forest/pkg-errors"
)
type SSHService struct {
@ -25,10 +25,10 @@ func NewSSHService(config *Config) *SSHService {
}
}
func (service *SSHService) RunScriptOnRemoteHost(script, username, ipv4 string) error {
func (service *SSHService) RunScriptOnRemoteHost(script, username, ipv4 string) (string, string, error) {
remoteCommand := fmt.Sprintf(
"sh -c 'base64 -d %s | sh'",
"sh -c 'echo %s | base64 -d | sh'",
base64.StdEncoding.EncodeToString([]byte(script)),
)
@ -38,16 +38,17 @@ func (service *SSHService) RunScriptOnRemoteHost(script, username, ipv4 string)
&remoteCommand,
"ssh", "-i", service.SSHPrivateKeyFile, userAtHost,
)
commandForErrorMessage := fmt.Sprintf(
"echo \"sh -c 'base64 -d <base64InstallScript> | sh'\" | ssh -i %s %s",
"echo \"sh -c 'echo <base64InstallScript> | base64 -d | sh'\" | ssh -i %s %s",
service.SSHPrivateKeyFile, userAtHost,
)
err = errorFromShellExecResult(commandForErrorMessage, exitCode, stderr, stdout, err)
if err != nil {
return err
return string(stdout), string(stderr), err
}
return nil
return string(stdout), string(stderr), nil
}
func (service *SSHService) AppendToKnownHostsFile(knownHostsFileContent string) error {
@ -148,13 +149,13 @@ func shellExecInputPipe(input *string, executable string, arguments ...string) (
process.Stderr = &processStderrBuffer
err := process.Start()
if err != nil {
err = errors.Wrapf(err, "can't shellExecInputPipe(echo '%s' | %s %s), process.Start() returned", input, executable, strings.Join(arguments, " "))
err = errors.Wrapf(err, "can't shellExecInputPipe(echo '%s' | %s %s), process.Start() returned", *input, executable, strings.Join(arguments, " "))
return process.ProcessState.ExitCode(), []byte(""), []byte(""), err
}
err = process.Wait()
if err != nil {
err = errors.Wrapf(err, "can't shellExecInputPipe(echo '%s' | %s %s), process.Wait() returned", input, executable, strings.Join(arguments, " "))
err = errors.Wrapf(err, "can't shellExecInputPipe(echo '%s' | %s %s), process.Wait() returned", *input, executable, strings.Join(arguments, " "))
}
return process.ProcessState.ExitCode(), processStdoutBuffer.Bytes(), processStderrBuffer.Bytes(), err


+ 47
- 24
threshold_provisioning_service.go View File

@ -6,6 +6,8 @@ import (
"fmt"
"strings"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
)
type ThresholdProvisioningService struct {
@ -21,22 +23,28 @@ func NewThresholdProvisioningService(config *Config, pkiService *PKIService) *Th
func (service *ThresholdProvisioningService) GetMultitenantInstallScript(tlsCertificateSubject string) (string, error) {
installScript := `#!/bin/sh -e
installScript := `#!/bin/bash
#set -x
set -e
echo "creating threshold user and group"
useradd threshold
groupadd threshold
echo "creating threshold user and group" >&1
useradd threshold
#groupadd: group 'threshold' already exists
#groupadd threshold
usermod -a -G threshold threshold
echo "establishing threshold folder"
echo "establishing threshold folder" >&1
mkdir /opt/threshold
chown threshold:threshold /opt/threshold
chmod 500 /opt/threshold
echo "downloading threshold tar file"
echo "downloading threshold tar file" >&1
curl -sS {{ARTIFACTS_BASE_URL}}/threshold-{{ARCH}}.tar.gz > /tmp/threshold-{{ARCH}}.tar.gz
echo "verifying checksum"
echo "verifying checksum" >&1
CORRECT_CHECKSUM="$(sha256sum /tmp/threshold-{{ARCH}}.tar.gz | grep '{{TAR_SHA256}}' | wc -l)"
if [ $CORRECT_CHECKSUM -ne 1 ]; then
echo "bad checksum on /tmp/threshold-{{ARCH}}.tar.gz:"
@ -45,28 +53,28 @@ func (service *ThresholdProvisioningService) GetMultitenantInstallScript(tlsCert
exit 1
fi
echo "unarchiving threshold binary"
echo "unarchiving threshold binary" >&1
tar -x -f /tmp/threshold-{{ARCH}}.tar.gz --directory /opt/threshold
chown threshold:threshold /opt/threshold/threshold
chmod 500 /opt/threshold/threshold
echo "use setcap to allow threshold binary to listen on all ports without running as admin"
echo "use setcap to allow threshold binary to listen on all ports without running as admin" >&1
setcap cap_net_bind_service=+ep /opt/threshold/threshold
echo "writing threshold config file"
echo "writing threshold config file" >&1
echo '
{
"DebugLog": false,
"Domain": "{{THRESHOLD_DOMAIN}}",
"MultiTenantMode": true,
"MultiTenantInternalAPIListenPort": 9057
"MultiTenantInternalAPICaCertificateFile": "greenhouse_management_api_auth_CA.crt"
"MultiTenantInternalAPIListenPort": 9057,
"MultiTenantInternalAPICaCertificateFile": "greenhouse_management_api_auth_CA.crt",
"ListenPort": 9056,
"UseTls": true,
"CaCertificateFilesGlob": "greenhouse_CA.crt",
"ServerTlsKeyFile": "threshold.key",
"ServerTlsCertificateFile": "threshold.crt"
"ServerTlsCertificateFile": "threshold.crt",
"Metrics": {
"PrometheusMetricsAPIPort": 9090
}
@ -74,9 +82,9 @@ func (service *ThresholdProvisioningService) GetMultitenantInstallScript(tlsCert
' > /opt/threshold/config.json
chown threshold:threshold /opt/threshold/threshold
chmod 400 /opt/threshold/threshold
chmod 500 /opt/threshold/threshold
echo "writing x.509 certificate files"
echo "writing x.509 certificate files" >&1
echo '{{GREENHOUSE_MANAGEMENT_API_AUTH_CA}}' > /opt/threshold/greenhouse_management_api_auth_CA.crt
chown threshold:threshold /opt/threshold/greenhouse_management_api_auth_CA.crt
@ -94,7 +102,7 @@ func (service *ThresholdProvisioningService) GetMultitenantInstallScript(tlsCert
chown threshold:threshold /opt/threshold/threshold.key
chmod 400 /opt/threshold/threshold.key
echo "writing threshold systemd service unit file"
echo "writing threshold systemd service unit file" >&1
echo '
[Unit]
Description=TCP reverse tunnel for greenhouse.server.garden
@ -108,7 +116,9 @@ Restart=always
RestartSec=5
# never give up on restarting this service if it crashes
StartLimitIntervalSec=0
# /etc/systemd/system/threshold.service:14: Unknown lvalue 'StartLimitIntervalSec' i
#StartLimitIntervalSec=0
WorkingDirectory=/opt/threshold
@ -125,21 +135,34 @@ WantedBy=multi-user.target
chown threshold:threshold /etc/systemd/system/threshold.service
chmod 755 /etc/systemd/system/threshold.service
echo "enabling threshold systemd service"
echo "enabling threshold systemd service" >&1
systemctl daemon-reload
systemctl enable threshold.service
systemctl enable threshold.service >&1
echo "starting threshold systemd service"
systemctl start threshold.service
echo "starting threshold systemd service" >&1
systemctl start threshold.service >&1
sleep 1
echo "threshold has started!"
echo "printing threshold logs:" >&1
echo "" >&1
journalctl -u threshold.service -n 1000 --no-pager >&1
echo "" >&1
echo "" >&1
echo "" >&1
echo "checking threshold service status:" >&1
echo "" >&1
systemctl status threshold.service --no-pager >&1
echo "" >&1
echo "" >&1
echo "threshold has started!" >&1
`
managementAPIAuthCA, err := service.PKI.GetCACertificate(managementAPIAuthCAName)
if err != nil {
return "", err
return "", errors.Wrap(err, "GetCACertificate")
}
managementAPIAuthCABytes := pem.EncodeToMemory(&pem.Block{
Bytes: managementAPIAuthCA.Raw,
@ -148,7 +171,7 @@ WantedBy=multi-user.target
mainCA, err := service.PKI.GetCACertificate(mainCAName)
if err != nil {
return "", err
return "", errors.Wrap(err, "GetCACertificate")
}
mainCABytes := pem.EncodeToMemory(&pem.Block{
Bytes: mainCA.Raw,


Loading…
Cancel
Save