From a16992ceb7e3a393b1b265bc95f2850d507443f1 Mon Sep 17 00:00:00 2001 From: Ben Busby Date: Wed, 18 Dec 2024 12:33:01 -0700 Subject: [PATCH] Set up deployment via Kamal Kamal has been integrated into the project for deploying to dev/prod/etc environments. The script for creating the deployment config is `deploy.sh` at the root level of the repo, and requires a single argument describing the environment being deployed (i.e. `deploy.sh prod`). This script reads from an environment file matching the specified environment (i.e. `prod.env`). The files generated by deploy.sh are used to deploy the app to the specified servers, allowing seamless deployments without any downtime for the user. --- .gitignore | 11 ++- Dockerfile | 1 + Makefile | 15 ++++ README.md | 6 +- backend/cache/cache.go | 4 +- backend/config/config.go | 8 -- backend/db/client.go | 30 ++++++- backend/db/cron.go | 4 +- backend/db/folders.go | 3 +- backend/db/vault.go | 5 +- backend/server/auth/handlers.go | 8 +- backend/server/payments/btcpay/btcpay.go | 4 +- backend/server/router.go | 5 +- backend/server/server.go | 4 + backend/server/transfer/send/utils.go | 1 + backend/server/transfer/upload.go | 4 +- backend/server/transfer/vault/handlers.go | 29 +++--- backend/service/b2.go | 2 +- backend/utils/misc.go | 60 ++++++------- cli/commands/vault/viewer/viewer.go | 2 +- cli/crypto/crypto.go | 5 ++ config/README.md | 15 ++++ config/deploy.template.yml | 7 ++ config/deploy.yml | 15 ++++ deploy.sh | 103 ++++++++++++++++++++++ docker-compose.yml | 2 +- 26 files changed, 264 insertions(+), 89 deletions(-) create mode 100644 config/README.md create mode 100644 config/deploy.template.yml create mode 100644 config/deploy.yml create mode 100755 deploy.sh diff --git a/.gitignore b/.gitignore index 4ae2e56..4df7ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .idea notes.txt yeetfile -yeetfile-server +yeetfile-server* *.enc test.sh lipsum-big.txt @@ -22,4 +22,13 @@ node_modules/ .pytest_cache/ __pycache__ *test_*.txt +<<<<<<< HEAD +*._* +*.pem products.json + +# Kamal +.kamal +config/* +!config/deploy.yml +!config/deploy.template.yml diff --git a/Dockerfile b/Dockerfile index 51b7289..c344780 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,7 @@ FROM alpine:latest WORKDIR /app COPY --from=builder /app/yeetfile-server /app RUN chmod +x /app/yeetfile-server +RUN apk add --update curl EXPOSE 8090 CMD ["/app/yeetfile-server"] diff --git a/Makefile b/Makefile index a0bfbf5..39fedf1 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,21 @@ backend: web cli: go build -ldflags="-s -w" -tags yeetfile -o yeetfile ./cli +deploy: + GOOS=linux GOARCH=amd64 go build \ + -tags yeetfile-server \ + -o yeetfile-server-deploy \ + ./backend + mv yeetfile-server-deploy ansible/roles/yeetfile/files/ + +deploy_dev: deploy + test -s ./ansible/roles/yeetfile/files/dev.env || { echo "dev.env not in ansible/roles/yeetfile/files -- exiting..."; exit 1; } + ansible-playbook -i ansible/inventory/dev.yml ansible/deploy.yml + +deploy_prod: deploy + test -s ./ansible/roles/yeetfile/files/prod.env || { echo "prod.env not in ansible/roles/yeetfile/files -- exiting..."; exit 1; } + ansible-playbook -i ansible/inventory/prod.yml deploy.yml + clean: rm -f yeetfile-web rm -f yeetfile diff --git a/README.md b/README.md index ac174b7..4f9beff 100644 --- a/README.md +++ b/README.md @@ -177,9 +177,9 @@ These are required to be set if you want to use Backblaze B2 to store data that | Name | Purpose | | -- | -- | -| B2_BUCKET_ID | The ID of the bucket that will be used for storing uploaded content | -| B2_BUCKET_KEY_ID | The ID of the key used for accessing the B2 bucket | -| B2_BUCKET_KEY | The value of the key used for accessing the B2 bucket | +| `YEETFILE_B2_BUCKET_ID` | The ID of the bucket that will be used for storing uploaded content | +| `YEETFILE_B2_BUCKET_KEY_ID` | The ID of the key used for accessing the B2 bucket | +| `YEETFILE_B2_BUCKET_KEY` | The value of the key used for accessing the B2 bucket | #### Misc Environment Variables diff --git a/backend/cache/cache.go b/backend/cache/cache.go index cae6ed6..2f5a480 100644 --- a/backend/cache/cache.go +++ b/backend/cache/cache.go @@ -31,7 +31,7 @@ func PrepCache(fileID string, size int64) { totalCacheSize, err := utils.CheckDirSize(path) if err != nil { - utils.Log(fmt.Sprintf("Unable to check cache dir size: %v", err)) + log.Printf(fmt.Sprintf("Unable to check cache dir size: %v", err)) return } @@ -265,5 +265,5 @@ func init() { panic(err) } - utils.Log(fmt.Sprintf("Caching files to directory: %s", path)) + log.Printf("Caching files to directory: %s", path) } diff --git a/backend/config/config.go b/backend/config/config.go index 0dffc10..01e6c3e 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -70,13 +70,11 @@ type StripeBillingConfig struct { Configured bool Key string WebhookSecret string - PortalLink string } var stripeBilling = StripeBillingConfig{ Key: os.Getenv("YEETFILE_STRIPE_KEY"), WebhookSecret: os.Getenv("YEETFILE_STRIPE_WEBHOOK_SECRET"), - PortalLink: os.Getenv("YEETFILE_STRIPE_PORTAL_LINK"), } // ============================================================================= @@ -85,17 +83,11 @@ var stripeBilling = StripeBillingConfig{ type BTCPayBillingConfig struct { Configured bool - APIKey string WebhookSecret string - StoreID string - ServerURL string } var btcPayBilling = BTCPayBillingConfig{ - APIKey: os.Getenv("YEETFILE_BTCPAY_API_KEY"), WebhookSecret: os.Getenv("YEETFILE_BTCPAY_WEBHOOK_SECRET"), - StoreID: os.Getenv("YEETFILE_BTCPAY_STORE_ID"), - ServerURL: os.Getenv("YEETFILE_BTCPAY_SERVER_URL"), } // ============================================================================= diff --git a/backend/db/client.go b/backend/db/client.go index 00f6041..08ac9fb 100644 --- a/backend/db/client.go +++ b/backend/db/client.go @@ -8,6 +8,7 @@ import ( _ "github.com/lib/pq" "io" "log" + "os" "sort" "strconv" "strings" @@ -30,15 +31,40 @@ func init() { user = utils.GetEnvVar("YEETFILE_DB_USER", "postgres") password = utils.GetEnvVar("YEETFILE_DB_PASS", "") dbname = utils.GetEnvVar("YEETFILE_DB_NAME", "yeetfile") + cert = utils.GetEnvVar("YEETFILE_DB_CERT", "") ) - connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", + connStr := fmt.Sprintf( + "postgres://%s:%s@%s:%s/%s", user, password, host, port, dbname) + if len(cert) > 0 { + cert = strings.ReplaceAll(cert, "\\n", "\n") + + certFile, err := os.CreateTemp("", ".*") + if err != nil { + log.Fatalln("Error creating tmp dir for db cert:", err) + } + + if _, err = certFile.WriteString(cert); err != nil { + log.Fatalf("Unable to write tmp CA cert file: %v\n", err) + } + + if err = certFile.Close(); err != nil { + log.Fatalf("Unable to close tmp CA cert file: %v\n", err) + } + + connStr += fmt.Sprintf( + "?sslmode=verify-full&sslrootcert=%s", + certFile.Name()) + } else { + connStr += "?sslmode=disable" + } + // Open db connection var err error db, err = sql.Open("postgres", connStr) @@ -129,7 +155,7 @@ func clearDatabase(id string) { func TableIDExists(tableName, id string) bool { rows, err := db.Query(`SELECT * FROM `+tableName+` WHERE id=$1`, id) if err != nil { - utils.Logf("Error checking for id in table '%s': %v", tableName, err) + log.Printf("Error checking for id in table '%s': %v", tableName, err) return true } diff --git a/backend/db/cron.go b/backend/db/cron.go index 2a1aeea..d12e1ae 100644 --- a/backend/db/cron.go +++ b/backend/db/cron.go @@ -134,7 +134,9 @@ func (task CronTask) runCronTask() { } if !lockAcquired { - log.Printf("'%s' task lock already acquired, skipping", task.Name) + if config.IsDebugMode { + log.Printf("'%s' task lock already acquired, skipping", task.Name) + } return } diff --git a/backend/db/folders.go b/backend/db/folders.go index 2c9fd9e..8687d2b 100644 --- a/backend/db/folders.go +++ b/backend/db/folders.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "time" - "yeetfile/backend/utils" "yeetfile/shared" ) @@ -79,7 +78,7 @@ func insertFolder(id, ownerID string, folder shared.NewVaultFolder, pwFolder boo func FolderIDExists(id string) bool { rows, err := db.Query(`SELECT * FROM folders WHERE id=$1`, id) if err != nil { - utils.Logf("Error checking folder id: %v", err) + log.Printf("Error checking folder id: %v", err) return true } diff --git a/backend/db/vault.go b/backend/db/vault.go index 3fa656f..e4f02ea 100644 --- a/backend/db/vault.go +++ b/backend/db/vault.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "time" - "yeetfile/backend/utils" "yeetfile/shared" ) @@ -107,7 +106,7 @@ func GetVaultItems( } if err != nil { - utils.Logf("Error retrieving vault contents: %v", err) + log.Printf("Error retrieving vault contents: %v", err) return nil, shared.FolderOwnershipInfo{}, err } @@ -306,7 +305,7 @@ func ShareFile(share shared.NewSharedItem, userID string) (string, error) { func VaultItemIDExists(id string) bool { rows, err := db.Query(`SELECT * FROM vault WHERE id=$1`, id) if err != nil { - utils.Logf("Error checking vault item id: %v", err) + log.Printf("Error checking vault item id: %v", err) return true } diff --git a/backend/server/auth/handlers.go b/backend/server/auth/handlers.go index 7339061..87d16c1 100644 --- a/backend/server/auth/handlers.go +++ b/backend/server/auth/handlers.go @@ -91,7 +91,7 @@ func SignupHandler(w http.ResponseWriter, req *http.Request) { response = shared.SignupResponse{ Error: "Error creating account ID", } - utils.Logf("Error: %v\n", err) + log.Printf("Error: %v\n", err) } else { response = shared.SignupResponse{ Identifier: id, @@ -263,7 +263,7 @@ func VerifyAccountHandler(w http.ResponseWriter, req *http.Request) { http.Error(w, "Unable to parse request", http.StatusBadRequest) return } else if utils.IsStructMissingAnyField(verify) { - utils.Log("Missing required fields for verification") + log.Println("Missing required fields for verification") http.Error(w, "Unable to parse request", http.StatusBadRequest) return } @@ -388,14 +388,14 @@ func PubKeyHandler(w http.ResponseWriter, req *http.Request, _ string) { } if err != nil || len(userID) == 0 { - utils.Logf("Error in user lookup for pub key: %v\n", err) + log.Printf("Error in user lookup for pub key: %v\n", err) http.Error(w, "User not found", http.StatusNotFound) return } pubKey, err := db.GetUserPubKey(userID) if err != nil { - utils.Logf("Error fetching pub key: %v\n", err) + log.Printf("Error fetching pub key: %v\n", err) http.Error(w, "User not found", http.StatusNotFound) return } diff --git a/backend/server/payments/btcpay/btcpay.go b/backend/server/payments/btcpay/btcpay.go index 26d111a..e5966ec 100644 --- a/backend/server/payments/btcpay/btcpay.go +++ b/backend/server/payments/btcpay/btcpay.go @@ -7,13 +7,13 @@ import ( "fmt" "log" "net/http" - "os" + "yeetfile/backend/config" "yeetfile/backend/utils" ) // IsValidRequest validates incoming webhook events from BTCPay Server func IsValidRequest(w http.ResponseWriter, req *http.Request) ([]byte, bool) { - secret := os.Getenv("YEETFILE_BTCPAY_WEBHOOK_SECRET") + secret := config.YeetFileConfig.BTCPayBilling.WebhookSecret sig := req.Header.Get("BTCPAY-SIG") if len(sig) == 0 || len(secret) == 0 { return nil, false diff --git a/backend/server/router.go b/backend/server/router.go index 17fb03d..3b2d49a 100644 --- a/backend/server/router.go +++ b/backend/server/router.go @@ -3,7 +3,6 @@ package server import ( "log" "net/http" - "os" "strings" "yeetfile/shared" "yeetfile/shared/endpoints" @@ -63,9 +62,7 @@ func (r *router) AddRoutes(routes []RouteDef) { func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { for el, handler := range r.routes { if r.matchPath(el.Path, req.URL.Path) && el.Method == req.Method { - if os.Getenv("YEETFILE_DEBUG") == "1" { - log.Printf("%s %s\n", req.Method, req.URL) - } + log.Printf("%s %s\n", req.Method, req.URL) handler(w, req) return } diff --git a/backend/server/server.go b/backend/server/server.go index 2649609..6d08174 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "os/signal" + "strings" "syscall" "yeetfile/backend/config" "yeetfile/backend/server/auth" @@ -172,6 +173,9 @@ func serve(r *router, host, port string) { addr := fmt.Sprintf("%s:%s", host, port) if len(config.TLSCert) > 0 && len(config.TLSKey) > 0 { + config.TLSKey = strings.ReplaceAll(config.TLSKey, "\\n", "\n") + config.TLSCert = strings.ReplaceAll(config.TLSCert, "\\n", "\n") + cert, err = tls.X509KeyPair( []byte(config.TLSCert), []byte(config.TLSKey)) diff --git a/backend/server/transfer/send/utils.go b/backend/server/transfer/send/utils.go index d754890..c78be43 100644 --- a/backend/server/transfer/send/utils.go +++ b/backend/server/transfer/send/utils.go @@ -31,6 +31,7 @@ func UserCanSend(size int64, req *http.Request) (bool, error) { log.Printf("Error validating ability to upload: %v\n", err) return false, err } else if availableSend-usedSend < size { + log.Printf("[Send] Out of space: %d - %d > %d", availableSend, usedSend, size) return false, OutOfSpaceError } diff --git a/backend/server/transfer/upload.go b/backend/server/transfer/upload.go index a08fe70..edfba8e 100644 --- a/backend/server/transfer/upload.go +++ b/backend/server/transfer/upload.go @@ -79,7 +79,7 @@ func UploadSingleChunk(upload FileUpload, b2Values db.B2Upload) (bool, error) { upload.data) if err != nil { - utils.Logf("Error uploading to B2: %v\n", err) + log.Printf("Error uploading to B2: %v\n", err) return false, err } @@ -112,7 +112,7 @@ func UploadMultiChunk(upload FileUpload, b2Values db.B2Upload) (bool, error) { upload.data) if err != nil { - utils.Logf("Error: %v\n", err) + log.Printf("Error: %v\n", err) return err } diff --git a/backend/server/transfer/vault/handlers.go b/backend/server/transfer/vault/handlers.go index fc91156..317f1cb 100644 --- a/backend/server/transfer/vault/handlers.go +++ b/backend/server/transfer/vault/handlers.go @@ -77,7 +77,7 @@ func folderViewHandler(w http.ResponseWriter, req *http.Request, userID string, items, ownership, err := db.GetVaultItems(userID, folderID, passVault) if err != nil { - utils.Logf("Error fetching vault items: %v\n", err) + log.Printf("Error fetching vault items: %v\n", err) if err == db.AccessError { http.Error(w, "Unauthorized access", @@ -312,21 +312,21 @@ func UploadDataHandler(w http.ResponseWriter, req *http.Request, userID string) metadata, err := db.RetrieveVaultMetadata(id, userID) if err != nil { - utils.Logf("[YF Vault] Error fetching metadata: %v\n", err) + log.Printf("[YF Vault] Error fetching metadata: %v\n", err) http.Error(w, "No metadata found", http.StatusBadRequest) return } data, err := utils.LimitedChunkReader(w, req.Body) if err != nil { - utils.Logf("[YF Vault] Error reading uploaded data: %v\n", err) + log.Printf("[YF Vault] Error reading uploaded data: %v\n", err) http.Error(w, "Error reading request", http.StatusBadRequest) abortUpload(metadata, userID, 0, chunkNum) return } if chunkNum > metadata.Chunks { - utils.Logf("[YF Vault] User uploading beyond stated # of chunks") + log.Printf("[YF Vault] User uploading beyond stated # of chunks") http.Error(w, "Attempting to upload more chunks than specified", http.StatusBadRequest) abortUpload(metadata, userID, 0, chunkNum) @@ -379,11 +379,8 @@ func DownloadHandler(w http.ResponseWriter, req *http.Request, userID string) { metadata, err := db.RetrieveVaultMetadata(id, userID) if err != nil { - serverMsg, clientMsg := utils.GenErrMsgs( - "Error fetching metadata", - err) - log.Println(serverMsg) - http.Error(w, clientMsg, http.StatusBadRequest) + log.Println("Error fetching metadata:", err) + http.Error(w, "Error fetching metadata", http.StatusBadRequest) return } @@ -392,9 +389,8 @@ func DownloadHandler(w http.ResponseWriter, req *http.Request, userID string) { if config.YeetFileConfig.DefaultUserStorage > 0 { bandwidth, err := db.GetUserBandwidth(userID) if err != nil { - serverMsg, clientMsg := utils.GenErrMsgs("Server error", err) - log.Println(serverMsg) - http.Error(w, clientMsg, http.StatusInternalServerError) + log.Println("Server error:", err) + http.Error(w, "Server error", http.StatusInternalServerError) return } else if bandwidth < metadata.Length { log.Printf("Bandwidth limit triggered") @@ -408,11 +404,8 @@ func DownloadHandler(w http.ResponseWriter, req *http.Request, userID string) { if metadata.PasswordData == nil || len(metadata.PasswordData) == 0 { downloadID, err = db.InitDownload(metadata.RefID, userID, metadata.Chunks) if err != nil { - serverMsg, clientMsg := utils.GenErrMsgs( - "Error initializing download", - err) - log.Println(serverMsg) - http.Error(w, clientMsg, http.StatusInternalServerError) + log.Println("Error initializing download:", err) + http.Error(w, "Error initializing download", http.StatusInternalServerError) return } } @@ -457,7 +450,7 @@ func DownloadChunkHandler(w http.ResponseWriter, req *http.Request, userID strin metadata, err := db.RetrieveVaultMetadata(metadataID, userID) if err != nil { - utils.Logf("Error fetching metadata: %v\n", err) + log.Printf("Error fetching metadata: %v\n", err) http.Error(w, "No metadata found", http.StatusBadRequest) return } diff --git a/backend/service/b2.go b/backend/service/b2.go index 257596e..4338d43 100644 --- a/backend/service/b2.go +++ b/backend/service/b2.go @@ -41,7 +41,7 @@ func init() { B2BucketID = os.Getenv("YEETFILE_B2_BUCKET_ID") if len(B2BucketID) == 0 { - log.Fatal("Missing B2_BUCKET_ID environment variable") + log.Fatal("Missing YEETFILE_B2_BUCKET_ID environment variable") } log.Println("Authorizing B2 account...") diff --git a/backend/utils/misc.go b/backend/utils/misc.go index e31f3f5..0a58945 100644 --- a/backend/utils/misc.go +++ b/backend/utils/misc.go @@ -21,35 +21,28 @@ import ( "yeetfile/shared/endpoints" ) -func Log(msg string) { - if GetEnvVar("YEETFILE_DEBUG", "0") == "1" { - log.Println(msg) +// GetEnvVar is the primary method for reading variables from the environment. +// Note that variables are unset after they are retrieved, so the value needs +// to be stored in some way if it needs to be accessed more than once. +func GetEnvVar(key string, fallback string) string { + value, exists := os.LookupEnv(key) + if !exists { + value = fallback } -} -func Logf(msg string, a ...any) { - if GetEnvVar("YEETFILE_DEBUG", "0") == "1" { - log.Printf(msg, a...) - } -} - -func GenErrMsgs(msg string, err error) (string, string) { - var serverMsg string - var clientMsg string - - serverMsg = fmt.Sprintf("%s\nā””ā”€ Error: %v\n", msg, err) - if GetEnvVar("YEETFILE_DEBUG", "0") == "1" { - clientMsg = serverMsg - } else { - clientMsg = msg + err := os.Unsetenv(key) + if err != nil { + log.Fatalf("Failed to unset %s key: %v\n", key, err) } - return serverMsg, clientMsg + return strings.TrimSpace(value) } +// GetEnvVarBytesB64 retrieves a base64 string from the environment and returns +// the value as a []byte. func GetEnvVarBytesB64(key string, fallback []byte) []byte { - value, exists := os.LookupEnv(key) - if !exists { + value := GetEnvVar(key, "") + if value == "" { return fallback } @@ -61,15 +54,8 @@ func GetEnvVarBytesB64(key string, fallback []byte) []byte { return decoded } -func GetEnvVar(key string, fallback string) string { - value, exists := os.LookupEnv(key) - if !exists { - value = fallback - } - - return strings.TrimSpace(value) -} - +// GetEnvVarInt retrieves a string value from the environment and converts it +// into an integer. func GetEnvVarInt(key string, fallback int) int { value := GetEnvVar(key, strconv.Itoa(fallback)) if value == "" { @@ -78,12 +64,15 @@ func GetEnvVarInt(key string, fallback int) int { num, err := strconv.Atoi(value) if err != nil { + log.Printf("WARNING: Value for %s is not a valid number, using fallback...\n", key) return fallback } return num } +// GetEnvVarInt64 retrieves a string valkue from the environment and converts it +// into a 64-bit integer. func GetEnvVarInt64(key string, fallback int64) int64 { value := GetEnvVar(key, strconv.FormatInt(fallback, 10)) if value == "" { @@ -98,14 +87,17 @@ func GetEnvVarInt64(key string, fallback int64) int64 { return num } +// GetEnvVarBool retrieves a value from the environment and interprets it as a +// bool value -- 0/n/false == false, 1/y/true == true func GetEnvVarBool(key string, fallback bool) bool { value := GetEnvVar(key, "") value = strings.ToLower(value) + if value == "" { return fallback - } else if value == "0" || value == "n" { + } else if value == "0" || value == "n" || value == "false" { return false - } else if value == "1" || value == "y" { + } else if value == "1" || value == "y" || value == "true" { return true } @@ -220,7 +212,7 @@ func ParseSizeString(str string) int64 { numStr := matches[1] num, err := strconv.Atoi(numStr) if err != nil { - Logf("Error converting number: %v\n", err) + log.Printf("Error converting number: %v\n", err) return 0 } diff --git a/cli/commands/vault/viewer/viewer.go b/cli/commands/vault/viewer/viewer.go index 9650298..82e528d 100644 --- a/cli/commands/vault/viewer/viewer.go +++ b/cli/commands/vault/viewer/viewer.go @@ -133,7 +133,7 @@ func downloadFile(id string, key []byte) ([]byte, error) { metadata.ID, strconv.Itoa(chunk)) chunkData, err := globals.API.DownloadFileChunk(url) - if err != nil { + if err != nil || len(chunkData) == 0 { return nil, err } diff --git a/cli/crypto/crypto.go b/cli/crypto/crypto.go index ab70f0b..83e33de 100644 --- a/cli/crypto/crypto.go +++ b/cli/crypto/crypto.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "errors" "fmt" "golang.org/x/crypto/argon2" "golang.org/x/crypto/blake2b" @@ -236,6 +237,10 @@ func EncryptChunk(key []byte, data []byte) ([]byte, error) { // the key is unable to decrypt the data, an error is returned, otherwise the // decrypted data is returned. func DecryptChunk(key []byte, chunk []byte) ([]byte, error) { + if len(chunk) <= constants.IVSize { + return nil, errors.New("invalid chunk size") + } + iv := chunk[:constants.IVSize] data := chunk[constants.IVSize:] diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..1b3591f --- /dev/null +++ b/config/README.md @@ -0,0 +1,15 @@ +# Kamal config (https://kamal-deploy.org) + +`.deploy.template.yml` is used to populate deployment configs for specific +environments (i.e. `deploy.dev.yml`, `deploy.prod.yml`, etc) using the +`deploy.sh` script in the root of the repo. This ensures that all .env +variables are used by the container, and that the deployment reaches the +correct server IPs. + +Common deployment configuration is in `deploy.yml`. + +An SSH-only yml can be added to further configure a specific env deployment +using a file named `ssh.[env].yml` (i.e. `ssh.prod.yml`). This should follow +the SSH config formatting described here: + +https://kamal-deploy.org/docs/configuration/ssh/ diff --git a/config/deploy.template.yml b/config/deploy.template.yml new file mode 100644 index 0000000..82921c1 --- /dev/null +++ b/config/deploy.template.yml @@ -0,0 +1,7 @@ +servers: + SERVER_IP_ADDRESSES + +# Inject ENV variables into containers (secrets come from .kamal/secrets.[mode]). +env: + secret: + ENV_LIST_SECRETS diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..50310b7 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,15 @@ +service: yeetfile +image: meddlehead/yeetfile + +registry: + server: registry.digitalocean.com + username: + - YEETFILE_KAMAL_REGISTRY_USERNAME + password: + - YEETFILE_KAMAL_REGISTRY_PASSWORD + +proxy: + forward_headers: true + +builder: + arch: amd64 diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..2763dd6 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +set -e + +MODE="$1" +ENV_FILE="$MODE.env" +SECRETS_FILE=".kamal/secrets.$MODE" +DEPLOY_FILE="config/deploy.$MODE.yml" +DEPLOY_FILE_TEMPLATE="config/deploy.template.yml" +SSH_FILE="config/ssh.$MODE.yml" +ENV_SECRETS_TMP_FILE="$(mktemp)" +SERVER_IPS_TMP_FILE="$(mktemp)" + +ENV_SECRETS_STR_REPL="ENV_LIST_SECRETS" +SERVER_IPS_STR_REPL="SERVER_IP_ADDRESSES" + +SERVER_IP_ENV_VAR_PREFIX="YEETFILE_SERVER_IP" + +function replace_in_file() { + TARGET="$1" + REPLACEMENT="$2" + FILE="$3" + + TMP_FILE=$(mktemp) + echo "$REPLACEMENT" > "$TMP_FILE" + + sed -e "/$TARGET/{ + r $TMP_FILE + d + }" "$FILE" > tmpfile && mv tmpfile "$FILE" + + rm "$TMP_FILE" +} + +if [ -z "$MODE" ]; then + echo "Missing first arg, should be 'dev', 'prod', etc." + exit 1 +fi + +if [ ! -f "$ENV_FILE" ]; then + echo "Env file $ENV_FILE does not exist" + exit 1 +fi + +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + +rm -f "$SECRETS_FILE" +rm -f "$ENV_SECRETS_TMP_FILE" +rm -f "$SERVER_IPS_TMP_FILE" +cp "$DEPLOY_FILE_TEMPLATE" "$DEPLOY_FILE" + +HAS_SERVER_IPS=0 + +while IFS='=' read -r var_name var_value; do + # Check if the line is a comment or blank + if [[ -n "$var_name" ]] && [[ ! "$var_name" == "#"* ]]; then + if [[ $var_name == "$SERVER_IP_ENV_VAR_PREFIX"* ]]; then + # Var is a server IP, add to the IPs list + echo " - $var_value" >> "$SERVER_IPS_TMP_FILE" + HAS_SERVER_IPS=1 + elif [[ $var_name == *"SSH"* ]]; then + # Replace SSH values in the deploy file directly + # (Kamal doesn't allow secrets in that section) + var_value=$(echo "${!var_name}" | sed 's/\//\\\//g') + sed -i "s/$var_name/$var_value/g" "$DEPLOY_FILE" + elif [[ "$var_name" == *"KAMAL"* ]] || [[ "$var_name" == *"YEETFILE"* ]]; then + # Add to env secrets list + echo "$var_name=\$$var_name" >> "$SECRETS_FILE" + if [[ ! $var_name == *"KAMAL"* ]]; then + echo " - $var_name" >> "$ENV_SECRETS_TMP_FILE" + fi + fi + fi +done < "$ENV_FILE" + +if [[ $HAS_SERVER_IPS -eq 0 ]]; then + echo "Server IPs not found!" + echo "These must be set in $ENV_FILE with" $SERVER_IP_ENV_VAR_PREFIX"_[N]=XX.XXX...." + echo "i.e." $SERVER_IP_ENV_VAR_PREFIX"_1=10.140.11.26" + exit 1 +fi + +# Update deploy file with contents +ENV_SECRETS=$(<"$ENV_SECRETS_TMP_FILE") +SERVER_IPS=$(<"$SERVER_IPS_TMP_FILE") + +replace_in_file "$ENV_SECRETS_STR_REPL" "$ENV_SECRETS" "$DEPLOY_FILE" +replace_in_file "$SERVER_IPS_STR_REPL" "$SERVER_IPS" "$DEPLOY_FILE" + +rm -f "$ENV_SECRETS_TMP_FILE" +rm -f "$SERVER_IPS_TMP_FILE" + +if [[ -f $SSH_FILE ]]; then + cat $SSH_FILE >> $DEPLOY_FILE +fi + +read -r -p "Show secrets? (y/N): " input && [[ "$input" == "y" ]] && \ + kamal secrets print -d "$MODE" +read -r -p "Ready to deploy? (y/N): " input && [[ "$input" == "y" ]] && \ + kamal deploy -d "$MODE" diff --git a/docker-compose.yml b/docker-compose.yml index b0232c0..8548cb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: POSTGRES_PASSWORD: ${YEETFILE_DB_PASS:-postgres} POSTGRES_DB: ${YEETFILE_DB_NAME:-yeetfile} #ports: - #- "5432:5432" # Map db port to host + # - "5432:5432" # Map db port to host healthcheck: test: [ "CMD-SHELL", "pg_isready -U postgres" ] interval: 3s