Browse Source

ingress service / integrate greenhouse daemon, multiple api tokens

master
forest 4 months ago
parent
commit
56de9e3c45
12 changed files with 481 additions and 246 deletions
  1. +11
    -1
      .gitignore
  2. +10
    -134
      backend.go
  3. +76
    -13
      db_model.go
  4. +67
    -19
      frontend.go
  5. +11
    -4
      frontend/admin.gotemplate.html
  6. +17
    -8
      frontend/profile.gotemplate.html
  7. +1
    -10
      frontend/static/greenhouse.css
  8. +9
    -0
      greenhouse-daemon/caddy-config.json
  9. +229
    -0
      ingress_service.go
  10. +1
    -1
      main.go
  11. +13
    -11
      schema_versions/02_up_create_tenants_etc.sql
  12. +36
    -45
      threshold_provisioning_service.go

+ 11
- 1
.gitignore View File

@ -4,4 +4,14 @@ config.json
favicon.ico
frontend/static/*.png
frontend/static/*.svg
threshold/threshold
threshold/threshold
greenhouse
greenhouse-daemon/caddy
greenhouse-daemon/caddyData
greenhouse-daemon/caddy.pid
greenhouse-daemon/daemon-config.json
greenhouse-daemon/daemon-tenant-info.json
greenhouse-daemon/greenhouse-daemon
greenhouse-daemon/threshold
greenhouse-daemon/threshold-config.json
greenhouse-daemon/threshold.pid

+ 10
- 134
backend.go View File

@ -2,8 +2,7 @@ package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
@ -22,7 +21,6 @@ import (
"git.sequentialread.com/forest/greenhouse/pki"
errors "git.sequentialread.com/forest/pkg-errors"
base58 "github.com/shengdoushi/base58"
)
type BaseHTTPService struct {
@ -109,7 +107,6 @@ const tenantPinDuration = 6 * time.Hour
const managementClientCertSubject = "management@greenhouse.server.garden"
const freeSubdomainDomain = "greenhouseusers.com"
const adminThresholdNodeId = "greenhouse_internal_node"
var projectedOverageAllowedBeforeSpawningNewInstance int64 = GIGABYTE * 250
var projectedUnderageAllowedBeforeTerminatingInstance int64 = TERABYTE
@ -132,7 +129,7 @@ func initBackend(
Gandi: NewGandiService(config),
BackblazeB2: NewBackblazeB2Service(config),
SSH: NewSSHService(config),
ThresholdProvisioning: NewThresholdProvisioningService(config, pkiService, config.AdminTenantId, adminThresholdNodeId, greenhouseThresholdServiceId),
ThresholdProvisioning: NewThresholdProvisioningService(pkiService),
ThresholdPort: config.ThresholdPort,
ThresholdManagementPort: config.ThresholdManagementPort,
AdminTenantId: config.AdminTenantId,
@ -224,16 +221,6 @@ func (app *BackendApp) InitializeTenant(tenantId int, email string) error {
return errors.Wrapf(err, "InitializeTenant() for '%s':", email)
}
apiTokenBuffer := make([]byte, 16)
rand.Read(apiTokenBuffer)
apiToken := base58.Encode(apiTokenBuffer, base58.BitcoinAlphabet)
rawHash := sha256.Sum256([]byte(apiToken))
hashedAPIToken := fmt.Sprintf("%x", rawHash)
err = app.Model.SetHashedAPIToken(tenantId, hashedAPIToken)
if err != nil {
return errors.Wrapf(err, "InitializeTenant() for '%s':", email)
}
return nil
}
@ -1075,125 +1062,6 @@ func (app *BackendApp) Rebalance() (bool, error) {
return true, nil
}
func (app *BackendApp) WriteAdminTenantThresholdConfig() error {
tenants, err := app.Model.GetTenants()
if err != nil {
return err
}
for tenantId, tenant := range tenants {
if tenantId == app.AdminTenantId {
clientConfig, err := app.ThresholdProvisioning.GetClientConfig(
app.AdminTenantId, fmt.Sprintf("%s.%s", tenant.Subdomain, freeSubdomainDomain), adminThresholdNodeId, "api_key_n_a",
)
if err != nil {
return err
}
clientConfig.ServiceToLocalAddrMap = &(map[string]string{"greenhouse_https": "127.0.0.1:8081"})
clientConfigBytes, err := json.MarshalIndent(clientConfig, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile("./threshold/config.json", clientConfigBytes, 755)
if err == nil {
log.Println("wrote threshold config!")
}
return err
}
}
return errors.New("admin tenant not found")
}
func (app *BackendApp) ConfigureAdminTenantOnThresholdServer() error {
log.Println("configuring threshold server...")
billingYear, billingMonth, _, _, _ := getBillingTimeInfo()
tenants, err := app.Model.GetTenants()
if err != nil {
return err
}
rows, err := app.Model.GetTenantVPSInstanceRows(billingYear, billingMonth)
if err != nil {
return err
}
vpsInstances, err := app.Model.GetVPSInstances()
if err != nil {
return err
}
actions := []func() taskResult{}
for _, row := range rows {
if row.TenantId == app.AdminTenantId {
vpsInstance, hasVpsInstance := vpsInstances[row.GetVPSInstanceId()]
tenant, hasTenant := tenants[row.TenantId]
if hasVpsInstance && hasTenant {
actions = append(actions, func() taskResult {
url := fmt.Sprintf("https://%s:%d/tunnels", vpsInstance.IPV4, app.ThresholdPort)
jsonBytes, err := json.Marshal([]ThresholdTunnel{
ThresholdTunnel{
ClientId: fmt.Sprintf("%d.%s", app.AdminTenantId, app.AdminThresholdNodeId),
ListenPort: 80,
ListenAddress: "0.0.0.0",
ListenHostnameGlob: fmt.Sprintf("%s.%s", tenant.Subdomain, freeSubdomainDomain),
BackEndService: app.GreenhouseThresholdServiceId,
},
ThresholdTunnel{
ClientId: fmt.Sprintf("%d.%s", app.AdminTenantId, app.AdminThresholdNodeId),
ListenPort: 443,
ListenAddress: "0.0.0.0",
ListenHostnameGlob: fmt.Sprintf("%s.%s", tenant.Subdomain, freeSubdomainDomain),
BackEndService: app.GreenhouseThresholdServiceId,
},
})
log.Println(string(jsonBytes))
if err != nil {
return taskResult{
Name: row.GetVPSInstanceId(),
Err: errors.Wrapf(
err,
"json serialization error calling %s (threshold admin tenant management API)",
url,
),
}
}
_, err = app.ThresholdProvisioning.MyHTTP200(
"PUT",
url,
bytes.NewBuffer(jsonBytes),
func(request *http.Request) { request.Header.Set("Content-Type", "application/json") },
)
if err != nil {
return taskResult{Name: row.GetVPSInstanceId(), Err: err}
}
return taskResult{Name: row.GetVPSInstanceId(), Err: nil}
})
}
}
}
if len(actions) == 0 {
return errors.New("admin tenant threshold server assignment not found")
}
errorStrings := []string{}
results := doInParallel(false, actions...)
for _, result := range results {
if result.Err != nil {
errorStrings = append(errorStrings, fmt.Sprintf("Can't update threshold tunnels for %s: %+v", result.Name, result.Err))
}
}
if len(errorStrings) > 0 {
return errors.Errorf("ConfigureThresholdServer(): \n%s", strings.Join(errorStrings, "\n"))
}
return nil
}
func getInstanceProjectedUsage(
instance *VPSInstance,
workingAllocations *map[string]map[int]bool,
@ -1401,6 +1269,14 @@ func (app *BackendApp) saveVpsInstanceTenantSettings(
// return responseObject, nil
// }
func CreateUnixTransport(socketFile string) *http.Transport {
return &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketFile)
},
}
}
func (service *BaseHTTPService) MyHTTP(
method string,
url string,


+ 76
- 13
db_model.go View File

@ -58,6 +58,7 @@ type TenantInfo struct {
PortBucket int
TunnelSettings *TunnelSettings
Deactivated bool
APITokens []APIToken
}
type TenantVPSInstance struct {
@ -70,6 +71,13 @@ type TenantVPSInstance struct {
DeactivatedAt *time.Time
}
type APIToken struct {
Name string
Active bool
Created time.Time
LastUsed time.Time
}
const DomainVerificationPollingInterval = time.Hour
func (i *TenantVPSInstance) GetVPSInstanceId() string {
@ -206,8 +214,8 @@ func (model *DBModel) Register(email, hashedPassword string) (int, error) {
var inserted int
err = model.DB.QueryRow(
"INSERT INTO tenants (email, hashed_password, hashed_api_token) VALUES ($1, $2, $3) RETURNING id",
strings.ToLower(email), hashedPassword, "",
"INSERT INTO tenants (email, hashed_password) VALUES ($1, $2) RETURNING id",
strings.ToLower(email), hashedPassword,
).Scan(&inserted)
if err != nil {
@ -332,18 +340,25 @@ func (model *DBModel) GetUserByAPIToken(hashedApiToken string) (*Session, error)
if len(hashedApiToken) < 8 {
return nil, errors.New("The given hashedApiToken token was too short")
}
err := model.DB.QueryRow(
`SELECT id, email, email_verified
FROM tenants WHERE hashed_api_token = $1`,
`SELECT tenant_id FROM api_tokens WHERE hashed_token = $1 AND active = TRUE`,
hashedApiToken,
).Scan(&loggedInTenantId, &email, &emailVerified)
).Scan(&loggedInTenantId)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, errors.Wrapf(err, "GetUserByAPIToken(hashedApiToken=%s): ", hashedApiToken)
}
err = model.DB.QueryRow(
`SELECT email, email_verified FROM tenants WHERE id = $1`, loggedInTenantId,
).Scan(&email, &emailVerified)
if err != nil {
return nil, errors.Wrapf(err, "GetUserByAPIToken(hashedApiToken=%s): ", hashedApiToken)
}
return &Session{
TenantId: loggedInTenantId,
Email: email,
@ -383,13 +398,35 @@ func (model *DBModel) SetReservedPorts(tenantId, portStart, portEnd, portBucket
return nil
}
func (model *DBModel) SetHashedAPIToken(tenantId int, hashedAPIToken string) error {
func (model *DBModel) CreateAPIToken(tenantId int, keyName, hashedAPIToken string) error {
_, err := model.DB.Exec(
"INSERT INTO api_tokens (tenant_id, key_name, hashed_token) VALUES ($1, $2, $3)",
tenantId, keyName, hashedAPIToken,
)
if err != nil {
return errors.Wrap(err, "CreateAPIToken(): ")
}
return nil
}
func (model *DBModel) SetAPITokenActive(tenantId int, keyName string, active bool) error {
_, err := model.DB.Exec(
"UPDATE api_tokens SET active = $1 WHERE tenant_id = $2 AND keyName = $3",
active, tenantId, keyName,
)
if err != nil {
return errors.Wrap(err, "SetAPITokenActive(): ")
}
return nil
}
func (model *DBModel) DeleteAPIToken(tenantId int, keyName string) error {
_, err := model.DB.Exec(
"UPDATE tenants SET hashed_api_token = $1 WHERE id = $2",
hashedAPIToken, tenantId,
"DELETE FROM api_tokens WHERE tenant_id = $1 AND keyName = $2",
tenantId, keyName,
)
if err != nil {
return errors.Wrap(err, "SetHashedAPIToken(): ")
return errors.Wrap(err, "DeleteAPIToken(): ")
}
return nil
}
@ -568,7 +605,7 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
func (model *DBModel) GetTenant(tenantId int) (*TenantInfo, error) {
rows, err := model.DB.Query(
`SELECT tenant_id, domain_name, last_verified FROM external_domains WHERE tenant_id = $1`,
`SELECT domain_name, last_verified FROM external_domains WHERE tenant_id = $1`,
tenantId,
)
if err != nil {
@ -578,10 +615,9 @@ func (model *DBModel) GetTenant(tenantId int) (*TenantInfo, error) {
verificationCutoff := time.Now().Add(-(DomainVerificationPollingInterval + time.Minute))
authorizedDomains := []string{}
for rows.Next() {
var tenantId int
var domainName string
var lastVerified time.Time
err := rows.Scan(&tenantId, &domainName, &lastVerified)
err := rows.Scan(&domainName, &lastVerified)
if err != nil {
return nil, errors.Wrapf(err, "GetTenant(%d): ", tenantId)
}
@ -591,6 +627,32 @@ func (model *DBModel) GetTenant(tenantId int) (*TenantInfo, error) {
}
}
rows, err = model.DB.Query(
`SELECT key_name, active, created, last_used FROM api_tokens WHERE tenant_id = $1`,
tenantId,
)
if err != nil {
return nil, errors.Wrapf(err, "GetTenant(%d): ", tenantId)
}
apiTokens := []APIToken{}
for rows.Next() {
var keyName string
var active bool
var created time.Time
var lastUsed time.Time
err := rows.Scan(&keyName, &active, &created, &lastUsed)
if err != nil {
return nil, errors.Wrapf(err, "GetTenant(%d): ", tenantId)
}
apiTokens = append(apiTokens, APIToken{
Name: keyName,
Active: active,
Created: created,
LastUsed: lastUsed,
})
}
var created time.Time
var subdomain *string
var email string
@ -638,6 +700,7 @@ func (model *DBModel) GetTenant(tenantId int) (*TenantInfo, error) {
PortEnd: portEnd,
AuthorizedDomains: authorizedDomains,
},
APITokens: apiTokens,
}, nil
}


+ 67
- 19
frontend.go View File

@ -53,6 +53,7 @@ type FrontendApp struct {
EmailService *EmailService
Model *DBModel
Backend *BackendApp
Ingress *IngressService
HTMLTemplates map[string]*template.Template
PasswordHashSalt string
SessionCache map[string]*Session
@ -74,6 +75,7 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
EmailService: emailService,
Model: model,
Backend: backend,
Ingress: NewIngressService(config, model),
HTMLTemplates: map[string]*template.Template{},
PasswordHashSalt: "Ko0jOdSCzEyDtK4rmoocfcR9LxwOrIZsaVPBjImkb6AhRW6yNSmgsU122ArU1URBjcJ1EnskZ5r7",
SessionCache: map[string]*Session{},
@ -310,6 +312,8 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
http.Redirect(responseWriter, request, "/verify-email", http.StatusFound)
return
}
// email is verified, continue:
rawHash := sha256.Sum256([]byte(session.SessionId))
hashOfSessionId := fmt.Sprintf("%x", rawHash[:8])
@ -326,7 +330,8 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
return
}
apiToken := (*session.Flash)["api-token"]
newAPIToken := (*session.Flash)["api-token"]
newAPITokenName := (*session.Flash)["api-token-name"]
if request.Method == "POST" {
postedHashOfSessionId := request.PostFormValue("hashOfSessionId")
if postedHashOfSessionId != hashOfSessionId {
@ -335,19 +340,31 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
return
}
action := request.PostFormValue("action")
if action == "reload_api_token" {
if action == "create_api_token" {
keyName := strings.TrimSpace(request.PostFormValue("key_name"))
if len(keyName) == 0 {
app.setFlash(responseWriter, session, "error", "key name is required\n")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
if !regexp.MustCompile("[A-Za-z0-9_-]+").MatchString(keyName) {
app.setFlash(responseWriter, session, "error", "key name may only contain letters, numbers, dashes, and underscores\n")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
apiTokenBuffer := make([]byte, 16)
rand.Read(apiTokenBuffer)
apiToken := base58.Encode(apiTokenBuffer, base58.BitcoinAlphabet)
rawHash := sha256.Sum256([]byte(apiToken))
hashedAPIToken := fmt.Sprintf("%x", rawHash)
err := app.Model.SetHashedAPIToken(session.TenantId, hashedAPIToken)
err := app.Model.CreateAPIToken(session.TenantId, keyName, hashedAPIToken)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
app.setFlash(responseWriter, session, "api-token", apiToken)
app.setFlash(responseWriter, session, "info", fmt.Sprintf("Success! Your new API Token is %s. It will not be displayed again, so make sure to copy and paste it or write it down now!\n", apiToken))
app.setFlash(responseWriter, session, "api-token-name", keyName)
app.setFlash(responseWriter, session, "info", fmt.Sprintf("Success! Your new '%s' API Token is %s. It will not be displayed again, so make sure to copy and paste it or write it down now!\n", keyName, apiToken))
} else {
app.setFlash(responseWriter, session, "error", "unknown action\n")
}
@ -355,9 +372,19 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
return
}
apiTokens := []APIToken{}
for _, token := range tenant.APITokens {
//fmt.Printf("%s %s", token.Name, newAPITokenName)
if token.Name != newAPITokenName {
apiTokens = append(apiTokens, token)
}
}
data := struct {
Subdomain string
APIToken string
APITokens []APIToken
NewAPIToken string
NewAPITokenName string
BytesSoFar string
BillingAlarmSMS string
BillingAlarmEmail string
@ -366,7 +393,9 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
HashOfSessionId string
}{
Subdomain: tenant.Subdomain,
APIToken: apiToken,
APITokens: apiTokens,
NewAPIToken: newAPIToken,
NewAPITokenName: newAPITokenName,
BytesSoFar: ByteCountSI(usageTotal),
BillingAlarmSMS: tenant.SMSAlarmNumber,
BillingAlarmEmail: tenant.Email,
@ -565,23 +594,42 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
}
})()
} else if action == "configure_threshold_client" {
err := app.Backend.WriteAdminTenantThresholdConfig()
if err != nil {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("configure_threshold_client failed: %+v\n", err))
log.Printf("configure_threshold_client failed: %+v\n", err)
} else if action == "start_daemon" {
if app.Ingress.DaemonIsRunning {
app.setFlash(responseWriter, session, "error", "daemon is already running\n")
log.Println("daemon is already running")
} else {
app.setFlash(responseWriter, session, "info", "wrote threshold client config!\n")
log.Println("wrote threshold client config!")
err := app.Ingress.StartGreenhouseDaemon()
if err != nil {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("start_daemon failed: %+v\n", err))
log.Printf("start_daemon failed: %+v\n", err)
} else {
app.setFlash(responseWriter, session, "info", "daemon appears to be running!\n")
log.Println("daemon appears to be running!")
}
}
} else if action == "configure_daemon" {
if !app.Ingress.DaemonIsRunning {
app.setFlash(responseWriter, session, "error", "daemon isn't running\n")
log.Println("daemon isn't running")
} else {
err := app.Ingress.ConfigureGreenhouseDaemon()
if err != nil {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("configure_daemon failed: %+v\n", err))
log.Printf("configure_daemon failed: %+v\n", err)
} else {
app.setFlash(responseWriter, session, "info", "daemon appears to be running!\n")
log.Println("daemon appears to be running!")
}
}
} else if action == "configure_threshold_server" {
err := app.Backend.ConfigureAdminTenantOnThresholdServer()
} else if action == "daemon_status" {
responseString, err := app.Ingress.GetGreenhouseDaemonStatus()
if err != nil {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("configure_threshold_server failed: %+v\n", err))
log.Printf("configure_threshold_server failed: %+v\n", err)
app.setFlash(responseWriter, session, "error", fmt.Sprintf("daemon_status failed: %+v\n", err))
log.Printf("daemon_status failed: %+v\n", err)
} else {
app.setFlash(responseWriter, session, "info", "configured threshold server!\n")
log.Println("configured threshold server!")
app.setFlash(responseWriter, session, "info", fmt.Sprintf("daemon status: %s\n", responseString))
log.Printf("daemon status: %s\n", responseString)
}
} else {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("Unknown action '%s'\n", action))


+ 11
- 4
frontend/admin.gotemplate.html View File

@ -18,15 +18,22 @@
<li>
<form method="POST" action="#">
<input type="hidden" name="hashOfSessionId" value="{{ .HashOfSessionId }}"/>
<input type="hidden" name="action" value="configure_threshold_client"/>
<input type="submit" value="configure_threshold_client"/>
<input type="hidden" name="action" value="start_daemon"/>
<input type="submit" value="start_daemon"/>
</form>
</li>
<li>
<form method="POST" action="#">
<input type="hidden" name="hashOfSessionId" value="{{ .HashOfSessionId }}"/>
<input type="hidden" name="action" value="configure_threshold_server"/>
<input type="submit" value="configure_threshold_server"/>
<input type="hidden" name="action" value="configure_daemon"/>
<input type="submit" value="configure_daemon"/>
</form>
</li>
<li>
<form method="POST" action="#">
<input type="hidden" name="hashOfSessionId" value="{{ .HashOfSessionId }}"/>
<input type="hidden" name="action" value="daemon_status"/>
<input type="submit" value="daemon_status"/>
</form>
</li>
</ul>


+ 17
- 8
frontend/profile.gotemplate.html View File

@ -14,21 +14,30 @@
</p>
<div class="horizontal justify-right align-center margin-bottom">
<form method="POST" action="#">
<label for="api-token">API Token: </label>
{{ if .APIToken }}
<span class="fake-text-input">{{ .APIToken }}</span>
{{ else }}
<span class="fake-text-input">••••••••••••••</span>
{{ if or .APITokens .NewAPIToken }}
<label for="api-tokens">API Tokens: </label>
<div id="api-tokens">
{{ range $token := .APITokens }}
<div class="horizontal"><span>{{ $token.Name }}</span></div>
{{ end }}
{{ if .NewAPIToken }}
<div class="new-api-token"><div>{{ .NewAPITokenName }}</div> <div class="new-api-token-token">{{ .NewAPIToken }}</div></div>
{{ end }}
</div>
{{ end }}
<label for="key_name">New API Token: </label>
<input type="hidden" name="hashOfSessionId" value="{{ .HashOfSessionId }}"/>
<input type="hidden" name="action" value="reload_api_token"/>
<input type="image" name="submit" src="/static/reload.svg" alt="Submit" class="reload-api-token-button" />
<input type="hidden" name="action" value="create_api_token"/>
<input type="text" name="key_name" id="key_name" />
<input type="submit" name="submit" alt="Submit" class="create-token-button" value="create" />
</form>
</div>
<div class="horizontal justify-right align-center margin-bottom">
<span class="fine-print">
{{ if .APIToken }}
{{ if .NewAPIToken }}
This API Token will not be displayed again, <br/>
so make sure to copy and paste it or write it down now!
{{ else }}


+ 1
- 10
frontend/static/greenhouse.css View File

@ -213,16 +213,7 @@ input[type=number] {
min-width: 18em;
}
.reload-api-token-button {
border-radius: 100%;
border: 2px solid #ccc;
display: inline-block;
width: 1.3em;
padding: 0.3em;
position: relative;
top: 0.68em;
margin-top: -0.68em;
}
label {
margin-right: 1em;


+ 9
- 0
greenhouse-daemon/caddy-config.json View File

@ -0,0 +1,9 @@
{
"admin": {
"disabled": false,
"listen": "unix///var/run/greenhouse-daemon-caddy-admin.sock",
"config": {
"persist": false
}
}
}

+ 229
- 0
ingress_service.go View File

@ -0,0 +1,229 @@
package main
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
base58 "github.com/shengdoushi/base58"
)
type IngressService struct {
BaseHTTPService
Model *DBModel
DaemonIsRunning bool
AdminTenantId int
FrontendPort int
}
type GUITunnel struct {
Protocol string `json:"protocol"`
HasSubdomain bool `json:"has_subdomain"`
Subdomain string `json:"subdomain"`
Domain string `json:"domain"`
DestinationType string `json:"destination_type"`
DestinationPort int `json:"destination_port"`
}
type GreenhouseDaemonStatus struct {
NeedsAPIToken bool `json:"needs_api_token"`
}
const adminThresholdNodeId = "greenhouse_internal_node"
func NewIngressService(config *Config, model *DBModel) *IngressService {
toReturn := &IngressService{
Model: model,
AdminTenantId: config.AdminTenantId,
FrontendPort: config.FrontendPort,
}
toReturn.ClientFactory = func() (*http.Client, *time.Time, error) {
// 99 years aka forever, never expire
expiryTime := time.Now().Add(time.Hour * 24 * 30 * 12 * 99)
return &http.Client{
Transport: CreateUnixTransport("/var/run/greenhouse-daemon.sock"),
Timeout: 10 * time.Second,
}, &expiryTime, nil
}
return toReturn
}
func (service *IngressService) StartGreenhouseDaemon() error {
if service.DaemonIsRunning {
return errors.New("Daemon is already Running ")
}
cwd, err := os.Getwd()
if err != nil {
return err
}
greenhouseDaemonPath := path.Join(cwd, "greenhouse-daemon")
command := exec.Command(path.Join(greenhouseDaemonPath, "greenhouse-daemon"))
env := os.Environ()
env = append(env, fmt.Sprintf("GREENHOUSE_DAEMON_PATH=%s", greenhouseDaemonPath))
env = append(env, "GREENHOUSE_DAEMON_USE_UNIX_SOCKETS=true")
env = append(env, fmt.Sprintf("GREENHOUSE_DAEMON_CLOUD_URL=http://localhost:%d", service.FrontendPort))
command.Env = env
command.Dir = greenhouseDaemonPath
stdoutReader, err := command.StdoutPipe()
if err != nil {
log.Printf("can't Start greenhouse-daemon, command.StdoutPipe() returned %s", err)
return err
}
stdoutScanner := bufio.NewScanner(stdoutReader)
stdoutScanner.Split(bufio.ScanLines)
go (func() {
for stdoutScanner.Scan() {
log.Printf("[greenhouse-daemon stdout] %s \n", stdoutScanner.Text())
}
})()
stderrReader, err := command.StderrPipe()
if err != nil {
log.Printf("can't Start(greenhouse-daemon), command.StdoutPipe() returned %s", err)
return err
}
stderrScanner := bufio.NewScanner(stderrReader)
stderrScanner.Split(bufio.ScanLines)
go (func() {
for stderrScanner.Scan() {
log.Printf("[greenhouse-daemon stderr] %s \n", stderrScanner.Text())
}
})()
err = command.Start()
if err != nil {
log.Printf("can't Start greenhouse-daemon, command.Start() returned %s", err)
return err
}
log.Printf("started greenhouse-daemon with PID %d\n", command.Process.Pid)
service.DaemonIsRunning = true
go (func() {
err := command.Wait()
service.DaemonIsRunning = false
if err != nil {
log.Printf("command.Wait() returned '%s' for greenhouse-daemon child process", err)
}
log.Printf("greenhouse-daemon child process ended with exit code %d", command.ProcessState.ExitCode())
})()
return nil
}
func (service *IngressService) GetGreenhouseDaemonStatus() (string, error) {
responseBytes, err := service.MyHTTP200("GET", "http://unix/status", nil, nil)
if err != nil {
return "", err
}
return string(responseBytes), nil
}
func (service *IngressService) ConfigureGreenhouseDaemon() error {
responseBytes, err := service.MyHTTP200("GET", "http://unix/status", nil, nil)
if err != nil {
return err
}
var responseStatus GreenhouseDaemonStatus
err = json.Unmarshal(responseBytes, &responseStatus)
if err != nil {
return err
}
tenant, err := service.Model.GetTenant(service.AdminTenantId)
if err != nil {
return err
}
if responseStatus.NeedsAPIToken {
i := 0
newTokenName := "greenhouse_builtin_ingress"
for i < 100 {
conflict := false
for _, token := range tenant.APITokens {
if token.Name == newTokenName {
conflict = true
}
}
if conflict == false {
break
}
i++
newTokenName = fmt.Sprintf("greenhouse_builtin_ingress_%d", i)
}
if i >= 100 {
return errors.New("too many greenhouse_builtin_ingress tokens")
}
apiTokenBuffer := make([]byte, 16)
rand.Read(apiTokenBuffer)
apiToken := base58.Encode(apiTokenBuffer, base58.BitcoinAlphabet)
rawHash := sha256.Sum256([]byte(apiToken))
hashedAPIToken := fmt.Sprintf("%x", rawHash)
err = service.Model.CreateAPIToken(service.AdminTenantId, newTokenName, hashedAPIToken)
if err != nil {
return err
}
_, err := service.MyHTTP200(
"POST",
fmt.Sprintf("http://unix/register?serverName=%s", adminThresholdNodeId),
nil,
func(request *http.Request) {
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiToken))
},
)
if err != nil {
return err
}
}
greenhouseGUITunnels := []GUITunnel{
GUITunnel{
Protocol: "https",
HasSubdomain: true,
Subdomain: "greenhouse",
Domain: fmt.Sprintf("%s.%s", tenant.Subdomain, freeSubdomainDomain),
DestinationType: "local_port",
DestinationPort: service.FrontendPort,
},
}
tunnelsBytes, err := json.Marshal(greenhouseGUITunnels)
if err != nil {
return err
}
_, err = service.MyHTTP200(
"POST",
"http://unix/apply_config",
bytes.NewBuffer(tunnelsBytes),
nil,
)
if err != nil {
return err
}
return nil
}

+ 1
- 1
main.go View File

@ -138,7 +138,7 @@ func getConfig(workingDirectory string) *Config {
}
if config.AdminTenantId == 0 {
config.AdminTenantId = 2
config.AdminTenantId = 1
}
configToLog, _ := json.MarshalIndent(config, "", " ")


+ 13
- 11
schema_versions/02_up_create_tenants_etc.sql View File

@ -3,7 +3,6 @@ CREATE TABLE tenants (
email TEXT NOT NULL UNIQUE,
subdomain TEXT UNIQUE,
hashed_password TEXT NOT NULL,
hashed_api_token TEXT NOT NULL,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
lax_cookie BOOLEAN NOT NULL DEFAULT TRUE,
sms_alarm_number TEXT NULL,
@ -15,14 +14,17 @@ CREATE TABLE tenants (
created TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE client_key_pairs (
id SERIAL PRIMARY KEY,
CREATE TABLE api_tokens (
tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT,
node_id TEXT NOT NULL,
key_pem TEXT NOT NULL,
cert_pem TEXT NOT NULL,
created TIMESTAMP NOT NULL DEFAULT NOW()
key_name TEXT NOT NULL,
hashed_token TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
created TIMESTAMP NOT NULL DEFAULT NOW(),
last_used TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (tenant_id, key_name)
);
CREATE INDEX by_hashed_token ON api_tokens (hashed_token);
CREATE TABLE vps_instances (
service_provider TEXT NOT NULL,
@ -89,10 +91,10 @@ CREATE TABLE pki_key_pairs (
CONSTRAINT pk_pki_key_pairs PRIMARY KEY (ca_name, name)
);
CREATE TABLE task_queue (
id SERIAL PRIMARY KEY,
task TEXT NOT NULL
);
-- CREATE TABLE task_queue (
-- id SERIAL PRIMARY KEY,
-- task TEXT NOT NULL
-- );
CREATE TABLE reserved_ports_counter (
port INTEGER NOT NULL,


+ 36
- 45
threshold_provisioning_service.go View File

@ -1,12 +1,10 @@
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"net/http"
"strings"
"time"
@ -21,8 +19,7 @@ const thresholdCertsDomain = "greenhouse.server.garden"
type ThresholdProvisioningService struct {
BaseHTTPService
PKI *pki.PKIService
GreenhouseThresholdServiceId string
PKI *pki.PKIService
}
type ThresholdTunnel struct {
@ -52,45 +49,42 @@ type ThresholdClientConfig struct {
ClientTlsCertificate string
}
func NewThresholdProvisioningService(config *Config, pkiService *pki.PKIService, adminTenantId int, adminThresholdNodeId, greenhouseThresholdServiceId string) *ThresholdProvisioningService {
func NewThresholdProvisioningService(pkiService *pki.PKIService) *ThresholdProvisioningService {
toReturn := &ThresholdProvisioningService{
PKI: pkiService,
GreenhouseThresholdServiceId: greenhouseThresholdServiceId,
}
toReturn := &ThresholdProvisioningService{PKI: pkiService}
toReturn.ClientFactory = func() (*http.Client, *time.Time, error) {
clientId := fmt.Sprintf("%d.%s", adminTenantId, adminThresholdNodeId)
adminTenantClientCertSubject := fmt.Sprintf("%s@%s", clientId, thresholdCertsDomain)
caCert, err := pkiService.GetCACertificate(mainCAName)
if err != nil {
return nil, nil, err
}
expiryTime := time.Now().Add(time.Hour * 24)
cert, err := pkiService.GetClientTLSCertificate(mainCAName, adminTenantClientCertSubject, expiryTime)
if err != nil {
return nil, nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AddCert(caCert)
tlsClientConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
tlsClientConfig.BuildNameToCertificate()
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsClientConfig,
},
Timeout: 10 * time.Second,
}, &expiryTime, nil
}
// toReturn.ClientFactory = func() (*http.Client, *time.Time, error) {
// clientId := fmt.Sprintf("%d.%s", adminTenantId, adminThresholdNodeId)
// adminTenantClientCertSubject := fmt.Sprintf("%s@%s", clientId, thresholdCertsDomain)
// caCert, err := pkiService.GetCACertificate(mainCAName)
// if err != nil {
// return nil, nil, err
// }
// expiryTime := time.Now().Add(time.Hour * 24)
// cert, err := pkiService.GetClientTLSCertificate(mainCAName, adminTenantClientCertSubject, expiryTime)
// if err != nil {
// return nil, nil, err
// }
// caCertPool := x509.NewCertPool()
// caCertPool.AddCert(caCert)
// tlsClientConfig := &tls.Config{
// Certificates: []tls.Certificate{cert},
// RootCAs: caCertPool,
// }
// tlsClientConfig.BuildNameToCertificate()
// return &http.Client{
// Transport: &http.Transport{
// TLSClientConfig: tlsClientConfig,
// },
// Timeout: 10 * time.Second,
// }, &expiryTime, nil
// }
return toReturn
}
@ -197,9 +191,6 @@ func (service *ThresholdProvisioningService) GetServerInstallScript(tlsCertifica
}
' > /opt/threshold/config.json
chown threshold:threshold /opt/threshold/threshold
chmod 500 /opt/threshold/threshold
echo "writing x.509 certificate files"
echo '{{GREENHOUSE_MANAGEMENT_API_AUTH_CA}}' > /opt/threshold/greenhouse_management_api_auth_CA.crt
@ -309,7 +300,7 @@ WantedBy=multi-user.target
substitutions := map[string]string{
"ARTIFACTS_BASE_URL": "https://f000.backblazeb2.com/file/server-garden-artifacts",
"ARCH": "amd64",
"TAR_SHA256": "e7f030c188e43d5867cea8a3763de6b610f9b2b6bba30eb7cfc4c6bb3271f9f9",
"TAR_SHA256": "4ce0ab86fec54c86493bf2d852a7782bdb92913fa798e706625017dff7676a76",
"THRESHOLD_DOMAIN": "greenhouse.server.garden",
"GREENHOUSE_MANAGEMENT_API_AUTH_CA": string(managementAPIAuthCABytes),
"GREENHOUSE_CA": string(mainCABytes),


Loading…
Cancel
Save