Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Dockerfile.data-service
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Stage 1: Build Go application
FROM golang:1.25.1-alpine AS go-builder

# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata

# Set working directory
WORKDIR /build

# Copy the main module files first (needed as dependency)
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
COPY . .

# Build the data-service binary
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o data-service ./data-service

# Stage 2: Final runtime image
FROM alpine:latest

# Install runtime dependencies
RUN apk add --no-cache ca-certificates tzdata

ENV TZ=Europe/Riga

# Create non-root user
RUN addgroup -g 1001 data-service && \
adduser -D -u 1001 -G data-service data-service

# Set working directory
WORKDIR /app

# Copy binary from builder stage
COPY --from=go-builder /build/data-service/data-service .

# Switch to non-root user
USER data-service

# Expose application port
EXPOSE 8081

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/mpc/get || exit 1

# Default command
ENTRYPOINT ["./data-service"]
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ dev-watch: ## Run with file watching (requires entr: brew install entr)
@which entr > /dev/null || (echo "entr not installed for file watching" && exit 1)
find . -name '*.go' | entr -r go run . -price-limit=50.0 -network=192.168.1.0/24

# Data-service Docker targets
DATA_SERVICE_IMAGE=data-service
DATA_SERVICE_DOCKERFILE=Dockerfile.data-service

docker-data-service-multi: ## Build Docker image for data-service for multiple platforms
docker buildx build --platform $(PLATFORMS) -t $(DATA_SERVICE_IMAGE):$(DOCKER_TAG) -f $(DATA_SERVICE_DOCKERFILE) .

docker-data-service: ## Build Docker image for data-service for ARM7 (Raspberry Pi)
rm -f ems-data-service-working.tar
docker buildx build --platform linux/arm/v7 --no-cache --output=type=docker -t $(DATA_SERVICE_IMAGE):latest-arm7 -f $(DATA_SERVICE_DOCKERFILE) .
docker save $(DATA_SERVICE_IMAGE):latest-arm7 > data-service.tar
skopeo copy docker-archive:data-service.tar docker-archive:ems-data-service-working.tar
cp ems-data-service-working.tar ~/Downloads/ems-data-service-working.tar

# Docker targets
docker: ## Build Docker image for ARM7 (Raspberry Pi)
rm -f ems-working.tar
Expand Down
Binary file added data-service/data-service
Binary file not shown.
101 changes: 101 additions & 0 deletions data-service/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Package main implements the data-service HTTP server for storing and retrieving MPC control decisions.
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"

"github.com/devskill-org/ems/mpc"
)

var (
decisions []mpc.ControlDecision
decisionsMu sync.RWMutex
)

func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8081"
}

mux := http.NewServeMux()
mux.HandleFunc("/mpc/save", handleMPCSave)
mux.HandleFunc("/mpc/get", handleMPCGet)

server := &http.Server{
Addr: ":" + port,
Handler: mux,
ReadHeaderTimeout: 1 * time.Second,
}

go func() {
log.Printf("data-service HTTP server listening on :%s", port)

Check failure on line 41 in data-service/main.go

View workflow job for this annotation

GitHub Actions / Lint Code

G706: Log injection via taint analysis (gosec)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

log.Println("Shutting down data-service...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("data-service stopped")
}

func handleMPCSave(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}

var saved []mpc.ControlDecision
if err := json.NewDecoder(r.Body).Decode(&saved); err != nil {
http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
return
}

decisionsMu.Lock()
decisions = saved
decisionsMu.Unlock()

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"decisions_saved": len(saved),
}); err != nil {
log.Printf("failed to encode response: %v", err)
}
}

func handleMPCGet(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}

decisionsMu.RLock()
snapshot := make([]mpc.ControlDecision, len(decisions))
copy(snapshot, decisions)
decisionsMu.RUnlock()

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(snapshot); err != nil {
log.Printf("failed to encode response: %v", err)
}
}
1 change: 1 addition & 0 deletions scheduler/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type Config struct {
PVPollInterval time.Duration `json:"pv_poll_interval"` // Poll interval for PV power (duration)
PVIntegrationPeriod time.Duration `json:"pv_integration_period"` // Integration period for PV power (duration)
PostgresConnString string `json:"postgres_conn_string"` // PostgreSQL connection string
DataServiceURL string `json:"data_service_url"` // URL of the data-service HTTP server for MPC decisions

// Weather API settings
WeatherUpdateInterval time.Duration `json:"weather_update_interval"` // How often to update weather
Expand Down
Loading
Loading