feat: enhance Dockerfile for SQLite support and update main.go for MySQL connection; add sensor creation endpoint and improve wait-for-mqtt script
This commit is contained in:
+5
-2
@@ -2,16 +2,19 @@
|
|||||||
FROM golang:1.21-alpine AS builder
|
FROM golang:1.21-alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
# sqlite3 driver requires CGO; install build deps
|
||||||
|
RUN apk add --no-cache build-base sqlite-dev
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||||
go build -ldflags "-s -w" -o /app/server ./cmd/server
|
go build -ldflags "-s -w" -o /app/server ./cmd/server
|
||||||
|
|
||||||
# Final image
|
# Final image
|
||||||
FROM alpine:3.18
|
FROM alpine:3.18
|
||||||
RUN apk add --no-cache ca-certificates netcat-openbsd curl
|
RUN apk add --no-cache ca-certificates netcat-openbsd curl sqlite-libs
|
||||||
COPY --from=builder /app/server /usr/local/bin/server
|
COPY --from=builder /app/server /usr/local/bin/server
|
||||||
COPY scripts/wait-for-mqtt.sh /usr/local/bin/wait-for-mqtt.sh
|
COPY scripts/wait-for-mqtt.sh /usr/local/bin/wait-for-mqtt.sh
|
||||||
RUN chmod +x /usr/local/bin/wait-for-mqtt.sh
|
RUN chmod +x /usr/local/bin/wait-for-mqtt.sh
|
||||||
|
|||||||
+14
-16
@@ -29,16 +29,16 @@ func main() {
|
|||||||
log.Fatalf("failed to load config: %v", err)
|
log.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to database
|
// connect to MySQL database
|
||||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", cfg.Database.User, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", cfg.Database.User, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
|
||||||
db, err := sql.Open("mysql", dsn)
|
sqlDB, err := sql.Open("mysql", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to connect to database: %v", err)
|
log.Fatalf("failed to connect to database: %v", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer sqlDB.Close()
|
||||||
|
|
||||||
// test connection
|
// test connection
|
||||||
if err := db.Ping(); err != nil {
|
if err := sqlDB.Ping(); err != nil {
|
||||||
log.Fatalf("failed to ping database: %v", err)
|
log.Fatalf("failed to ping database: %v", err)
|
||||||
}
|
}
|
||||||
log.Println("connected to database")
|
log.Println("connected to database")
|
||||||
@@ -55,7 +55,7 @@ func main() {
|
|||||||
// connect to MQTT broker (best-effort)
|
// connect to MQTT broker (best-effort)
|
||||||
var mq *mqttclient.Client
|
var mq *mqttclient.Client
|
||||||
// initialize sqlite for state messages
|
// initialize sqlite for state messages
|
||||||
var db *storage.DB
|
var stateDB *storage.DB
|
||||||
{
|
{
|
||||||
dbPath := os.Getenv("SQLITE_PATH")
|
dbPath := os.Getenv("SQLITE_PATH")
|
||||||
if dbPath == "" {
|
if dbPath == "" {
|
||||||
@@ -65,8 +65,8 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("warning: sqlite init failed: %v", err)
|
log.Printf("warning: sqlite init failed: %v", err)
|
||||||
} else {
|
} else {
|
||||||
db = dbInit
|
stateDB = dbInit
|
||||||
storage.SetDefault(db)
|
storage.SetDefault(stateDB)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.MQTT.Broker != "" {
|
if cfg.MQTT.Broker != "" {
|
||||||
@@ -108,7 +108,7 @@ func main() {
|
|||||||
r.Use(gin.Recovery())
|
r.Use(gin.Recovery())
|
||||||
r.Use(middleware.GinLogger())
|
r.Use(middleware.GinLogger())
|
||||||
|
|
||||||
h := &handler.Handler{DB: db, JWTSecret: cfg.Server.JWTSecret}
|
h := &handler.Handler{DB: sqlDB, JWTSecret: cfg.Server.JWTSecret}
|
||||||
|
|
||||||
// Public routes
|
// Public routes
|
||||||
r.GET("/health", h.Health)
|
r.GET("/health", h.Health)
|
||||||
@@ -122,6 +122,9 @@ func main() {
|
|||||||
auth := r.Group("/")
|
auth := r.Group("/")
|
||||||
auth.Use(middleware.AuthMiddleware(cfg.Server.JWTSecret))
|
auth.Use(middleware.AuthMiddleware(cfg.Server.JWTSecret))
|
||||||
auth.GET("/protected", h.Protected)
|
auth.GET("/protected", h.Protected)
|
||||||
|
auth.GET("/mqttping", h.MQTTPing)
|
||||||
|
auth.GET("/messages", handler.GetMessages)
|
||||||
|
auth.POST("/sensors", h.CreateSensor)
|
||||||
|
|
||||||
// Device CRUD routes
|
// Device CRUD routes
|
||||||
auth.POST("/devices", h.CreateDevice)
|
auth.POST("/devices", h.CreateDevice)
|
||||||
@@ -152,15 +155,10 @@ func main() {
|
|||||||
if mq != nil {
|
if mq != nil {
|
||||||
mq.Close()
|
mq.Close()
|
||||||
}
|
}
|
||||||
if db != nil {
|
if stateDB != nil {
|
||||||
db.Close()
|
stateDB.Close()
|
||||||
}
|
}
|
||||||
log.Println("server exiting")
|
log.Println("server exiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getJWTSecret() string {
|
// removed unused getJWTSecret helper; configuration provides JWTSecret
|
||||||
if s := os.Getenv("JWT_SECRET"); s != "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return "secret"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,10 +68,10 @@ func (h *Handler) Login(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *Handler) CreateSensor(c *gin.Context) {
|
func (h *Handler) CreateSensor(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
DeviceID string `json:"device_id" binding:"required,uuid"`
|
DeviceID string `json:"device_id" binding:"required,uuid"`
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Type string `json:"type" binding:"required"`
|
Type string `json:"type" binding:"required"`
|
||||||
Data_Type string `json:"data_type" binding:"required"`
|
DataTypeID int `json:"data_type_id" binding:"required,min=1"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
@@ -84,9 +84,9 @@ func (h *Handler) CreateSensor(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create sensor in database
|
// Create sensor in database (schema uses data_type_id)
|
||||||
sensorID := uuid.New()
|
sensorID := uuid.New()
|
||||||
_, err = h.DB.Exec("INSERT INTO sensors (id, device_id, name, type, data_type) VALUES (UUID_TO_BIN(?), UUID_TO_BIN(?), ?, ?, ?)", sensorID.String(), deviceUUID.String(), req.Name, req.Type, req.Data_Type)
|
_, err = h.DB.Exec("INSERT INTO sensors (id, device_id, name, type, data_type_id) VALUES (UUID_TO_BIN(?), UUID_TO_BIN(?), ?, ?, ?)", sensorID.String(), deviceUUID.String(), req.Name, req.Type, req.DataTypeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create sensor"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create sensor"})
|
||||||
return
|
return
|
||||||
@@ -246,6 +246,23 @@ func (h *Handler) Protected(c *gin.Context) {
|
|||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "no claims"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "no claims"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package-level handlers used by tests
|
||||||
|
func Health(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hello(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "hello from lambdaiot"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Protected(c *gin.Context) {
|
||||||
|
if v, ok := c.Get("claims"); ok {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"claims": v})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "no claims"})
|
||||||
|
}
|
||||||
|
|
||||||
// GetMessages returns the last 100 state messages with optional pagination
|
// GetMessages returns the last 100 state messages with optional pagination
|
||||||
// query parameter: ?page=N (0-based)
|
// query parameter: ?page=N (0-based)
|
||||||
func GetMessages(c *gin.Context) {
|
func GetMessages(c *gin.Context) {
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ Repository: https://git.piskot.si/SeminarM2/lambdaiot-core
|
|||||||
|
|
||||||
## Features ✅
|
## Features ✅
|
||||||
|
|
||||||
- Simple HTTP server with two endpoints:
|
- HTTP API with health, greeting, auth, device CRUD, sensor creation, MQTT ping, and stored message retrieval
|
||||||
- `GET /health` — basic health check
|
- JWT-based auth middleware with demo login (`admin`/`password`)
|
||||||
- `GET /hello` — example greeting endpoint
|
- MQTT client with startup publish and best-effort subscription to persist `state:` topics into SQLite
|
||||||
- Tests for handlers using `httptest`
|
- SQLite sidecar (file) for recent MQTT state messages
|
||||||
- Multi-stage `Dockerfile` for small production images
|
- Multi-stage `Dockerfile` (CGO-enabled for sqlite3) and `Makefile` for common tasks
|
||||||
- `Makefile` with common tasks (build, run, test, docker-build)
|
- `test/docker-compose.yml` spins up MySQL, Mosquitto, phpMyAdmin, and the server for local integration
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,16 +42,35 @@ Then run it:
|
|||||||
docker run -p 8080:8080 lambdaiot-core:latest
|
docker run -p 8080:8080 lambdaiot-core:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Integration stack (MySQL + Mosquitto + server):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd test
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
The compose file seeds the database from `ai-improved.sql` and exposes:
|
||||||
|
- API on http://localhost:8080
|
||||||
|
- MySQL on localhost:3306 (root/rootpass)
|
||||||
|
- Mosquitto on localhost:1883
|
||||||
|
- phpMyAdmin on http://localhost:8081
|
||||||
|
|
||||||
### Endpoints
|
### Endpoints
|
||||||
|
|
||||||
- `http://localhost:8080/health` — returns `{ "status": "ok" }`
|
- `GET /health` — returns `{ "status": "ok" }`
|
||||||
- `http://localhost:8080/hello` — returns a small greeting JSON
|
- `GET /hello` — greeting JSON
|
||||||
- `POST http://localhost:8080/login` — demo login, JSON body: `{ "username": "admin", "password": "password" }`, returns `{ "token": "..." }`
|
- `POST /login` — demo login, body `{ "username": "admin", "password": "password" }`, returns `{ "token": "..." }`
|
||||||
- `GET http://localhost:8080/protected` — protected endpoint requiring `Authorization: Bearer <token>` header
|
- `GET /protected` — JWT required
|
||||||
|
- `GET /devices` — list devices (public) / `POST /devices`, `GET/PUT/DELETE /devices/:id` (JWT)
|
||||||
|
- `POST /sensors` — create sensor (JWT)
|
||||||
|
- `GET /mqttping` — publish timestamp to MQTT default topic (JWT)
|
||||||
|
- `GET /messages` — last stored MQTT state messages from SQLite (JWT)
|
||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
- `JWT_SECRET` — (optional) secret used to sign tokens; defaults to `secret` for local/dev use
|
- `JWT_SECRET` — (optional) secret used to sign tokens; defaults to `secret` for local/dev use
|
||||||
|
- `MQTT_*` — configure broker, client ID, topic, username/password
|
||||||
|
- `DB_*` — MySQL connection overrides; see `config.toml` defaults in `internal/config`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,22 @@ while ! nc -z "$HOST" "$PORT"; do
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Broker $HOST:$PORT is up, starting server"
|
echo "Broker $HOST:$PORT is up"
|
||||||
|
|
||||||
|
# Optionally wait for MySQL if DB_HOST/DB_PORT are set
|
||||||
|
DB_HOST=${DB_HOST:-}
|
||||||
|
DB_PORT=${DB_PORT:-}
|
||||||
|
if [ -n "$DB_HOST" ]; then
|
||||||
|
if [ -z "$DB_PORT" ]; then
|
||||||
|
DB_PORT=3306
|
||||||
|
fi
|
||||||
|
echo "Waiting for MySQL $DB_HOST:$DB_PORT..."
|
||||||
|
while ! nc -z "$DB_HOST" "$DB_PORT"; do
|
||||||
|
echo "mysql not ready, retrying..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "MySQL $DB_HOST:$DB_PORT is up"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting server"
|
||||||
exec /usr/local/bin/server
|
exec /usr/local/bin/server
|
||||||
|
|||||||
Reference in New Issue
Block a user