Browse Source

got buttons working on admin panel and rebalance is no longer throwing

errors
master
forest 6 months ago
parent
commit
ee9b35d3b0
10 changed files with 383 additions and 57 deletions
  1. +35
    -16
      backend.go
  2. +29
    -8
      db_model.go
  3. +0
    -1
      digitalocean_service.go
  4. +93
    -8
      frontend.go
  5. +84
    -2
      frontend/admin.gotemplate.html
  6. +127
    -0
      frontend/static/greenhouse.css
  7. +2
    -14
      main.go
  8. +8
    -4
      pki_service.go
  9. +1
    -1
      schema_versions/02_up_create_tenants_etc.sql
  10. +4
    -3
      threshold_provisioning_service.go

+ 35
- 16
backend.go View File

@ -11,6 +11,7 @@ import (
"log"
"math"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
@ -95,13 +96,14 @@ func initBackend(
) *BackendApp {
toReturn := BackendApp{
WorkingDirectory: workingDirectory,
EmailService: emailService,
Model: model,
DigitalOcean: NewDigitalOceanService(config),
BackblazeB2: NewBackblazeB2Service(config),
SSH: NewSSHService(config),
ThresholdProvisioning: NewThresholdProvisioningService(config, pkiService),
WorkingDirectory: workingDirectory,
EmailService: emailService,
Model: model,
DigitalOcean: NewDigitalOceanService(config),
BackblazeB2: NewBackblazeB2Service(config),
SSH: NewSSHService(config),
ThresholdProvisioning: NewThresholdProvisioningService(config, pkiService),
ThresholdManagementPort: config.ThresholdManagementPort,
}
toReturn.ClientFactory = func() (*http.Client, *time.Time, error) {
@ -111,7 +113,7 @@ func initBackend(
}
expiryTime := time.Now().Add(time.Hour * 24)
cert, err := pkiService.GetTLSCertificate(managementAPIAuthCAName, managementClientCertSubject, expiryTime)
cert, err := pkiService.GetTLSCertificate(managementAPIAuthCAName, managementClientCertSubject, []net.IP{}, expiryTime)
if err != nil {
return nil, nil, err
}
@ -203,7 +205,6 @@ func (app *BackendApp) GetInstances() (map[string]*VPSInstance, map[string]*VPSI
validVpsInstances := map[string]*VPSInstance{}
onlyCloudInstances := map[string]*VPSInstance{}
onlyDBInstances := map[string]*VPSInstance{}
errorStrings := []string{}
for k, v := range cloudVPSInstances {
if _, has := dbVPSInstances[k]; !has {
onlyCloudInstances[k] = v
@ -219,16 +220,16 @@ func (app *BackendApp) GetInstances() (map[string]*VPSInstance, map[string]*VPSI
return validVpsInstances, onlyDBInstances, onlyCloudInstances, nil
}
func (app *BackendApp) HealthcheckInstances(vpsInstances map[string]*VPSInstance) []string {
func (app *BackendApp) HealthcheckInstances(vpsInstances map[string]*VPSInstance) map[string]bool {
if len(vpsInstances) == 0 {
return []string{}
return map[string]bool{}
}
actions := make([]func() taskResult, len(vpsInstances))
i := 0
for instanceId, instance := range vpsInstances {
actions[i] = func() taskResult {
responseBytes, err := app.MyHTTP200(
_, err := app.MyHTTP200(
"GET",
fmt.Sprintf("https://%s:%d/ping", instance.IPV4, app.ThresholdManagementPort),
nil,
@ -245,15 +246,15 @@ func (app *BackendApp) HealthcheckInstances(vpsInstances map[string]*VPSInstance
results := doInParallel(false, actions...)
errorInstanceIds := []string{}
healthStatus := map[string]bool{}
for _, result := range results {
if result.Err != nil {
log.Printf("error pinging %s: %s", result.Name, result.Err)
}
errorInstanceIds = append(errorInstanceIds, result.Name)
healthStatus[result.Name] = (result.Err == nil)
}
return errorInstanceIds
return healthStatus
}
func (app *BackendApp) Rebalance() (bool, error) {
@ -498,6 +499,9 @@ func (app *BackendApp) Rebalance() (bool, error) {
}
for _, instanceMetadata := range lowestUsedInstances {
if _, has := (*workingAllocations)[instanceMetadata.id]; !has {
(*workingAllocations)[instanceMetadata.id] = map[int]bool{}
}
(*workingAllocations)[instanceMetadata.id][tenant.Id] = true
}
}
@ -714,6 +718,9 @@ func (app *BackendApp) Rebalance() (bool, error) {
}
for instanceId, instanceAllocations := range workingAllocations {
for tenantId := range instanceAllocations {
if _, has := newConfig[instanceId]; !has {
newConfig[instanceId] = map[int]*TunnelSettings{}
}
newConfig[instanceId][tenantId] = tenants[tenantId].TunnelSettings
}
}
@ -818,7 +825,19 @@ func (app *BackendApp) SpawnNewMultitenantInstance() (*VPSInstance, error) {
}
log.Printf("Generating threshold install script for %s (%s)...\n", instance.GetId(), instance.IPV4)
provisionThresholdScript, err := app.ThresholdProvisioning.GetMultitenantInstallScript(instance.IPV4)
ipv4 := net.ParseIP(instance.IPV4)
ipv6 := net.ParseIP(instance.IPV6)
ips := []net.IP{}
if ipv4 != nil {
ips = append(ips, ipv4)
}
if ipv6 != nil {
ips = append(ips, ipv6)
}
if len(ips) == 0 {
return nil, errors.Errorf("failed to convert instance IPs '%s' or '%s' to net.IP", instance.IPV4, instance.IPV6)
}
provisionThresholdScript, err := app.ThresholdProvisioning.GetMultitenantInstallScript(instance.GetId(), ips)
if err != nil {
return nil, errors.Wrap(err, "failed to ThresholdProvisioning.GetMultitenantInstallScript")
}


+ 29
- 8
db_model.go View File

@ -366,6 +366,24 @@ func (model *DBModel) CreateVPSInstance(toCreate *VPSInstance) error {
return nil
}
func (model *DBModel) DeleteVPSInstance(provider, providerInstanceId string) error {
result, err := model.DB.Exec(
`UPDATE vps_instances SET deleted = TRUE WHERE service_provider = $1 AND provider_instance_id = $2`,
provider, providerInstanceId,
)
if err != nil {
return errors.Wrap(err, "DeleteVPSInstance(): ")
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return errors.Wrap(err, "DeleteVPSInstance(): ")
}
if rowsAffected == 0 {
return errors.Errorf("DeleteVPSInstance(): '%s-%s' was not found", provider, providerInstanceId)
}
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`)
@ -426,9 +444,9 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
for rows.Next() {
var tenantId int
var tenantCreated time.Time
var subdomain string
var subdomain *string
var serviceLimitCents int
err := rows.Scan(&tenantId, &tenantCreated, subdomain, &serviceLimitCents)
err := rows.Scan(&tenantId, &tenantCreated, &subdomain, &serviceLimitCents)
if err != nil {
return nil, errors.Wrap(err, "GetTenants(): ")
}
@ -437,6 +455,9 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
if thisTenantDomains == nil {
thisTenantDomains = []string{}
}
if subdomain != nil {
thisTenantDomains = append(thisTenantDomains, *subdomain)
}
thisTenantPorts := reservedPorts[tenantId]
if thisTenantPorts == nil {
@ -448,7 +469,7 @@ func (model *DBModel) GetTenants() (map[int]*TenantInfo, error) {
Created: tenantCreated,
ServiceLimitCents: serviceLimitCents,
TunnelSettings: &TunnelSettings{
AuthorizedDomains: append(thisTenantDomains, subdomain),
AuthorizedDomains: thisTenantDomains,
ReservedPorts: thisTenantPorts,
},
}
@ -540,17 +561,17 @@ func (model *DBModel) SaveInstanceConfiguration(
_, err = model.DB.Exec(`
INSERT INTO tenant_vps_instance (
billing_year, billing_month, tenant_id, service_provider, provider_instance_id
shadow_config, active
billing_year, billing_month, tenant_id, service_provider, provider_instance_id,
shadow_config, bytes, active
)
VALUES($1, $2, $3, $4, $5,
$6, $7)
$6, 0, $7)
ON CONFLICT ON CONSTRAINT pk_tenant_vps_instance
DO
UPDATE SET shadow_config = $6, active = $7;
`,
billingYear, billingMonth, tenantId, instance.ServiceProvider, instance.ProviderInstanceId,
shadowConfig, true, time.Now().UTC(),
shadowConfig, true,
)
if err != nil {
return errors.Wrap(err, "SaveInstanceConfiguration(): ")
@ -622,7 +643,7 @@ func (model *DBModel) rowToVPSInstance(row RowScanner) (*VPSInstance, error) {
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, &ipv6, &bytesMonthly, &created, &deprecated, &deleted)
if err != nil {
return nil, errors.Wrap(err, "rowToVPSInstance(): ")
}


+ 0
- 1
digitalocean_service.go View File

@ -37,7 +37,6 @@ type VPSInstance struct {
Created time.Time
Deprecated bool
Deleted bool
Unhealthy bool
}
func (i *VPSInstance) GetId() string {


+ 93
- 8
frontend.go View File

@ -65,7 +65,6 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
EmailService: emailService,
Model: model,
Backend: backend,
DigitalOcean: NewDigitalOceanService(config),
HTMLTemplates: map[string]*template.Template{},
PasswordHashSalt: "Ko0jOdSCzEyDtK4rmoocfcR9LxwOrIZsaVPBjImkb6AhRW6yNSmgsU122ArU1URBjcJ1EnskZ5r7",
SessionCache: map[string]*Session{},
@ -323,22 +322,108 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
app.handleWithSpecificUser("/admin", app.AdminTenantId, func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
rawHash := sha256.Sum256([]byte(session.SessionId))
hashOfSessionId := fmt.Sprintf("%x", rawHash[:])
if request.Method == "POST" {
postedHashOfSessionId := request.PostFormValue("hashOfSessionId")
if postedHashOfSessionId != hashOfSessionId {
app.setFlash(responseWriter, session, "error", "anti-CSRF validation failed\n")
http.Redirect(responseWriter, request, "/admin", http.StatusFound)
return
}
action := request.PostFormValue("action")
if action == "delete-from-db" {
instance := request.PostFormValue("instance")
split := strings.Split(instance, "-")
if len(split) != 2 {
(*session.Flash)["error"] += fmt.Sprintf("invalid instance '%s'. expected <provider>-<provider_id>\n", instance)
} else {
err := app.Model.DeleteVPSInstance(split[0], split[1])
if err != nil {
app.unhandledError(responseWriter, err)
return
}
}
} else if action == "rebalance" {
app.setFlash(responseWriter, session, "info", "rebalance has been kicked off in the background\n")
go (func() {
log.Println("Starting backendApp.Rebalance()")
completed, err := app.Backend.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 := app.Backend.Rebalance()
if err != nil {
log.Printf("Rebalance failed: %+v\n", err)
}
}
})()
} else {
app.setFlash(responseWriter, session, "error", fmt.Sprintf("Unknown action '%s'\n", action))
}
http.Redirect(responseWriter, request, "/admin", http.StatusFound)
return
}
validVpsInstances, dbOnlyInstances, cloudOnlyInstances, err := app.Backend.GetInstances()
if err != nil {
app.unhandledError(responseWriter, err)
return
}
healthStatus := app.Backend.HealthcheckInstances(validVpsInstances)
failedHealthcheckInstanceIds := app.Backend.HealthcheckInstances(validVpsInstances)
type TenantDisplay struct {
Name string
Color string
}
type VPSInstanceForDisplay struct {
VPSInstance
CurrentTenants []TenantDisplay
PinnedTenants []TenantDisplay
ThermometerReal1 int
ThermometerReal2 int
ThermometerProjected1 int
ThermometerProjected2 int
Healthy string
}
for _, id := range failedHealthcheckInstanceIds {
validVpsInstances[id].Unhealthy = true
display := map[string]*VPSInstanceForDisplay{}
for k, v := range validVpsInstances {
healthy := "healthy"
if !healthStatus[k] {
healthy = "unhealthy"
}
display[k] = &VPSInstanceForDisplay{
VPSInstance: *v,
CurrentTenants: []TenantDisplay{},
PinnedTenants: []TenantDisplay{},
ThermometerReal1: 0,
ThermometerReal2: 0,
ThermometerProjected1: 0,
ThermometerProjected2: 0,
Healthy: healthy,
}
}
data := struct {
ValidVPSInstances map[string]*VPSInstance
ValidVPSInstances map[string]*VPSInstanceForDisplay
DBOnlyVPSInstances map[string]*VPSInstance
CloudOnlyVPSInstances map[string]*VPSInstance
HashOfSessionId string
}{
ValidVPSInstances: validVpsInstances,
ValidVPSInstances: display,
DBOnlyVPSInstances: dbOnlyInstances,
CloudOnlyVPSInstances: cloudOnlyInstances,
HashOfSessionId: hashOfSessionId,
}
app.buildPageFromTemplate(responseWriter, session, "admin.html", data)
@ -394,7 +479,7 @@ func (app *FrontendApp) getSession(request *http.Request, domain string) (Sessio
Flash: &(map[string]string{}),
}
for _, cookie := range request.Cookies() {
log.Printf("getSession %t: %s: %s\n", toReturn.SessionId == "", cookie.Name, cookie.Value)
//log.Printf("getSession %t: %s: %s\n", toReturn.SessionId == "", cookie.Name, cookie.Value)
if cookie.Name == "sessionId" || (cookie.Name == "sessionIdLax" && toReturn.SessionId == "") {
app.SessionCacheMutex.Lock()
session, hasSession := app.SessionCache[cookie.Value]
@ -432,7 +517,7 @@ func (app *FrontendApp) getSession(request *http.Request, domain string) (Sessio
toReturn.LaxCookie = session.LaxCookie
toReturn.Expires = session.Expires
}
log.Printf("toReturn.SessionId %s\n", toReturn.SessionId)
//log.Printf("toReturn.SessionId %s\n", toReturn.SessionId)
} else if cookie.Name == "flash" && cookie.Value != "" {
bytes, err := base64.RawURLEncoding.DecodeString(cookie.Value)
if err != nil {


+ 84
- 2
frontend/admin.gotemplate.html View File

@ -1,5 +1,87 @@
<div class="box">
<h2>administration</h2>
<div></div>
<div class="horizontal flip space-around instances">
<div class="box admin-box">
<h4>actions</h4>
<ul>
<li>
<form method="POST" action="#">
<input type="hidden" name="hashOfSessionId" value="{{ .HashOfSessionId }}"/>
<input type="hidden" name="action" value="rebalance"/>
<input type="submit" value="rebalance"/>
</form>
</li>
</ul>
</div>
<div class="box admin-box">
<h4>only in database</h4>
<ul>
{{range $k, $v := .DBOnlyVPSInstances}}
<li>{{ $k }} ({{ $v.IPV4 }})
<form method="POST" action="#">
<input type="hidden" name="hashOfSessionId" value=".HashOfSessionId"/>
<input type="hidden" name="action" value="delete-from-db"/>
<input type="hidden" name="instance" value="{{ $k }}"/>
<input class="delete-button" type="submit" value="DELETE"/>
</form>
</li>
{{ end }}
</ul>
</div>
<div>
<div class="box admin-box">
<h4>only in cloud provider</h4>
<ul>
{{range $k, $v := .CloudOnlyVPSInstances}}
<li>{{ $k }} ({{ $v.IPV4 }})
<form method="POST" action="#">
<input type="hidden" name="hashOfSessionId" value=".HashOfSessionId"/>
<input type="hidden" name="action" value="delete-from-cloud"/>
<input type="hidden" name="instance" value="{{ $k }}"/>
<input class="delete-button" type="submit" value="DELETE"/>
</form>
</li>
{{ end }}
</ul>
</div>
</div>
<div class="instance-group">
<h4>valid</h4>
{{range $k, $v := .ValidVPSInstances}}
<div class="instance {{ $v.Healthy }}">
<div class="instance-name">{{ $k }}</div>
<div class="instance-ip">{{ $v.IPV4 }}</div>
<div class="instance-content">
<div class="current-tenants">
{{range $tenant := $v.CurrentTenants}}
<span class="tenant" style="color: {{ $tenant.Color }};">
{{ $tenant.Name }}
</span>
{{end}}
</div>
<div class="pinned-tenants">
{{range $tenant := $v.PinnedTenants}}
<span class="tenant" style="color: {{ $tenant.Color }};">
{{ $tenant.Name }}
</span>
{{end}}
</div>
<div class="thermometer real">
<div class="marker real-1" style="background-color: cyan; min-height: {{ $v.ThermometerReal1 }}%;"></div>
<div class="marker real-2" style="background-color: orange; min-height: {{ $v.ThermometerReal2 }}%;"></div>
</div>
<div class="thermometer projected">
<div class="marker projected-1" style="background-color: green; min-height: {{ $v.ThermometerProjected1 }}%;"></div>
<div class="marker projected-2" style="background-color: orange; min-height: {{ $v.ThermometerProjected2 }}%;"></div>
</div>
</div>
</div>
{{end}}
</div>
</div>

+ 127
- 0
frontend/static/greenhouse.css View File

@ -391,3 +391,130 @@ input[type="radio"].tab:checked+label {
align-self: baseline;
}
.admin-box {
border-radius: 0.5rem;
min-width: 11rem;
padding: 1rem;
padding-bottom: 0rem;
box-shadow: 0 0.5rem 1rem 0 #00000070;
border: 2px solid #ccc;
}
.admin-box form {
display: inline;
}
.instance-group {
min-height: 100px;
width: 100%;
border: 3px dashed rgb(173, 230, 109);
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-start;
align-content: flex-start;
}
.instance-group>h4,
.instance-group>h3,
.instance-group>h5 {
width: 100%;
text-align: center;
}
.instance {
width: 100px;
height: 120px;
margin: 10px;
padding: 10px;
background-color: rgb(102, 241, 176);
border: 1px solid rgb(56, 161, 165);
border-radius: 1em;
display: flex;
flex-direction: column;
box-sizing: content-box;
}
.instance-name,
.instance-ip {
background-color: white;
color: #333;
margin-bottom: 5px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
}
.instance-content {
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-grow: 1;
box-sizing: content-box;
max-height: calc(100% - 40px);
}
.current-tenants, .pinned-tenants {
display: inline-flex;
writing-mode: vertical-lr;
flex-wrap: wrap;
align-content: flex-start;
margin-right: 5px;
padding: 2px;
min-width: 10px;
min-height: 100%;
}
.current-tenants {
border: 1px solid greenyellow;
}
.pinned-tenants {
border: 1px dashed gray;
}
.tenant {
writing-mode: horizontal-tb;
color: white;
border-radius: 5px;
padding: 3px;
font-size: 10px;
}
.thermometer {
background-color: gray;
min-height: 100%;
width: 10px;
padding: 2px;
margin-right: 5px;
display: flex;
flex-direction: column-reverse;
}
.thermometer.projected {
margin-right: 0;
}
.marker {
display: block;
width: 8px;
}
.delete-button {
border: 2px solid red;
color: #400;
background-color: #fcc;
border-radius: 4px;
outline: 0;
padding: 0.2em;
}

+ 2
- 14
main.go View File

@ -56,12 +56,12 @@ func main() {
panic(err)
}
pkiService := NewPKIService(config, model)
backendApp := initBackend(workingDirectory, config, pkiService, model, emailService)
frontendApp := initFrontend(workingDirectory, config, model, backendApp, emailService)
pkiService := NewPKIService(config, model)
go (func(backendApp *BackendApp) {
defer (func() {
@ -70,18 +70,6 @@ func main() {
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


+ 8
- 4
pki_service.go View File

@ -8,6 +8,7 @@ import (
"encoding/pem"
"errors"
"math/big"
"net"
"time"
easypkiCertificate "git.sequentialread.com/forest/easypki.git/pkg/certificate"
@ -37,10 +38,11 @@ func NewPKIService(config *Config, db *DBModel) *PKIService {
func (service *PKIService) GetTLSCertificate(
caName string,
subject string,
ipAddresses []net.IP,
expiry time.Time,
) (tls.Certificate, error) {
key, cert, err := service.GetKeyPair(caName, subject, expiry)
key, cert, err := service.GetKeyPair(caName, subject, ipAddresses, expiry)
if err != nil {
return tls.Certificate{}, err
}
@ -60,6 +62,7 @@ func (service *PKIService) GetTLSCertificate(
func (service *PKIService) GetKeyPair(
caName string,
subject string,
ipAddresses []net.IP,
expiry time.Time,
) (*rsa.PrivateKey, *x509.Certificate, error) {
@ -75,9 +78,10 @@ func (service *PKIService) GetKeyPair(
&easypki.Request{
Name: subject,
Template: &x509.Certificate{
NotAfter: expiry,
IsCA: false,
Subject: getSubject(subject),
NotAfter: expiry,
IsCA: false,
Subject: getSubject(subject),
IPAddresses: ipAddresses,
},
},
)


+ 1
- 1
schema_versions/02_up_create_tenants_etc.sql View File

@ -41,7 +41,7 @@ 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 NUMERIC NOT NULL,
bytes NUMERIC NOT NULL DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT FALSE,
deactivated_at TIMESTAMP NULL,
CONSTRAINT vps_instance


+ 4
- 3
threshold_provisioning_service.go View File

@ -4,6 +4,7 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"strings"
"time"
@ -21,7 +22,7 @@ func NewThresholdProvisioningService(config *Config, pkiService *PKIService) *Th
}
}
func (service *ThresholdProvisioningService) GetMultitenantInstallScript(tlsCertificateSubject string) (string, error) {
func (service *ThresholdProvisioningService) GetMultitenantInstallScript(tlsCertificateSubject string, tlsCertificateIps []net.IP) (string, error) {
installScript := `#!/bin/bash
@ -182,7 +183,7 @@ WantedBy=multi-user.target
// this way the cert will be unique to each instance and its harder to steal/exploit
// this would require either DNS or manual validation by the clients.
expiry := time.Now().Add(time.Hour * time.Duration(24*31*12*99))
thresholdKey, thresholdCert, err := service.PKI.GetKeyPair(mainCAName, tlsCertificateSubject, expiry)
thresholdKey, thresholdCert, err := service.PKI.GetKeyPair(mainCAName, tlsCertificateSubject, tlsCertificateIps, expiry)
thresholdKeyBytes := pem.EncodeToMemory(&pem.Block{
Bytes: x509.MarshalPKCS1PrivateKey(thresholdKey),
@ -196,7 +197,7 @@ WantedBy=multi-user.target
substitutions := map[string]string{
"ARTIFACTS_BASE_URL": "https://f000.backblazeb2.com/file/server-garden-artifacts",
"ARCH": "amd64",
"TAR_SHA256": "796573e206f0c50c603249d2181111323e7009f20fac6afcace1d62aa24c574a",
"TAR_SHA256": "ef4a019e81106e7591fff39f918a04d666141a0a65f70616de6edd90b6e0ca05",
"THRESHOLD_DOMAIN": "greenhouse.server.garden",
"GREENHOUSE_MANAGEMENT_API_AUTH_CA": string(managementAPIAuthCABytes),
"GREENHOUSE_CA": string(mainCABytes),


Loading…
Cancel
Save