Compare commits

..

10 Commits

Author SHA1 Message Date
Blake Ridgway
5e737ebdd3 ci-ish: build script adapted for the admin panel 2025-11-21 08:18:40 -06:00
Blake Ridgway
59514f721a Merge pull request #3 from RideAware/feat/go-rewrite
Fix podman building, syntax, updated packages
2025-11-17 08:54:59 -06:00
Cipher Vance
f6029f4a66 fix: fixed syntax error 2025-11-17 08:37:42 -06:00
Cipher Vance
66300be495 chore: updated package versions 2025-11-17 08:37:27 -06:00
Cipher Vance
a92e836cc1 ci: golang to 1.25 2025-11-17 08:37:02 -06:00
Blake Ridgway
97f25814a1 Merge pull request #1 from RideAware/feat/go-rewrite
Feat: Rewrite from Python to Go
2025-11-15 20:02:40 -06:00
Cipher Vance
1b91b72ffb set tls version to 1.2 2025-11-15 19:11:39 -06:00
Cipher Vance
85e49c9e9f some more email work for smtp errors 2025-11-15 18:48:14 -06:00
Cipher Vance
9444bab05f add support for better ssl/tls handling 2025-11-15 18:43:51 -06:00
Cipher Vance
ae3a484139 add gin_mode env 2025-11-15 15:41:28 -06:00
6 changed files with 282 additions and 66 deletions

View File

@@ -1,5 +1,5 @@
# Stage 1: Build # Stage 1: Build
FROM docker.io/library/golang:1.23-alpine AS builder FROM docker.io/library/golang:1.25-alpine AS builder
WORKDIR /build WORKDIR /build

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"log" "log"
"os"
"github.com/rideaware/admin-panel/internal/config" "github.com/rideaware/admin-panel/internal/config"
"github.com/rideaware/admin-panel/internal/database" "github.com/rideaware/admin-panel/internal/database"
@@ -17,11 +18,22 @@ import (
// authenticated routes, and starts the HTTP server on the configured port. // authenticated routes, and starts the HTTP server on the configured port.
func main() { func main() {
cfg := config.Load() cfg := config.Load()
// Set Gin mode based on environment (default to release)
if os.Getenv("GIN_MODE") == "" {
gin.SetMode(gin.ReleaseMode)
}
middleware.Init() middleware.Init()
database.Init(cfg) database.Init(cfg)
defer database.Close() defer database.Close()
router := gin.Default() router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())
// Trust only localhost proxy in production
router.SetTrustedProxies([]string{"127.0.0.1", "localhost", "::1"})
router.LoadHTMLGlob("web/templates/*.html") router.LoadHTMLGlob("web/templates/*.html")
router.Static("/static", "web/static") router.Static("/static", "web/static")

13
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/rideaware/admin-panel module github.com/rideaware/admin-panel
go 1.23 go 1.24.0
toolchain go1.24.10 toolchain go1.24.10
@@ -9,8 +9,7 @@ require (
github.com/gorilla/sessions v1.4.0 github.com/gorilla/sessions v1.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/wneessen/go-mail v0.4.0 golang.org/x/crypto v0.36.0
golang.org/x/crypto v0.14.0
) )
require ( require (
@@ -33,9 +32,9 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.10.0 // indirect golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.29.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

24
go.sum
View File

@@ -23,7 +23,6 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -70,26 +69,23 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wneessen/go-mail v0.4.0 h1:Oo4HLIV8My7G9JuZkoOX6eipXQD+ACvIqURYeIzUc88=
github.com/wneessen/go-mail v0.4.0/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,25 +1,19 @@
package email package email
import ( import (
"crypto/tls"
"fmt" "fmt"
"log" "log"
"net/smtp"
"net/url" "net/url"
"strings" "strings"
"time"
"github.com/rideaware/admin-panel/internal/config" "github.com/rideaware/admin-panel/internal/config"
"github.com/rideaware/admin-panel/internal/database" "github.com/rideaware/admin-panel/internal/database"
"github.com/wneessen/go-mail"
) )
// SendUpdate sends a newsletter with the given subject and body to all subscriber emails stored in the database. // SendUpdate sends a newsletter with the given subject and body to all subscriber emails stored in the database.
// It returns a human-readable status message and, when subscriber retrieval fails, the underlying error. // It returns a human-readable status message and, when subscriber retrieval fails, the underlying error.
// - If retrieving subscribers fails: returns "Failed to retrieve subscribers" and the error.
// - If no subscribers are found: returns "No subscribers found." and nil.
// - If sending to a specific subscriber fails: returns "Failed to send to <email>" and nil.
// - On success: returns "Email has been sent to all subscribers." and nil.
// Note: logging the newsletter entry in the database is attempted after sending and any logging failure is non-fatal.
func SendUpdate(subject, body string) (string, error) { func SendUpdate(subject, body string) (string, error) {
subscribers, err := database.GetAllEmails() subscribers, err := database.GetAllEmails()
if err != nil { if err != nil {
@@ -28,6 +22,7 @@ func SendUpdate(subject, body string) (string, error) {
if len(subscribers) == 0 { if len(subscribers) == 0 {
return "No subscribers found.", nil return "No subscribers found.", nil
} }
var succeeded, failed int var succeeded, failed int
for _, email := range subscribers { for _, email := range subscribers {
if send(subject, body, email) { if send(subject, body, email) {
@@ -36,79 +31,119 @@ func SendUpdate(subject, body string) (string, error) {
failed++ failed++
} }
} }
if err := database.LogNewsletter(subject, body); err != nil { if err := database.LogNewsletter(subject, body); err != nil {
log.Printf("Error logging newsletter: %v", err) log.Printf("Error logging newsletter: %v", err)
} }
if failed == 0 { if failed == 0 {
return fmt.Sprintf("Email sent to all %d subscribers.", succeeded), nil return fmt.Sprintf("Email sent to all %d subscribers.", succeeded), nil
} }
return fmt.Sprintf("Sent to %d/%d subscribers; %d failed.", succeeded, succeeded+failed, failed), nil return fmt.Sprintf("Sent to %d/%d subscribers; %d failed.", succeeded, succeeded+failed, failed), nil
} }
// send constructs and sends an HTML newsletter update to the specified recipient using the current SMTP configuration. // send constructs and sends an HTML newsletter update to the specified recipient
// It embeds an unsubscribe link for the recipient and returns true if the message was sent successfully, false if client creation, message setup, or sending fails.
func send(subject, body, recipient string) bool { func send(subject, body, recipient string) bool {
cfg := config.Current cfg := config.Current
client, err := mail.NewClient( log.Printf("Attempting to send email to %s via %s:%d", recipient, cfg.SMTPServer, cfg.SMTPPort)
cfg.SMTPServer,
mail.WithPort(cfg.SMTPPort),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(cfg.SMTPUser),
mail.WithPassword(cfg.SMTPPassword),
mail.WithTimeout(10*time.Second),
)
if err != nil {
log.Printf("Failed to create mail client: %v", err)
return false
}
defer client.Close()
m := mail.NewMsg() addr := fmt.Sprintf("%s:%d", cfg.SMTPServer, cfg.SMTPPort)
if err := m.From(cfg.SenderEmail); err != nil {
log.Printf("Failed to set from: %v", err)
return false
}
if err := m.To(recipient); err != nil {
log.Printf("Failed to set to: %v", err)
return false
}
m.Subject(subject)
unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s", unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s",
cfg.BaseURL, url.QueryEscape(recipient)) cfg.BaseURL, url.QueryEscape(recipient))
// Build HTML body with unsubscribe link
htmlBody := buildHTMLBody(body, unsubLink) htmlBody := buildHTMLBody(body, unsubLink)
m.SetBodyString(mail.TypeTextHTML, htmlBody) message := buildMessage(cfg.SenderEmail, recipient, subject, htmlBody)
if err := client.Send(m); err != nil { // Create TLS connection
log.Printf("Failed to send email to %s: %v", recipient, err) tlsconfig := &tls.Config{
ServerName: cfg.SMTPServer,
MinVersion: tls.VersionTLS12,
}
conn, err := tls.Dial("tcp", addr, tlsconfig)
if err != nil {
log.Printf("Failed to connect to SMTP %s: %v", addr, err)
return false return false
} }
defer conn.Close()
// Create SMTP client
client, err := smtp.NewClient(conn, cfg.SMTPServer)
if err != nil {
log.Printf("Failed to create SMTP client: %v", err)
return false
}
defer client.Close()
// Authenticate
auth := smtp.PlainAuth("", cfg.SMTPUser, cfg.SMTPPassword, cfg.SMTPServer)
if err := client.Auth(auth); err != nil {
log.Printf("SMTP auth failed for %s: %v", cfg.SMTPUser, err)
return false
}
// Send the email
if err := client.Mail(cfg.SenderEmail); err != nil {
log.Printf("MAIL command failed: %v", err)
return false
}
if err := client.Rcpt(recipient); err != nil {
log.Printf("RCPT command failed for %s: %v", recipient, err)
return false
}
w, err := client.Data()
if err != nil {
log.Printf("DATA command failed: %v", err)
return false
}
_, err = w.Write([]byte(message))
if err != nil {
log.Printf("Failed to write message: %v", err)
return false
}
err = w.Close()
if err != nil {
log.Printf("Failed to close DATA: %v", err)
return false
}
client.Quit()
log.Printf("Update email sent to: %s", recipient) log.Printf("Update email sent to: %s", recipient)
return true return true
} }
func buildMessage(from, to, subject, body string) string {
msg := fmt.Sprintf("From: %s\r\n", from)
msg += fmt.Sprintf("To: %s\r\n", to)
msg += fmt.Sprintf("Subject: %s\r\n", subject)
msg += "MIME-Version: 1.0\r\n"
msg += "Content-Type: text/html; charset=\"utf-8\"\r\n"
msg += "\r\n"
msg += body
return msg
}
// buildHTMLBody constructs the final HTML email body by appending an unsubscribe footer to the user-provided content. // buildHTMLBody constructs the final HTML email body by appending an unsubscribe footer to the user-provided content.
// It handles both complete HTML documents and HTML fragments.
func buildHTMLBody(body, unsubLink string) string { func buildHTMLBody(body, unsubLink string) string {
footer := fmt.Sprintf( footer := fmt.Sprintf(
"<br><br><hr><p style='font-size: 12px; color: #666;'>If you ever wish to unsubscribe, "+ "<br><br><hr><p style='font-size: 12px; color: #666;'>If you ever wish to unsubscribe, "+
"please click <a href='%s'>here</a>.</p>", "please click <a href='%s'>here</a>.</p>",
unsubLink) unsubLink)
// If body contains closing html tag, insert before it
if strings.Contains(strings.ToLower(body), "</html>") { if strings.Contains(strings.ToLower(body), "</html>") {
return strings.Replace(body, "</html>", footer+"</html>", 1) return strings.Replace(body, "</html>", footer+"</html>", 1)
} }
// If body contains closing body tag, insert before it
if strings.Contains(strings.ToLower(body), "</body>") { if strings.Contains(strings.ToLower(body), "</body>") {
return strings.Replace(body, "</body>", footer+"</body>", 1) return strings.Replace(body, "</body>", footer+"</body>", 1)
} }
// Otherwise just append
return body + footer return body + footer
} }

174
scripts/build.sh Executable file
View File

@@ -0,0 +1,174 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
IMAGE_NAME="rideaware-admin"
IMAGE_TAG="latest"
NO_CACHE=false
RUN_CONTAINER=false
CONTAINER_NAME="rideaware-admin"
# Help function
show_help() {
cat << EOF
Usage: $0 [OPTIONS]
OPTIONS:
-t, --tag TAG Image tag (default: latest)
-n, --name NAME Image name (default: rideaware)
-r, --run Run container after build
-c, --container NAME Container name when running (default: rideaware-admin)
--no-cache Build without cache
-h, --help Show this help message
EXAMPLES:
$0 # Build as rideaware:latest
$0 -t v1.0 # Build as rideaware:v1.0
$0 -t dev --run # Build and run
$0 --no-cache -t prod # Build without cache as rideaware:prod
EOF
exit 0
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-t|--tag)
IMAGE_TAG="$2"
shift 2
;;
-n|--name)
IMAGE_NAME="$2"
shift 2
;;
-r|--run)
RUN_CONTAINER=true
shift
;;
-c|--container)
CONTAINER_NAME="$2"
shift 2
;;
--no-cache)
NO_CACHE=true
shift
;;
-h|--help)
show_help
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
show_help
;;
esac
done
FULL_IMAGE="$IMAGE_NAME:$IMAGE_TAG"
BUILD_ARGS=""
if [ "$NO_CACHE" = true ]; then
BUILD_ARGS="--no-cache"
fi
# Function to stop and remove container
cleanup_container() {
local name=$1
if podman ps -a --format "{{.Names}}" | grep -q "^${name}\$"; then
echo -e "${YELLOW}Removing existing container: $name${NC}"
# Stop if running
if podman ps --format "{{.Names}}" | grep -q "^${name}\$"; then
echo " Stopping container..."
podman kill "$name" 2>/dev/null || true
fi
# Remove
echo " Removing container..."
if podman rm "$name" 2>/dev/null; then
echo -e "${GREEN} ✓ Container removed${NC}"
else
echo -e "${RED} ✗ Failed to remove container${NC}"
return 1
fi
fi
return 0
}
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Building Podman Image ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
echo -e "${YELLOW}Image: $FULL_IMAGE${NC}"
echo ""
if ! podman build $BUILD_ARGS -f Dockerfile -t "$FULL_IMAGE" .; then
echo -e "${RED}✗ Build failed${NC}"
exit 1
fi
echo -e "${GREEN}✓ Image built successfully${NC}"
echo ""
# Show image info
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Image Details ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
podman images "$IMAGE_NAME:$IMAGE_TAG" \
--format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.Created}}"
echo ""
if [ "$RUN_CONTAINER" = true ]; then
echo -e "${BLUE}╔════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Starting Container ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════╝${NC}"
# Cleanup existing container
if ! cleanup_container "$CONTAINER_NAME"; then
echo -e "${RED}✗ Failed to clean up existing container${NC}"
exit 1
fi
echo ""
echo "Starting new container: $CONTAINER_NAME"
if podman run -d \
--name "$CONTAINER_NAME" \
-p 5001:5001 \
--env-file .env \
"$FULL_IMAGE"; then
echo -e "${GREEN}✓ Container running: $CONTAINER_NAME${NC}"
echo ""
# Wait for startup
sleep 2
echo -e "${YELLOW}Container logs:${NC}"
podman logs "$CONTAINER_NAME"
echo ""
echo -e "${GREEN}API available at: http://localhost:5001${NC}"
echo -e "${YELLOW}To view logs: podman logs -f $CONTAINER_NAME${NC}"
echo -e "${YELLOW}To stop: podman kill $CONTAINER_NAME${NC}"
else
echo -e "${RED}✗ Failed to start container${NC}"
exit 1
fi
else
echo -e "${YELLOW}To run the container:${NC}"
echo " podman run -d --name $CONTAINER_NAME -p 5001:5001 --env-file .env $FULL_IMAGE"
echo ""
echo -e "${YELLOW}Or use this script with --run:${NC}"
echo " $0 -t $IMAGE_TAG --run"
fi
echo ""
echo -e "${GREEN}✓ Done!${NC}"