Browse Source

docker deployment and telemetry

main
forest 3 years ago
parent
commit
53b21090f8
  1. 8
      .dockerignore
  2. 2
      .gitignore
  3. 44
      Dockerfile
  4. 41
      build-docker.sh
  5. 497
      docker-compose.yml
  6. 34
      example_config.json
  7. 90
      frontend.go
  8. 12
      frontend/alpha-profile.gotemplate.html
  9. 55
      frontend/static/image-sources/generate_webp.sh
  10. 4
      frontend/static/install.sh
  11. 12
      frontend_admin_panel.go
  12. 9
      frontend_howto.go
  13. 106
      frontend_login.go
  14. 66
      frontend_profile.go
  15. 4
      gandi_service.go
  16. 2
      ingress_service.go
  17. 73
      main.go
  18. 10
      public_api.go
  19. 12
      scheduled_tasks.go
  20. 64
      telemetry.go
  21. 9
      threshold/src_remote_install.sh
  22. 9
      threshold_provisioning_service.go

8
.dockerignore

@ -0,0 +1,8 @@
threshold
releases
postgres-data
greenhouse-daemon
dockerbuild
threshold
readme
postgres-data

2
.gitignore vendored

@ -24,3 +24,5 @@ greenhouse-daemon/daemon.log
greenhouse-daemon/old
greenhouse-daemon/old2
greenhouse-daemon/old3
dockerbuild
postgres-data

44
Dockerfile

@ -0,0 +1,44 @@
FROM golang:1.15.2-alpine as build
ARG GOARCH=
ARG GO_BUILD_ARGS=
RUN mkdir /build
WORKDIR /build
COPY . .
RUN go build -v $GO_BUILD_ARGS -o /build/greenhouse .
FROM alpine
WORKDIR /greenhouse
RUN apk add --no-cache openssh-client
COPY --from=build /build/greenhouse /greenhouse/greenhouse
# for now I think this will be set up manually.
# RUN mkdir -p /greenhouse/greenhouse-daemon && \
# curl -sS -o "daemon.gz" "https://picopublish.sequentialread.com/files/greenhouse-daemon-alpha-rc0-315e67e-82d8-linux-$GOARCH.gz" && \
# gzip --stdout --decompress "daemon.gz" > "/greenhouse/greenhouse-daemon/greenhouse-daemon" && \
# rm "daemon.gz" && chmod +x "/greenhouse/greenhouse-daemon/greenhouse-daemon" && \
# curl -sS -o "threshold.gz" "https://picopublish.sequentialread.com/files/threshold-0.0.0-6cfcabd-f27e-linux-$GOARCH.gz" && \
# gzip --stdout --decompress "threshold.gz" > "/greenhouse/greenhouse-daemon/greenhouse-threshold" && \
# rm "threshold.gz" && chmod +x "/greenhouse/greenhouse-daemon/greenhouse-threshold" && \
# curl -sS -o "caddy.gz" "https://picopublish.sequentialread.com/files/caddy-v2.4.0-beta.2-forest-078f12e0-b4a8-linux-$GOARCH.gz" && \
# gzip --stdout --decompress "caddy.gz" > "/greenhouse/greenhouse-daemon/greenhouse-caddy" && \
# rm "caddy.gz" && chmod +x "/greenhouse/greenhouse-daemon/greenhouse-caddy" && \
# echo '{
# "admin": {
# "disabled": false,
# "listen": "unix///var/run/greenhouse-daemon-caddy-admin.sock",
# "config": {
# "persist": false
# }
# }
# }' > /greenhouse/greenhouse-daemon/caddy-config.json
COPY ./example_config.json /greenhouse/config.json
COPY ./frontend /greenhouse/frontend
COPY ./schema_versions /greenhouse/schema_versions
ENTRYPOINT ["/greenhouse/greenhouse"]

41
build-docker.sh

@ -0,0 +1,41 @@
#!/bin/bash -e
VERSION="0.0.6"
rm -rf dockerbuild || true
mkdir dockerbuild
#cp Dockerfile dockerbuild/Dockerfile-amd64
cp Dockerfile dockerbuild/Dockerfile-arm
#cp Dockerfile dockerbuild/Dockerfile-arm64
#sed -E 's|FROM alpine|FROM amd64/alpine|' -i dockerbuild/Dockerfile-amd64
sed -E 's|FROM alpine|FROM arm32v7/alpine|' -i dockerbuild/Dockerfile-arm
#sed -E 's|FROM alpine|FROM arm64v8/alpine|' -i dockerbuild/Dockerfile-arm64
#sed -E 's/GOARCH=/GOARCH=amd64/' -i dockerbuild/Dockerfile-amd64
sed -E 's/GOARCH=/GOARCH=arm/' -i dockerbuild/Dockerfile-arm
#sed -E 's/GOARCH=/GOARCH=arm64/' -i dockerbuild/Dockerfile-arm64
#docker build -f dockerbuild/Dockerfile-amd64 -t sequentialread/greenhouse:$VERSION-amd64 .
docker build -f dockerbuild/Dockerfile-arm -t sequentialread/greenhouse:$VERSION-arm .
#docker build -f dockerbuild/Dockerfile-arm64 -t sequentialread/greenhouse:$VERSION-arm64 .
#docker push sequentialread/greenhouse:$VERSION-amd64
docker push sequentialread/greenhouse:$VERSION-arm
#docker push sequentialread/greenhouse:$VERSION-arm64
export DOCKER_CLI_EXPERIMENTAL=enabled
# docker manifest create sequentialread/greenhouse:$VERSION \
# sequentialread/greenhouse:$VERSION-amd64 \
# sequentialread/greenhouse:$VERSION-arm \
# sequentialread/greenhouse:$VERSION-arm64
docker manifest create sequentialread/greenhouse:$VERSION sequentialread/greenhouse:$VERSION-arm \
#docker manifest annotate --arch amd64 sequentialread/greenhouse:$VERSION sequentialread/greenhouse:$VERSION-amd64
docker manifest annotate --arch arm sequentialread/greenhouse:$VERSION sequentialread/greenhouse:$VERSION-arm
#docker manifest annotate --arch arm64 sequentialread/greenhouse:$VERSION sequentialread/greenhouse:$VERSION-arm64
docker manifest push sequentialread/greenhouse:$VERSION

497
docker-compose.yml

@ -1,497 +0,0 @@
version: "3.3"
services:
caddy:
image: caddy:2.3.0
networks:
- blog
- internet-access
- sequentialread
- gitea
ports:
- "80:80"
- "443:443"
command: ["/bin/sh", "-c", "rm -f /caddysocket/caddy.sock && caddy run -config /config/config.json"]
volumes:
- type: bind
source: ./caddy/static
target: /srv/static
read_only: true
- type: bind
source: ./caddy/config
target: /config
read_only: true
- type: bind
source: ./caddy/data
target: /data
- type: bind
source: ./caddy/log
target: /var/log
- type: volume
source: caddy-socket-volume
target: /caddysocket
caddy-config:
image: sequentialread/caddy-config:0.0.13
#mem_limit: 50m
networks:
- sequentialread
volumes:
- type: bind
source: ./security-gateway/
target: /dockersocket/
- type: volume
source: caddy-socket-volume
target: /caddysocket
environment:
- DOCKER_SOCKET=/dockersocket/docker.sock
- CADDY_ACME_DOMAINS_CSV=*.sequentialread.com
- CADDY_ACME_ISSUER_URL=https://acme-v02.api.letsencrypt.org/directory
- CADDY_ACME_CLIENT_EMAIL_ADDRESS=forest.n.johnson@gmail.com
docker-api-security-gateway:
image: sequentialread/docker-api-security-gateway:0.0.16
#mem_limit: 50m
userns_mode: "host"
volumes:
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
read_only: true
- type: bind
source: ./security-gateway/
target: /var/run/docker-api-security-gateway/
environment:
DOCKER_API_PROXY_DEBUG: "false"
DOCKER_API_PROXY_ALLOW_0_METHODREGEX: ^GET$$
DOCKER_API_PROXY_ALLOW_0_URIREGEX: ^/info(\?)?$$
DOCKER_API_PROXY_ALLOW_1_METHODREGEX: ^GET$$
DOCKER_API_PROXY_ALLOW_1_URIREGEX: ^/containers/json(\?)?((filters=%7B%22status%22%3A%5B%22running%22%5D%7D&)?limit=0)?$$
DOCKER_API_PROXY_ALLOW_2_METHODREGEX: ^GET$$
DOCKER_API_PROXY_ALLOW_2_URIREGEX: ^/containers/[a-f0-9]+/stats(\?)?(stream=0)?$$
DOCKER_API_PROXY_ALLOW_3_METHODREGEX: ^GET$$
DOCKER_API_PROXY_ALLOW_3_URIREGEX: ^/containers/[a-f0-9]+/json(\?)?$$
DOCKER_API_PROXY_RESULTFILTERS_0_METHODREGEX: .*
DOCKER_API_PROXY_RESULTFILTERS_0_URIREGEX: ^/containers/[a-f0-9]+/json(\?)?$$
DOCKER_API_PROXY_RESULTFILTERS_0_BLACKLIST_0_PATH: .Config.Env
DOCKER_API_PROXY_HTTP_LISTENUNIXSOCKET: /var/run/docker-api-security-gateway/docker.sock
DOCKER_API_PROXY_HTTP_LISTENUNIXSOCKETUID: 165536
DOCKER_API_PROXY_HTTP_LISTENUNIXSOCKETGID: 165536
DOCKER_HOST: unix:///var/run/docker.sock
DOCKER_API_VERSION: '1.40'
goatcounter:
image: sequentialread/goatcounter:1.4.2-9
restart: always
entrypoint: ["/bin/sh"]
command: ["-c", "/app/goatcounter migrate -createdb all && /app/goatcounter serve -tls none -listen '*:8080'"]
networks:
sequentialread:
ipv4_address: ${SEQUENTIALREAD_NETWORK_GOATCOUNTER_IPV4}
volumes:
- type: bind
source: ./goatcounter/db
target: /app/db
labels:
sequentialread-8080-public-port: 443
sequentialread-8080-public-protocol: https
sequentialread-8080-public-hostnames: "goatcounter.sequentialread.com,goatcounter.beta.sequentialread.com"
sequentialread-8080-container-protocol: http
goatcounter-log-publisher:
image: sequentialread/goatcounter:1.4.2-34
restart: always
entrypoint: ["/bin/sh"]
command: ["-c", "tail -F /caddylog/caddy-goatcounter.log | ./goatcounter-caddy-log-adapter | ./goatcounter import -site http://goatcounter.sequentialread.com:8080 -format combined-vhost -- -"]
extra_hosts:
- "goatcounter.beta.sequentialread.com:${SEQUENTIALREAD_NETWORK_GOATCOUNTER_IPV4}"
- "goatcounter.sequentialread.com:${SEQUENTIALREAD_NETWORK_GOATCOUNTER_IPV4}"
volumes:
- type: bind
source: ./goatcounter/db
target: /app/db
- type: bind
source: ./caddy/log
target: /caddylog
networks:
- sequentialread
environment:
- GOATCOUNTER_API_KEY=${GOATCOUNTER_API_KEY}
- LOGADAPTER_INCLUDESUCCESSORFAILUREINKEY=false
- LOGADAPTER_DEBUG=false
- LOGADAPTER_ALWAYSINCLUDEURISCSV=,
- LOGADAPTER_BLACKLISTIPSCSV=192.168.0.1,192.168.0.46,71.34.24.79
- LOGADAPTER_DOMAINS_0_MATCHHOSTNAMEREGEX=^(www\.)?((grafana|git|stream|pwm|captcha|comments)\.)?(beta\.)?sequentialread.com
- LOGADAPTER_DOMAINS_0_CONTENTTYPEWHITELISTREGEX=[^/]+/html
- LOGADAPTER_DOMAINS_1_MATCHHOSTNAMEREGEX=(goatcounter|influxdb)
- LOGADAPTER_DOMAINS_1_CONTENTTYPEWHITELISTREGEX=DROP_ALL
influxdb:
image: influxdb:1.8.4
restart: always
networks:
- sequentialread
volumes:
- type: bind
source: ./influxdb/data/
target: /var/lib/influxdb
environment:
- INFLUXDB_REPORTING_DISABLED=true
- INFLUXDB_HTTP_AUTH_ENABLED=true
- INFLUXDB_HTTP_PPROF_AUTH_ENABLED=true
- INFLUXDB_HTTP_DEBUG_PPROF_ENABLED=true
labels:
sequentialread-8086-public-port: 443
sequentialread-8086-public-protocol: https
sequentialread-8086-public-hostnames: "influxdb.sequentialread.com,influxdb.beta.sequentialread.com"
sequentialread-8086-container-protocol: http
telegraf:
image: telegraf:1.17.3
restart: always
# in order to get telegraf to report the host's network io correctly,
# it has to run on the host network... and the only way to do that
# would be to disable the user namespace remap. so we manually set it to the
# remapped UID and GID
userns_mode: "host"
network_mode: "host"
user: 165536:165536
# now that we are on the hosts network, in order to talk to influx we have to go in through the
# front door: Caddy, which is listening on 80 and 443 on localhost.
extra_hosts:
- "influxdb.beta.sequentialread.com:127.0.0.1"
- "influxdb.sequentialread.com:127.0.0.1"
entrypoint: /bin/sh
# this runs telegraf and then ends the container process when telegraf logs "Cannot connect to the Docker daemon"
# Just a simple way to forcing it to restart when the docker daemon socket file gets pulled out from under it
command:
- "-c"
- |
echo '' > t.log
sh -c '/entrypoint.sh --config /telegrafconfig/telegraf.conf 2>&1 | tee t.log' &
tpid=$$?
tail -f t.log | grep -q 'Cannot connect to the Docker daemon'
kill $$tpid
volumes:
- type: bind
source: ./security-gateway/
target: /dockersocket/
- type: bind
source: /
target: /hostfs
read_only: true
- type: bind
source: ./telegraf
target: /telegrafconfig
read_only: true
depends_on:
- docker-api-security-gateway
environment:
- HOST_MOUNT_PREFIX=/hostfs
- HOST_PROC=/hostfs/proc
- TELEGRAF_DOCKER_SOCKET=/dockersocket/docker.sock
- TELEGRAF_INFLUX_PASSWORD=${TELEGRAF_INFLUX_PASSWORD}
- TELEGRAF_INFLUX_URL=https://influxdb.sequentialread.com
grafana:
image: grafana/grafana:7.4.3
# The Grafana docker container is missing the /etc/nsswitch.conf file which
# many things rely on in order to know how to properly look up domain names.
# See: https://github.com/gliderlabs/docker-alpine/issues/367
# without this file, grafana will only use the dns server to resolve names,
# it won't look at the /etc/hosts file at all.
# Since grafana normally runs as a separate user, we have to override the container to run as root 1st
# so it can write the nsswitch.conf file, then run the grafana start script as the grafana user.
user: '0'
entrypoint: /bin/sh
command: ["-c", "echo 'hosts: files dns' > /etc/nsswitch.conf && su -s '/bin/sh' -c '/run.sh' grafana"]
extra_hosts:
- "smtp.gmail.com:${SEQUENTIALREAD_NETWORK_EXTERNAL_SERVICE_IPV4}"
networks:
- sequentialread
volumes:
- type: bind
source: ./grafana/data/
target: /var/lib/grafana
environment:
GF_SERVER_ROOT_URL: 'https://grafana.sequentialread.com'
GF_SERVER_ENABLE_GZIP: 'true'
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
GF_SECURITY_DISABLE_GRAVATAR: 'true'
GF_SECURITY_COOKIE_SECURE: 'true'
GF_SECURITY_SECRET_KEY: ${GRAFANA_SECRET_KEY}
GF_AUTH_ANONYMOUS_ENABLED: 'true'
GF_AUTH_ANONYMOUS_ORG_NAME: sequentialread
GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer
GF_SMTP_ENABLED: 'true'
GF_SMTP_HOST: 'smtp.gmail.com:465'
GF_SMTP_PASSWORD: ${GMAIL_PASSWORD}
GF_SMTP_USER: gitlab.sequentialread.com@gmail.com
GF_SMTP_FROM_ADDRESS: gitlab.sequentialread.com@gmail.com
GF_SMTP_STARTTLS_POLICY: MandatoryStartTLS
labels:
sequentialread-3000-public-port: 443
sequentialread-3000-public-protocol: https
sequentialread-3000-public-hostnames: "grafana.sequentialread.com,grafana.beta.sequentialread.com"
sequentialread-3000-container-protocol: http
sequentialread-external-service:
image: sequentialread/external-service:0.0.14
restart: always
networks:
sequentialread:
ipv4_address: ${SEQUENTIALREAD_NETWORK_EXTERNAL_SERVICE_IPV4}
internet-access:
environment:
DEBUG_LOG: 'true'
SERVICE_0_LISTEN: ':465'
SERVICE_0_DIAL: 'smtp.gmail.com:465'
SERVICE_1_LISTEN: ':8080'
SERVICE_1_DIAL: '192.168.0.46:8080'
pwm:
image: sequentialread/sequentialread-password-manager:2.0.6
restart: always
networks:
- sequentialread
environment:
- SEQUENTIALREAD_PWM_BACKBLAZE_BUCKET_NAME=sequentialread-password-manager
- SEQUENTIALREAD_PWM_BACKBLAZE_BUCKET_REGION=us-west-000
- SEQUENTIALREAD_PWM_BACKBLAZE_ACCESS_KEY_ID=0003ea77f5997840000000015
- SEQUENTIALREAD_PWM_BACKBLAZE_SECRET_ACCESS_KEY=${PWM_BACKBLAZE_SECRET_ACCESS_KEY}
volumes:
- type: bind
source: ./pwm/data/
target: /app/data/
labels:
sequentialread-8073-public-port: 443
sequentialread-8073-public-protocol: https
sequentialread-8073-public-hostnames: "pwm.sequentialread.com,pwm.beta.sequentialread.com"
sequentialread-8073-container-protocol: http
picopublish:
image: sequentialread/picopublish:0.1.7
restart: always
networks:
- sequentialread
environment:
- PICO_PUBLISH_PASSWORD=${PICOPUBLISH_PASSWORD}
volumes:
- type: bind
source: ./picopublish/data
target: /app/data
labels:
sequentialread-8080-public-port: 443
sequentialread-8080-public-protocol: https
sequentialread-8080-public-hostnames: "picopublish.sequentialread.com,picopublish.beta.sequentialread.com"
sequentialread-8080-container-protocol: http
webclip:
image: sequentialread/webclip:0.0.2
restart: always
networks:
- sequentialread
labels:
sequentialread-8080-public-port: 443
sequentialread-8080-public-protocol: https
sequentialread-8080-public-hostnames: "webclip.sequentialread.com,webclip.beta.sequentialread.com"
sequentialread-8080-container-protocol: http
gitea:
image: sequentialread/gitea-armv7:1.13.2
restart: always
mem_limit: 600m
ports:
- "10022:10022"
networks:
- gitea
volumes:
- type: bind
source: ./gitea/data
target: /data
environment:
- APP_NAME='SequentialRead Git'
- UI_META_AUTHOR='Forest Johnson (SequentialRead)'
- UI_META_DESCRIPTION='Forest's self-hosted git server for sequentialread.com'
- SERVER_LANDING_PAGE='explore'
- SSH_PORT=10022
- SSH_LISTEN_PORT=10022
- ROOT_URL=https://git.beta.sequentialread.com
- DISABLE_REGISTRATION=true
- INSTALL_LOCK=true
- SECRET_KEY=${GITEA_SECRET_KEY}
- DB_TYPE=mysql
- DB_HOST=gitea-mariadb
- DB_NAME=gitea
- DB_USER=gitea
- DB_PASSWD=${GITEA_MARIADB_GITEA_PASSWORD}
depends_on:
- gitea-mariadb
labels:
sequentialread-3000-public-port: 443
sequentialread-3000-public-protocol: https
sequentialread-3000-public-hostnames: "www.git.sequentialread.com,git.sequentialread.com,git.beta.sequentialread.com"
sequentialread-3000-container-protocol: http
gitea-mariadb:
image: sequentialread/mariadb-armv7:10.3
restart: always
networks:
- gitea
volumes:
- type: bind
source: ./gitea-mariadb/
target: /var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${GITEA_MARIADB_ROOT_PASSWORD}
- MYSQL_DATABASE=gitea
- MYSQL_USER=gitea
- MYSQL_PASSWORD=${GITEA_MARIADB_GITEA_PASSWORD}
stream:
image: sequentialread/owncast-caching-proxy:0.0.18
networks:
- sequentialread
command: '${SEQUENTIALREAD_NETWORK_EXTERNAL_SERVICE_IPV4}:8080'
volumes:
- type: bind
source: ./owncast-caching-proxy/cache
target: /app/cache
environment:
DEBUG: 0
labels:
sequentialread-8080-public-port: 443
sequentialread-8080-public-protocol: https
sequentialread-8080-public-hostnames: "stream.sequentialread.com,stream.beta.sequentialread.com"
sequentialread-8080-container-protocol: http
ghost:
image: ghost:3.41-alpine
restart: always
extra_hosts:
- "smtp.gmail.com:${BLOG_NETWORK_EXTERNAL_SERVICE_IPV4}"
networks:
- blog
volumes:
- type: bind
source: ./ghost
target: /var/lib/ghost/content
environment:
- NODE_ENV=production
- url=https://sequentialread.com
- database__client=sqlite3
- database__connection__filename=content/data/ghost-prod.db
- database__useNullAsDefault=true
- database__debug=false
- mail__from=gitlab.sequentialread.com@gmail.com
- mail__transport=SMTP
- mail__options__host=smtp.gmail.com
- mail__options__port=465
- mail__options__auth__user=gitlab.sequentialread.com@gmail.com
- mail__options__auth__pass=${GMAIL_PASSWORD}
labels:
sequentialread-2368-public-port: 443
sequentialread-2368-public-protocol: https
sequentialread-2368-public-hostnames: "sequentialread.com,www.sequentialread.com,beta.sequentialread.com,www.beta.sequentialread.com"
sequentialread-2368-container-protocol: http
comments:
image: sequentialread/comments:0.1.45
restart: always
extra_hosts:
- "smtp.gmail.com:${BLOG_NETWORK_EXTERNAL_SERVICE_IPV4}"
- "captcha.sequentialread.com:${BLOG_NETWORK_EXTERNAL_SERVICE_IPV4}"
networks:
- blog
volumes:
- type: bind
source: ./comments
target: /app/data
environment:
- COMMENTS_LISTEN_PORT=8080
- COMMENTS_BASE_URL=https://comments.sequentialread.com
- COMMENTS_HASH_SALT=klnv5ii043nbkjz__g34nnk_34wgn26lqlwqb7841mf
- COMMENTS_CORS_ORIGINS=https://sequentialread.com,https://www.sequentialread.com,https://beta.sequentialread.com,https://www.beta.sequentialread.com
- COMMENTS_CAPTCHA_API_TOKEN=${CAPTCHA_API_TOKEN}
- COMMENTS_CAPTCHA_URL=https://captcha.sequentialread.com
- COMMENTS_CAPTCHA_DIFFICULTY_LEVEL=8
- COMMENTS_EMAIL_HOST=smtp.gmail.com
- COMMENTS_EMAIL_PORT=465
- COMMENTS_EMAIL_USER=gitlab.sequentialread.com@gmail.com
- COMMENTS_EMAIL_PASSWORD=${GMAIL_PASSWORD}
- COMMENTS_NOTIFICATION_TARGET=forest.n.johnson@gmail.com
- COMMENTS_ADMIN_PASSWORD=${COMMENTS_ADMIN_PASSWORD}
labels:
sequentialread-8080-public-port: 443
sequentialread-8080-public-protocol: https
sequentialread-8080-public-hostnames: "comments.sequentialread.com,comments.beta.sequentialread.com"
sequentialread-8080-container-protocol: http
blog-external-service:
image: sequentialread/external-service:0.0.14
restart: always
networks:
blog:
ipv4_address: ${BLOG_NETWORK_EXTERNAL_SERVICE_IPV4}
internet-access:
environment:
DEBUG_LOG: 'true'
SERVICE_0_LISTEN: ':465'
SERVICE_0_DIAL: 'smtp.gmail.com:465'
SERVICE_1_LISTEN: ':443'
SERVICE_1_DIAL: '172.17.0.1:443'
captcha:
image: sequentialread/pow-captcha:0.0.11
restart: always
networks:
- blog
volumes:
- type: bind
source: ./captcha/tokens
target: /app/PoW_Captcha_API_Tokens
environment:
- POW_CAPTCHA_ADMIN_API_TOKEN=${CAPTCHA_ADMIN_API_TOKEN}
labels:
sequentialread-2370-public-port: 443
sequentialread-2370-public-protocol: https
sequentialread-2370-public-hostnames: "captcha.sequentialread.com,captcha.beta.sequentialread.com"
sequentialread-2370-container-protocol: http
networks:
internet-access:
driver: bridge
ipam:
driver: default
config:
- subnet: "${INTERNET_ACCESS_NETWORK_CIDR}"
gitea:
driver: bridge
internal: true
ipam:
driver: default
config:
- subnet: "${GITEA_NETWORK_CIDR}"
sequentialread:
driver: bridge
internal: true
ipam:
driver: default
config:
- subnet: "${SEQUENTIALREAD_NETWORK_CIDR}"
blog:
driver: bridge
internal: true
ipam:
driver: default
config:
- subnet: "${BLOG_NETWORK_CIDR}"
volumes:
caddy-socket-volume:

34
example_config.json

@ -0,0 +1,34 @@
{
"FrontendPort": 8080,
"FrontendDomain": "greenhouse-alpha.server.garden",
"DatabaseConnectionString": "host=localhost port=5432 user=postgres password=dev database=postgres sslmode=disable",
"DatabaseType": "postgres",
"DatabaseSchema": "public",
"DigitalOceanAPIKey": "",
"DigitalOceanRegion": "tor1",
"DigitalOceanImage": "ubuntu-18-04-x64",
"DigitalOceanSSHAuthorizedKeys": [
{
"Name": "Forest's SSH Key",
"PublicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3XzZTbTteIgnaFY+fiiOl9EnNN+twyNchnWjCkYqv forest@tower"
},
{
"Name": "Greenhouse SSH Key",
"PublicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO4vgkLngsS8FprpDmCBQhFsIz/jcFhT3fD56nnbgdeM greenhouse@greenhouse"
}
],
"GandiAPIKey": "",
"SSHPrivateKeyFile": "/greenhouse/config/greenhouse_ed25519",
"BackblazeBucketName": "",
"BackblazeKeyId": "",
"BackblazeSecretKey": "",
"ThresholdManagementPort": 9057,
"ThresholdPort": 9056,
"SMTP": {
"Host": "",
"Port": 465,
"Username": "",
"Password": "",
"Encryption": "SMTPS"
}
}

90
frontend.go

@ -11,6 +11,7 @@ import (
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
@ -84,15 +85,15 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
app.handleWithSessionNotRequired("/", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
pageContent, err := app.renderTemplateToHTML("index.html", nil)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
highlightContent, err := app.renderTemplateToHTML("index-highlight.html", nil)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
app.buildPage(responseWriter, session, highlightContent, pageContent, "")
app.buildPage(responseWriter, request, session, highlightContent, pageContent, "")
})
registerHowtoRoutes(&app)
@ -109,6 +110,22 @@ func initFrontend(workingDirectory string, config *Config, model *DBModel, backe
log.Printf("serving static files from %s", staticFilesDir)
app.Router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticFilesDir))))
releasesDir := filepath.Join(workingDirectory, "releases")
log.Printf("serving releases from %s", releasesDir)
// https://stackoverflow.com/questions/49589685/good-way-to-disable-directory-listing-with-http-fileserver-in-go
noDirectoryListingHTTPDir := justFilesFilesystem{fs: http.Dir(releasesDir), readDirBatchSize: 20}
releasesStaticFileHandler := http.StripPrefix("/releases/", http.FileServer(noDirectoryListingHTTPDir))
app.Router.PathPrefix("/releases/").HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
if !strings.Contains(strings.ToLower(request.URL.Path), "greenhouse") {
http.Error(responseWriter, "404 not found", http.StatusNotFound)
}
go postTelemetryFromRequest("release_download", request, &app, request.URL.Path)
releasesStaticFileHandler.ServeHTTP(responseWriter, request)
})
return app
}
@ -248,8 +265,11 @@ func (app *FrontendApp) setSession(responseWriter http.ResponseWriter, session *
return nil
}
func (app *FrontendApp) unhandledError(responseWriter http.ResponseWriter, err error) {
func (app *FrontendApp) unhandledError(responseWriter http.ResponseWriter, request *http.Request, err error) {
log.Printf("500 internal server error: %+v\n", err)
go postTelemetryFromRequest("unhandled-error", request, app, fmt.Sprintf("%s: %s", request.URL.Path, err))
responseWriter.Header().Add("Content-Type", "text/plain")
responseWriter.WriteHeader(http.StatusInternalServerError)
responseWriter.Write([]byte("500 internal server error"))
@ -279,7 +299,7 @@ func (app *FrontendApp) handleWithSessionImpl(path string, required bool, requir
log.Printf("handleWithSession(): %s\n", string(bytes))
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
} else {
log.Printf("%d, %d, %t, %t", requireUserId, session.TenantId, requireUserId != 0, session.TenantId != requireUserId)
if (required && session.TenantId == 0) || (requireUserId != 0 && session.TenantId != requireUserId) {
@ -305,7 +325,7 @@ func (app *FrontendApp) handleWithSessionImpl(path string, required bool, requir
})
}
func (app *FrontendApp) buildPage(responseWriter http.ResponseWriter, session Session, highlight, page template.HTML, pageClass string) {
func (app *FrontendApp) buildPage(responseWriter http.ResponseWriter, request *http.Request, session Session, highlight, page template.HTML, pageClass string) {
var buffer bytes.Buffer
templateName := "page.html"
pageTemplate, hasPageTemplate := app.HTMLTemplates[templateName]
@ -324,7 +344,7 @@ func (app *FrontendApp) buildPage(responseWriter http.ResponseWriter, session Se
app.deleteCookie(responseWriter, "flash")
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
} else {
io.Copy(responseWriter, &buffer)
}
@ -343,16 +363,16 @@ func (app *FrontendApp) renderTemplateToHTML(templateName string, data interface
return template.HTML(buffer.String()), nil
}
func (app *FrontendApp) buildPageFromTemplate(responseWriter http.ResponseWriter, session Session, templateName string, data interface{}) {
app.buildPageFromTemplateWithClass(responseWriter, session, templateName, data, "")
func (app *FrontendApp) buildPageFromTemplate(responseWriter http.ResponseWriter, request *http.Request, session Session, templateName string, data interface{}) {
app.buildPageFromTemplateWithClass(responseWriter, request, session, templateName, data, "")
}
func (app *FrontendApp) buildPageFromTemplateWithClass(responseWriter http.ResponseWriter, session Session, templateName string, data interface{}, pageClass string) {
func (app *FrontendApp) buildPageFromTemplateWithClass(responseWriter http.ResponseWriter, request *http.Request, session Session, templateName string, data interface{}, pageClass string) {
content, err := app.renderTemplateToHTML(templateName, data)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
} else {
app.buildPage(responseWriter, session, template.HTML(""), content, pageClass)
app.buildPage(responseWriter, request, session, template.HTML(""), content, pageClass)
}
}
@ -396,6 +416,52 @@ func (app *FrontendApp) reloadTemplates() {
}
type justFilesFilesystem struct {
fs http.FileSystem
// readDirBatchSize - configuration parameter for `Readdir` func
readDirBatchSize int
}
func (fs justFilesFilesystem) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
}
return neuteredStatFile{File: f, readDirBatchSize: fs.readDirBatchSize}, nil
}
type neuteredStatFile struct {
http.File
readDirBatchSize int
}
func (e neuteredStatFile) Stat() (os.FileInfo, error) {
s, err := e.File.Stat()
if err != nil {
return nil, err
}
if s.IsDir() {
LOOP:
for {
fl, err := e.File.Readdir(e.readDirBatchSize)
switch err {
case io.EOF:
break LOOP
case nil:
for _, f := range fl {
if f.Name() == "index.html" {
return s, err
}
}
default:
return nil, err
}
}
return nil, os.ErrNotExist
}
return s, err
}
// func hashTemplateAndStaticFiles(workingDirectory string) string {
// filenameMatch := regexp.MustCompile("(\\.gotemplate)|(\\.html)|(\\.css)|(\\.js)$")
// toHash := map[string]bool{}

12
frontend/alpha-profile.gotemplate.html

@ -110,7 +110,7 @@
<input type="radio" name="client" value="windows" id="client-windows" {{ if eq .OperatingSystem "windows" }}checked="checked"{{ end }}></input>
<label class="tab" for="client-windows">
<div class="horizontal align-center">
<img class="os-image" src="static/images/windows.webp"/> windows
<img class="os-image" src="/static/images/windows.webp"/> windows
</div>
</label>
<div class="vertical tab-content">
@ -118,8 +118,8 @@
You may download our installer:
</p>
<blockquote>
<a class="horizontal align-center" href="https://picopublish.sequentialread.com/files/greenhouse-setup-alpha-rc0.exe">
<img class="os-image" src="static/images/windows-installer.webp"/> greenhouse-setup-alpha-rc0.exe
<a class="horizontal align-center" href="/releases/greenhouse-setup-alpha-rc0.exe">
<img class="os-image" src="/static/images/windows-installer.webp"/> greenhouse-setup-alpha-rc0.exe
</a>
</blockquote>
<br/>
@ -181,7 +181,7 @@
<input type="radio" name="client" value="macos" id="client-macos" {{ if eq .OperatingSystem "macos" }}checked="checked"{{ end }}></input>
<label class="tab" for="client-macos">
<div class="horizontal align-center">
<img class="os-image" src="static/images/macos.webp"/> mac os
<img class="os-image" src="/static/images/macos.webp"/> mac os
</div>
</label>
<div class="vertical tab-content">
@ -189,8 +189,8 @@
During the alpha test phase, we're only offering a simple application download for MacOS.
</p>
<blockquote>
<a class="horizontal align-center" href="https://picopublish.sequentialread.com/files/greenhouse-desktop-alpha-rc0.dmg">
<img class="os-image" src="static/images/macos_disk_image.webp"/> greenhouse-desktop-alpha-rc0.dmg
<a class="horizontal align-center" href="/releases/greenhouse-desktop-alpha-rc0.dmg">
<img class="os-image" src="/static/images/macos_disk_image.webp"/> greenhouse-desktop-alpha-rc0.dmg
</a>
</blockquote>
<br/>

55
frontend/static/image-sources/generate_webp.sh

@ -36,32 +36,33 @@ pngToWebp () {
cwebp -q "$quality" -m 6 -preset text -alpha_filter best "$pngFilename" -o "$webpFilename"
}
svgToWebp "greenhouse-border.svg" "$quality" "icon"
svgToWebp "free.svg" "$quality" "icon"
# svgToWebp "blah.svg" "$quality" "120" "105"
# svgToWebp "greenhouse-border.svg" "$quality" "icon"
# svgToWebp "free.svg" "$quality" "icon"
# # svgToWebp "blah.svg" "$quality" "120" "105"
pngToWebp "mascot-angry.png" "$lowerquality" "icon"
pngToWebp "mascot-dead.png" "$lowerquality" "icon"
pngToWebp "mascot-laptop.png" "$lowerquality" "icon"
pngToWebp "mascot-reading.png" "$lowerquality" "icon"
pngToWebp "mascot-sad.png" "$lowerquality" "icon"
pngToWebp "mascot-science.png" "$lowerquality" "icon"
pngToWebp "mascot-surfing.png" "$lowerquality" "icon"
pngToWebp "beginner.png" "$quality" "icon"
pngToWebp "custodialcloud.png" "$lowerquality" "text"
pngToWebp "hybridcloud.png" "$lowerquality" "text"
pngToWebp "selfhost.png" "$lowerquality" "text"
pngToWebp "namecheap-1.png" "$quality" "text"
pngToWebp "namecheap-2.png" "$quality" "text"
pngToWebp "namecheap-3.png" "$quality" "text"
pngToWebp "shine.png" "$quality" "icon"
pngToWebp "macos.png" "$quality" "icon"
pngToWebp "macos_disk_image.png" "$quality" "icon"
pngToWebp "tux.png" "$quality" "icon"
pngToWebp "windows.png" "$quality" "icon"
pngToWebp "windows-installer.png" "$quality" "icon"
# pngToWebp "intel.png" "$quality" "text"
# pngToWebp "amd.png" "$quality" "text"
# pngToWebp "arm.png" "$quality" "text"
pngToWebp "raspberrypi.png" "$quality" "icon"
# pngToWebp "mascot-angry.png" "$lowerquality" "icon"
# pngToWebp "mascot-dead.png" "$lowerquality" "icon"
pngToWebp "mascot-dancing.png" "$lowerquality" "icon"
# pngToWebp "mascot-laptop.png" "$lowerquality" "icon"
# pngToWebp "mascot-reading.png" "$lowerquality" "icon"
# pngToWebp "mascot-sad.png" "$lowerquality" "icon"
# pngToWebp "mascot-science.png" "$lowerquality" "icon"
# pngToWebp "mascot-surfing.png" "$lowerquality" "icon"
# pngToWebp "beginner.png" "$quality" "icon"
# pngToWebp "custodialcloud.png" "$lowerquality" "text"
# pngToWebp "hybridcloud.png" "$lowerquality" "text"
# pngToWebp "selfhost.png" "$lowerquality" "text"
# pngToWebp "namecheap-1.png" "$quality" "text"
# pngToWebp "namecheap-2.png" "$quality" "text"
# pngToWebp "namecheap-3.png" "$quality" "text"
# pngToWebp "shine.png" "$quality" "icon"
# pngToWebp "macos.png" "$quality" "icon"
# pngToWebp "macos_disk_image.png" "$quality" "icon"
# pngToWebp "tux.png" "$quality" "icon"
# pngToWebp "windows.png" "$quality" "icon"
# pngToWebp "windows-installer.png" "$quality" "icon"
# # pngToWebp "intel.png" "$quality" "text"
# # pngToWebp "amd.png" "$quality" "text"
# # pngToWebp "arm.png" "$quality" "text"
# pngToWebp "raspberrypi.png" "$quality" "icon"

4
frontend/static/install.sh

@ -122,8 +122,8 @@ update_binary()
if ! printf "%s" "$sha256sum_output" | sha256sum -c > /dev/null 2>&1 ; then
printf "Downloading %s binary...\n" "$binary_name"
picopublish_url="https://picopublish.sequentialread.com/files"
download_url="$picopublish_url/$file_name-$binary_version-linux-$binary_arch.gz"
releases_url="https://greenhouse-alpha.server.garden/releases"
download_url="$releases_url/$file_name-$binary_version-linux-$binary_arch.gz"
temporary_gzip_file="/tmp/greenhouse-install-$file_name.gz"
curl_log_file="/tmp/greenhouse-install-download-$file_name.log"

12
frontend_admin_panel.go

@ -41,7 +41,7 @@ func registerAdminPanelRoutes(app *FrontendApp) {
} else {
err := app.Model.DeleteVPSInstance(split[0], split[1])
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
}
@ -59,7 +59,7 @@ func registerAdminPanelRoutes(app *FrontendApp) {
BytesMonthly: DEFAULT_INSTANCE_MONTHLY_BYTES,
})
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
}
@ -127,18 +127,18 @@ func registerAdminPanelRoutes(app *FrontendApp) {
validVpsInstances, dbOnlyInstances, cloudOnlyInstances, err := app.Backend.GetInstances()
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
tenants, err := app.Model.GetTenants()
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
tenantVpsInstanceRows, err := app.Model.GetTenantVPSInstanceRows(billingYear, billingMonth)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
@ -255,7 +255,7 @@ func registerAdminPanelRoutes(app *FrontendApp) {
HashOfSessionId: hashOfSessionId,
}
app.buildPageFromTemplate(responseWriter, session, "admin.html", data)
app.buildPageFromTemplate(responseWriter, request, session, "admin.html", data)
})
}

9
frontend_howto.go

@ -24,20 +24,20 @@ func registerHowtoRoutes(app *FrontendApp) {
if session.TenantId != 0 {
tenant, err := app.Model.GetTenant(session.TenantId)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
templateData.Subdomain = tenant.Subdomain
}
app.buildPageFromTemplate(responseWriter, session, "about-dns.html", templateData)
app.buildPageFromTemplate(responseWriter, request, session, "about-dns.html", templateData)
},
)
app.handleWithSessionNotRequired(
"/install-greenhouse-self-hosting-software-linux",
func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
app.buildPageFromTemplate(responseWriter, session, "install-linux.html", struct{}{})
app.buildPageFromTemplate(responseWriter, request, session, "install-linux.html", struct{}{})
},
)
@ -61,6 +61,9 @@ func registerHowtoRoutes(app *FrontendApp) {
http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError)
return
}
go postTelemetryFromRequest("release_download", request, app, scriptFileName)
responseWriter.Header().Add("Content-Type", "application/x-sh")
responseWriter.Header().Add("Content-Length", strconv.Itoa(int(stat.Size())))
io.Copy(responseWriter, file)

106
frontend_login.go

@ -7,7 +7,6 @@ import (
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
@ -22,7 +21,8 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
if request.Method == "POST" {
remoteUsersHMAC := request.PostFormValue("hmac")
loginSuccess := false
tenantId, databasesHashedPassword, emailVerified := app.Model.GetLoginInfo(request.PostFormValue("email"))
email := request.PostFormValue("email")
tenantId, databasesHashedPassword, emailVerified := app.Model.GetLoginInfo(email)
if remoteUsersHMAC == "" {
saltedPassword := fmt.Sprintf("%s%s", app.PasswordHashSalt, request.PostFormValue("password"))
@ -38,7 +38,7 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
} else {
databasesHashedPasswordBytes, err := base64.StdEncoding.DecodeString(databasesHashedPassword)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
hmacFromDB := hmac.New(sha256.New, databasesHashedPasswordBytes)
@ -49,12 +49,23 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
}
}
returnTo := "/profile"
if request.PostFormValue("returnTo") != "" && app.basicURLPathRegex.MatchString(request.PostFormValue("returnTo")) {
returnTo = request.PostFormValue("returnTo")
}
loginTelemetry := fmt.Sprintf(
"JS HMAC Login: %t, loginSuccess: %t, emailVerified: %t, returnTo: %s",
remoteUsersHMAC != "", loginSuccess, emailVerified, returnTo,
)
go postTelemetry("login", getRemoteIpFromRequest(request), email, loginTelemetry)
if loginSuccess {
err := app.setSession(
responseWriter,
&Session{
TenantId: tenantId,
Email: strings.ToLower(request.PostFormValue("email")),
Email: strings.ToLower(email),
EmailVerified: emailVerified,
// we will use the SameSite Lax cookie policy until the first time that the user re-logs-in AFTER confirming thier
@ -69,12 +80,8 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
},
)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
} else {
returnTo := "/profile"
if request.PostFormValue("returnTo") != "" && app.basicURLPathRegex.MatchString(request.PostFormValue("returnTo")) {
returnTo = request.PostFormValue("returnTo")
}
http.Redirect(responseWriter, request, returnTo, http.StatusFound)
}
return
@ -96,7 +103,7 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
Timestamp: fmt.Sprintf("%d", time.Now().Unix()),
ReturnTo: returnTo,
}
app.buildPageFromTemplate(responseWriter, session, "login.html", data)
app.buildPageFromTemplate(responseWriter, request, session, "login.html", data)
})
app.handleWithSessionNotRequired("/register", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
@ -122,6 +129,7 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
hashedPasswordByteArray := sha256.Sum256([]byte(saltedPassword))
hashedPassword = base64.StdEncoding.EncodeToString(hashedPasswordByteArray[:])
}
if (*session.Flash)["error"] == "" {
tenantId, err := app.Model.Register(data.Email, hashedPassword)
if err != nil {
@ -132,7 +140,7 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
emailVerificationToken := hex.EncodeToString(emailVerificationTokenBuffer)
err := app.Model.CreateEmailVerificationToken(emailVerificationToken, tenantId, time.Now().Add(time.Hour))
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
err = app.setSession(
@ -146,7 +154,7 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
},
)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
protocol := "https"
@ -158,23 +166,41 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
specialPort = fmt.Sprintf(":%d", app.Port)
}
err = emailService.SendEmail(
fmt.Sprintf("Please verify your account on %s", app.Domain),
fmt.Sprintf("Please verify your email on %s", app.Domain),
data.Email,
fmt.Sprintf(
"Please click the following link to verify your account: %s://%s%s/verify-email/%s",
"Please click the following link to verify your email: %s://%s%s/verify-email/%s",
protocol, app.Domain, specialPort, emailVerificationToken,
),
)
if err != nil {
(*session.Flash)["error"] += fmt.Sprintln(err)
registerTelemetry := fmt.Sprintf(
"registration (JS HMAC: %t, tokenHash=%s) send email failed: %s",
hashedPassword != "", getHashForTelemetry(app, emailVerificationToken), (*session.Flash)["error"],
)
go postTelemetry("register", getRemoteIpFromRequest(request), data.Email, registerTelemetry)
} else {
registerTelemetry := fmt.Sprintf(
"registration (JS HMAC: %t, tokenHash=%s) success!!",
hashedPassword != "", getHashForTelemetry(app, emailVerificationToken),
)
go postTelemetry("register", getRemoteIpFromRequest(request), data.Email, registerTelemetry)
http.Redirect(responseWriter, request, "/verify-email", http.StatusFound)
return
}
}
} else {
registerTelemetry := fmt.Sprintf(
"registration (JS HMAC: %t) not attempted because: %s",
hashedPassword != "", (*session.Flash)["error"],
)
go postTelemetry("register", getRemoteIpFromRequest(request), data.Email, registerTelemetry)
}
}
app.buildPageFromTemplate(responseWriter, session, "register.html", data)
app.buildPageFromTemplate(responseWriter, request, session, "register.html", data)
})
verifyEmailHandler := func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
@ -183,7 +209,7 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
log.Printf("verify email handler: [%s] [%d]\n", mux.Vars(request)["token"], session.TenantId)
//log.Printf("verify email handler: [%s] [%d]\n", mux.Vars(request)["token"], session.TenantId)
if mux.Vars(request)["token"] != "" && session.TenantId != 0 {
err := app.Model.VerifyEmail(mux.Vars(request)["token"], session.TenantId)
if err != nil {
@ -191,7 +217,12 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
} else {
err = app.Backend.InitializeTenant(session.TenantId, session.Email)
if err != nil {
app.unhandledError(responseWriter, err)
go postTelemetryFromRequest("verify-email", request, app, fmt.Sprintf(
"tokenHash=%s InitializeTenant failed: %s",
getHashForTelemetry(app, mux.Vars(request)["token"]), err,
))
app.unhandledError(responseWriter, request, err)
return
}
@ -206,16 +237,21 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
}
err = app.setSession(responseWriter, newSession)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
go postTelemetryFromRequest("verify-email", request, app, fmt.Sprintf(
"tokenHash=%s success!!",
getHashForTelemetry(app, mux.Vars(request)["token"]),
))
app.setFlash(responseWriter, session, "info", "Your email address has been verified!")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
}
app.buildPageFromTemplate(responseWriter, session, "verify-email.html", nil)
app.buildPageFromTemplate(responseWriter, request, session, "verify-email.html", nil)
}
app.handleWithSession("/verify-email", verifyEmailHandler)
@ -225,7 +261,7 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
if session.TenantId != 0 {
err := app.Model.LogoutTenant(session.TenantId)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
}
@ -244,26 +280,40 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
return
}
telemetryValue := fmt.Sprintf("POST id=%s name=%s", getHashForTelemetry(app, appAuthSessionId), name)
go postTelemetry("app-auth-session", getRemoteIpFromRequest(request), "", telemetryValue)
app.ApplicationAuthSessions[appAuthSessionId] = &ApplicationAuthSession{
Name: name,
}
responseWriter.Write([]byte("OK"))
} else {
telemetryValue := fmt.Sprintf("GET id=%s", getHashForTelemetry(app, appAuthSessionId))
appAuthSession, hasAppAuthSession := app.ApplicationAuthSessions[appAuthSessionId]
if hasAppAuthSession {
if appAuthSession.TenantId != 0 {
telemetryValue = fmt.Sprintf("%s name=%s", telemetryValue, appAuthSession.Name)
apiToken, _, err := app.Backend.CreateAPIToken(appAuthSession.TenantId, appAuthSession.Name)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
telemetryValue = fmt.Sprintf("%s success!!", telemetryValue)
go postTelemetry("app-auth-session", getRemoteIpFromRequest(request), "", telemetryValue)
responseWriter.Header().Set("Content-Type", "text/plain")
responseWriter.Write([]byte(apiToken))
} else {
telemetryValue = fmt.Sprintf("%s 401 unauthorized", telemetryValue)
go postTelemetry("app-auth-session", getRemoteIpFromRequest(request), "", telemetryValue)
http.Error(responseWriter, "401 unauthorized", http.StatusUnauthorized)
}
} else {
telemetryValue = fmt.Sprintf("%s 404 not found", telemetryValue)
go postTelemetry("app-auth-session", getRemoteIpFromRequest(request), "", telemetryValue)
http.Error(responseWriter, "404 not found", http.StatusNotFound)
}
}
@ -271,11 +321,21 @@ func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
app.handleWithSession("/app-connect", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
appAuthSessionId := request.URL.Query().Get("session")
telemetryValue := fmt.Sprintf("GET id=%s", getHashForTelemetry(app, appAuthSessionId))
appAuthSession, hasAppAuthSession := app.ApplicationAuthSessions[appAuthSessionId]
if hasAppAuthSession && appAuthSession.TenantId == 0 {
app.ApplicationAuthSessions[appAuthSessionId].TenantId = session.TenantId
app.buildPageFromTemplate(responseWriter, session, "app-connect.html", nil)
app.buildPageFromTemplate(responseWriter, request, session, "app-connect.html", nil)
telemetryValue = fmt.Sprintf("%s success!!", telemetryValue)
go postTelemetryFromRequest("app-connect", request, app, telemetryValue)
} else {
telemetryValue = fmt.Sprintf("%s 404 not found", telemetryValue)
go postTelemetryFromRequest("app-connect", request, app, telemetryValue)
http.Error(responseWriter, "404 not found", http.StatusNotFound)
}
})

66
frontend_profile.go

@ -19,25 +19,6 @@ import (
func registerProfileRoutes(app *FrontendApp) {
// TODO remove this?
app.handleWithSessionNotRequired("/ostest", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
goatCounterOSName := os_from_user_agent_header.Parse(request.Header.Get("User-Agent")).OSName
simplifiedOSName := map[string]string{
"iOS": "macos",
"macOS": "macos",
"Windows Phone": "windows",
"Windows": "windows",
}[goatCounterOSName]
if simplifiedOSName == "" {
simplifiedOSName = "linux"
}
responseWriter.Header().Set("Content-Type", "text/plain")
responseWriter.Write([]byte(simplifiedOSName))
})
app.handleWithSession("/profile", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
if !session.EmailVerified {
// anti-XSS: only set path into the flash cookie if it matches a basic url pattern
@ -56,14 +37,14 @@ func registerProfileRoutes(app *FrontendApp) {
tenant, err := app.Model.GetTenant(session.TenantId)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
billingYear, billingMonth, _, _, _ := getBillingTimeInfo()
usageTotal, err := app.Model.GetTenantUsageTotal(session.TenantId, billingYear, billingMonth)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
@ -78,6 +59,7 @@ func registerProfileRoutes(app *FrontendApp) {
}
action := request.PostFormValue("action")
if action == "update_free_subdomain" {
postedFreeSubdomain := strings.ToLower(request.PostFormValue("subdomain"))
subdomainRegex := regexp.MustCompile("^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$")
if !subdomainRegex.MatchString(postedFreeSubdomain) {
@ -90,6 +72,10 @@ func registerProfileRoutes(app *FrontendApp) {
}
alreadyTaken, err := app.Model.SetFreeSubdomain(session.TenantId, postedFreeSubdomain)
if err != nil {
go postTelemetryFromRequest("update_free_subdomain", request, app, fmt.Sprintf(
"'%s': db error: %s", postedFreeSubdomain, err,
))
errorMessage := "unable to update your subdomain: internal server error"
log.Printf("%s: %+v", errorMessage, err)
app.setFlash(responseWriter, session, "error", errorMessage)
@ -104,6 +90,10 @@ func registerProfileRoutes(app *FrontendApp) {
err = app.Backend.Reallocate(false, false)
if err != nil {
go postTelemetryFromRequest("update_free_subdomain", request, app, fmt.Sprintf(
"'%s': reallocate error: %s", postedFreeSubdomain, err,
))
errorMessage := "unable to update your subdomain: internal server error"
log.Printf("%s: %+v", errorMessage, err)
app.setFlash(responseWriter, session, "error", errorMessage)
@ -111,6 +101,10 @@ func registerProfileRoutes(app *FrontendApp) {
return
}
go postTelemetryFromRequest("update_free_subdomain", request, app, fmt.Sprintf(
"'%s': success!!", postedFreeSubdomain,
))
successMessage := fmt.Sprintf("Success! Your personal subdomain is now '%s.%s'\n", postedFreeSubdomain, freeSubdomainDomain)
app.setFlash(responseWriter, session, "info", successMessage)
} else if action == "add_external_domain" {
@ -130,6 +124,10 @@ func registerProfileRoutes(app *FrontendApp) {
usersPersonalSubdomain := fmt.Sprintf("%s.%s", tenant.Subdomain, freeSubdomainDomain)
valid, err := app.Backend.ValidateExternalDomain(postedExternalDomain, usersPersonalSubdomain, false)
if err != nil {
go postTelemetryFromRequest("add_external_domain", request, app, fmt.Sprintf(
"'%s' -> '%s': validate: %s", postedExternalDomain, usersPersonalSubdomain, err,
))
errorMessage := fmt.Sprintf("unable to update your subdomain: %s", err)
log.Printf("%s: %+v", errorMessage, err)
app.setFlash(responseWriter, session, "error", errorMessage)
@ -137,6 +135,11 @@ func registerProfileRoutes(app *FrontendApp) {
return
}
if !valid {
go postTelemetryFromRequest("add_external_domain", request, app, fmt.Sprintf(
"'%s' -> '%s': CNAME is missing", postedExternalDomain, usersPersonalSubdomain,
))
app.setFlash(responseWriter, session, "error", fmt.Sprintf(
"the domain '%s' does not appear to have a CNAME record pointing to '%s'. Either you have not created the CNAME record set yet, it was not created correctly, or not enough time has elapsed for the DNS record change to propagate.",
postedExternalDomain, usersPersonalSubdomain,
@ -147,6 +150,10 @@ func registerProfileRoutes(app *FrontendApp) {
err = app.Model.AddExternalDomain(session.TenantId, postedExternalDomain)
if err != nil {
go postTelemetryFromRequest("add_external_domain", request, app, fmt.Sprintf(
"'%s' -> '%s': model error: %s", postedExternalDomain, usersPersonalSubdomain, err,
))
errorMessage := "unable to update your subdomain: internal server error"
log.Printf("%s: %+v", errorMessage, err)
app.setFlash(responseWriter, session, "error", errorMessage)
@ -154,6 +161,10 @@ func registerProfileRoutes(app *FrontendApp) {
return
}
go postTelemetryFromRequest("add_external_domain", request, app, fmt.Sprintf(
"'%s' -> '%s': success!!", postedExternalDomain, usersPersonalSubdomain,
))
app.setFlash(responseWriter, session, "info", fmt.Sprintf("Success! '%s' has been added as an external domain. You may now create tunnels from '%s' using any of your greenhouse clients. \n", postedExternalDomain, postedExternalDomain))
} else if action == "create_api_token" {
@ -171,9 +182,12 @@ func registerProfileRoutes(app *FrontendApp) {
apiToken, name, err := app.Backend.CreateAPIToken(session.TenantId, keyName)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
go postTelemetryFromRequest("create_api_token", request, app, fmt.Sprintf("'%s': success!!", keyName))
app.setFlash(responseWriter, session, "api-token", apiToken)
app.setFlash(responseWriter, session, "api-token-name", name)
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))
@ -234,13 +248,13 @@ func registerProfileRoutes(app *FrontendApp) {
HashOfSessionId: hashOfSessionId,
OperatingSystem: simplifiedOSName,
}
app.buildPageFromTemplateWithClass(responseWriter, session, "alpha-profile.html", data, "no-horizontal-margin")
app.buildPageFromTemplateWithClass(responseWriter, request, session, "alpha-profile.html", data, "no-horizontal-margin")
})
app.handleWithSession("/profile/usage_graph.png", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
// tenant, err := app.Model.GetTenant(session.TenantId)
// if err != nil {
// app.unhandledError(responseWriter, err)
// app.unhandledError(responseWriter, request, err)
// return
// }
@ -248,7 +262,7 @@ func registerProfileRoutes(app *FrontendApp) {
usageMetrics, err := app.Model.GetTenantUsageMetrics(session.TenantId, start, end)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
@ -391,7 +405,7 @@ func registerProfileRoutes(app *FrontendApp) {
buffer := bytes.NewBuffer([]byte{})
err = graph.Render(chart.PNG, buffer)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}

4
gandi_service.go

@ -64,6 +64,10 @@ func (service *GandiService) UpdateFreeSubdomains(freeSubdomains map[string][]st
)
}
if len(requestBody.Items) == 0 {
return nil
}
requestBodyBytes, err := json.Marshal(requestBody)
if err != nil {
return err

2
ingress_service.go

@ -41,7 +41,7 @@ type GreenhouseDaemonStatus struct {
ApplyConfigStatusError string `json:"apply_config_status_error"`
}
const adminThresholdNodeId = "greenhouse_internal_node"
const adminThresholdNodeId = "greenhouse-internal-node"
const greenhouseExternalDomain = "greenhouse-alpha.server.garden"

73
main.go

@ -11,6 +11,7 @@ import (
"regexp"
"runtime"
"runtime/debug"
"strconv"
"time"
"git.sequentialread.com/forest/greenhouse/pki"
@ -50,6 +51,7 @@ const isProduction = false
func main() {
_, err := time.LoadLocation("UTC")
if err != nil {
go postTelemetry("greenhouse-cloud", "", "admin", fmt.Sprintf("load UTC location failed: %s", err))
panic(errors.Wrap(err, "can't start the app because can't load UTC location"))
}
@ -59,6 +61,7 @@ func main() {
emailService := &config.SMTP
err = emailService.Initialize()
if err != nil {
go postTelemetry("greenhouse-cloud", "", "admin", fmt.Sprintf("emailService.Initialize() failed: %s", err))
panic(err)
}
@ -76,12 +79,15 @@ func main() {
err = scheduledTasks.Initialize()
if err != nil {
// TODO should this be Fatalf??
go postTelemetry("greenhouse-cloud", "", "admin", fmt.Sprintf("initialization process failed: %s", err))
log.Printf("Greenhouse's initialization process failed: \n%+v\n\n", err)
}
go (func(backendApp *BackendApp) {
defer (func() {
if r := recover(); r != nil {
go postTelemetry("greenhouse-cloud", "", "admin", fmt.Sprintf("metric collection panic: %s", r))
fmt.Printf("backendApp: panic: %+v\n", r)
debug.PrintStack()
}
@ -90,6 +96,7 @@ func main() {
for {
err := backendApp.ConsumeMetrics()
if err != nil {
go postTelemetry("greenhouse-cloud", "", "admin", fmt.Sprintf("metric collection failed: %s", err))
log.Printf("metric collection failed: %+v\n", err)
}
time.Sleep(time.Second * 6)
@ -127,14 +134,20 @@ func main() {
// })()
}
go postTelemetry("greenhouse-cloud", "", "admin", "greenhouse started up!")
err = frontendApp.ListenAndServe()
go postTelemetry("greenhouse-cloud", "", "admin", fmt.Sprintf("frontendApp.ListenAndServe(): %s", err))
panic(err)
}
func getConfig(workingDirectory string) *Config {
configBytes, err := ioutil.ReadFile(filepath.Join(workingDirectory, "config.json"))
if err != nil {
log.Fatalf("getConfig(): can't ioutil.ReadFile(\"config.json\") because %+v \n", err)
configBytes, err = ioutil.ReadFile(filepath.Join(workingDirectory, "config/config.json"))
if err != nil {
log.Fatalf("getConfig(): can't ioutil.ReadFile(\"config.json\" or \"config/config.json\") because %+v \n", err)
}
}
var config Config
@ -147,6 +160,64 @@ func getConfig(workingDirectory string) *Config {
config.AdminTenantId = 1
}
digitalOceanAPIKey := os.Getenv("GREENHOUSE_DIGITALOCEAN_API_KEY")
databaseConnectionString := os.Getenv("GREENHOUSE_DATABASE_CONNECTION_STRING")
databaseSchema := os.Getenv("GREENHOUSE_DATABASE_SCHEMA")
gandiAPIKey := os.Getenv("GREENHOUSE_GANDI_API_KEY")
sshPrivateKeyFile := os.Getenv("GREENHOUSE_SSH_PRIVATE_KEY_FILE")
backblazeBucketName := os.Getenv("GREENHOUSE_BACKBLAZE_BUCKET_NAME")
backblazeKeyId := os.Getenv("GREENHOUSE_BACKBLAZE_KEY_ID")
backblazeSecretKey := os.Getenv("GREENHOUSE_BACKBLAZE_SECRET_KEY")
smtpHost := os.Getenv("GREENHOUSE_SMTP_HOST")
smtpPortString := os.Getenv("GREENHOUSE_SMTP_PORT")
smtpUsername := os.Getenv("GREENHOUSE_SMTP_USERNAME")
smtpPassword := os.Getenv("GREENHOUSE_SMTP_PASSWORD")
smtpEncryption := os.Getenv("GREENHOUSE_SMTP_ENCRYPTION")
if digitalOceanAPIKey != "" {
config.DigitalOceanAPIKey = digitalOceanAPIKey
}
if databaseConnectionString != "" {
config.DatabaseConnectionString = databaseConnectionString
}
if databaseSchema != "" {
config.DatabaseSchema = databaseSchema
}
if gandiAPIKey != "" {
config.GandiAPIKey = gandiAPIKey
}
if sshPrivateKeyFile != "" {
config.SSHPrivateKeyFile = sshPrivateKeyFile
}
if backblazeBucketName != "" {
config.BackblazeBucketName = backblazeBucketName
}
if backblazeKeyId != "" {
config.BackblazeKeyId = backblazeKeyId
}
if backblazeSecretKey != "" {
config.BackblazeSecretKey = backblazeSecretKey
}
if smtpHost != "" {
config.SMTP.Host = smtpHost
}
if smtpPortString != "" {
smtpPort, err := strconv.Atoi(smtpPortString)
if err != nil {
log.Fatalf("getConfig(): can't convert smtpPortString '%s' to int: %+v \n", smtpPortString, err)
}
config.SMTP.Port = smtpPort
}
if smtpUsername != "" {
config.SMTP.Username = smtpUsername
}
if smtpPassword != "" {
config.SMTP.Password = smtpPassword
}
if smtpEncryption != "" {
config.SMTP.Encryption = smtpEncryption
}
configToLog, _ := json.MarshalIndent(config, "", " ")
configToLogString := string(configToLog)
configToLogString = regexp.MustCompile(

10
public_api.go

@ -16,12 +16,12 @@ func AddAPIRoutesToFrontend(app *FrontendApp) {
//log.Println("GET /api/tenant_info")
tenantInfo, err := app.Backend.GetTenantInfo(user.TenantId)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
tenantInfoBytes, err := json.Marshal(tenantInfo)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
@ -50,7 +50,7 @@ func AddAPIRoutesToFrontend(app *FrontendApp) {
// used to use fmt.Sprintf("%s.%s", tenant.Subdomain, freeSubdomainDomain) as the greenhouseDomain
// tenant, err := app.Model.GetTenant(user.TenantId)
// if err != nil {
// app.unhandledError(responseWriter, err)
// app.unhandledError(responseWriter, request, err)
// return
// }
@ -58,13 +58,13 @@ func AddAPIRoutesToFrontend(app *FrontendApp) {
user.TenantId, greenhouseExternalDomain, newNodeId, user.APIToken,
)
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}
clientConfigBytes, err := json.MarshalIndent(clientConfig, "", " ")
if err != nil {
app.unhandledError(responseWriter, err)
app.unhandledError(responseWriter, request, err)
return
}

12
scheduled_tasks.go

@ -1,7 +1,9 @@
package main
import (
"fmt"
"log"
"runtime/debug"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
@ -102,6 +104,15 @@ func (tasks *ScheduledTasks) Initialize() error {
log.Println("🌱🏠 greenhouse has initialized successfully!")
go (func() {
defer (func() {
if r := recover(); r != nil {
go postTelemetry("greenhouse-cloud", "", "admin", fmt.Sprintf("scheduledTasks panic: %s", r))
fmt.Printf("scheduledTasks panic: %+v\n", r)
debug.PrintStack()
}
})()
for {
time.Sleep(time.Second * 10)
for name, scheduledTask := range tasks.Registry {
@ -117,6 +128,7 @@ func (tasks *ScheduledTasks) Initialize() error {
go (func(name string, scheduledTask ScheduledTask) {
err := scheduledTask.Action()
if err != nil {
go postTelemetry("greenhouse-cloud", "", "admin", fmt.Sprintf("scheduledTask '%s' failed: %s\n", name, err))
log.Printf("scheduledTask (%s) Action() failed: %+v\n", name, err)
} else {
err := tasks.DB.ScheduledTaskCompleted(name)

64
telemetry.go

@ -0,0 +1,64 @@
package main
import (
"bytes"
"crypto/sha256"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)
var telemetryClient *http.Client
func getTelemetryClient() *http.Client {
if telemetryClient == nil {
telemetryClient = &http.Client{
Timeout: time.Second * 10,
}
}
return telemetryClient
}
func postTelemetryFromRequest(tyype string, request *http.Request, app *FrontendApp, content string) {
account := ""
session, err := app.getSession(request, app.Domain)
if err == nil {
account = session.Email
}
postTelemetry(tyype, getRemoteIpFromRequest(request), account, content)
}
func postTelemetry(tyype string, ip string, account string, content string) {
if ip != "" {
ip = fmt.Sprintf("&ip=%s", ip)
}
if account != "" {
account = fmt.Sprintf("&account=%s", account)
}
response, err := getTelemetryClient().Post(
fmt.Sprintf("https://greenhouse-telemetry.sequentialread.com/?type=%s%s%s", tyype, ip, account),
"text/plain",
bytes.NewBufferString(content),
)
if err != nil {
log.Printf("postTelemetry: %s\n", err)
} else if response.StatusCode > 299 {
responseString := "<read error>"
responseBytes, err := ioutil.ReadAll(response.Body)
if err == nil {
responseString = string(responseBytes)
}
log.Printf("postTelemetry: HTTP %d: %s\n", response.StatusCode, responseString)
}
}
func getRemoteIpFromRequest(request *http.Request) string {
return request.Header.Get("X-Forwarded-For")
}
func getHashForTelemetry(app *FrontendApp, secretValue string) string {
hash1 := sha256.Sum256([]byte(fmt.Sprintf("%s%s", app.PasswordHashSalt, secretValue)))
return fmt.Sprintf("%x", hash1[:4])
}

9
threshold/src_remote_install.sh

@ -5,18 +5,19 @@ systemctl stop threshold
rm -f threshold
curl -sS "https://f000.backblazeb2.com/file/server-garden-artifacts/threshold-amd64.tar.gz" > "./threshold-amd64.tar.gz"
curl -sS "https://picopublish.sequentialread.com/files/threshold-0.0.0-6cfcabd-f445-linux-amd64.gz" > "./threshold.gz"
tar -x -f "./threshold-amd64.tar.gz"
gzip --stdout --decompress "threshold.gz" > "./threshold"
chown threshold:threshold ./threshold
chmod 500 ./threshold
setcap cap_net_bind_service=+ep ./threshold
rm "./threshold-amd64.tar.gz"
rm "./threshold.gz"
systemctl start threshold
sleep 1
journalctl -u threshold -n 50 --no-pager
journalctl -u threshold -n 50 --no-pager

9
threshold_provisioning_service.go

@ -152,7 +152,7 @@ func (service *ThresholdProvisioningService) GetServerInstallScript(tlsCertifica
chmod 500 /opt/threshold
echo "downloading threshold tar file"
curl -sS {{ARTIFACTS_BASE_URL}}/threshold-{{ARCH}}.tar.gz > /tmp/threshold-{{ARCH}}.tar.gz
curl -sS "{{ARTIFACT_URL}}" > /tmp/threshold.gz
# TODO actually verify the checksum here.
#echo "verifying checksum"
@ -165,7 +165,7 @@ func (service *ThresholdProvisioningService) GetServerInstallScript(tlsCertifica
#fi
echo "unarchiving threshold binary"
tar -x -f /tmp/threshold-{{ARCH}}.tar.gz --directory /opt/threshold
gzip --stdout --decompress "/tmp/threshold.gz" > "/opt/threshold/threshold"
chown threshold:threshold /opt/threshold/threshold
chmod 500 /opt/threshold/threshold
@ -299,9 +299,8 @@ WantedBy=multi-user.target
})
substitutions := map[string]string{
"ARTIFACTS_BASE_URL": "https://f000.backblazeb2.com/file/server-garden-artifacts",
"ARCH": "amd64",
"TAR_SHA256": "1207c89f5269220f56e93c96d6c4b06ee3c317ea463b23c3a9210343893c426b",
"ARTIFACT_URL": "https://picopublish.sequentialread.com/files/threshold-0.0.0-6cfcabd-f445-linux-amd64.gz",
"THRESHOLD_SHA256": "9e769e539adeff539ffeacd1a44968da0d43f04afcd6c3faaae90689533e8cca",
"THRESHOLD_DOMAIN": "greenhouse.server.garden",
"GREENHOUSE_MANAGEMENT_API_AUTH_CA": string(managementAPIAuthCABytes),
"GREENHOUSE_CA": string(mainCABytes),

Loading…
Cancel
Save