Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for different service names and container names #6

Merged
merged 1 commit into from
Dec 19, 2024
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ supabaseRealtimeUrl: "wss://your-project.supabase.co/realtime/v1/websocket"
supabaseAnonKey: "your-anon-key"
pathToDockerCompose: "./config/docker-compose.yaml"
frontendServiceName: "frontend"
frontendContainerName: "frontend"
backendServiceName: "backend"
backendContainerName: "backend"
statsServiceName: "stats"
statsContainerName: "stats"
table: "silos"
chainId: 10
```
Expand Down Expand Up @@ -126,8 +129,11 @@ blockscout-vc/
| `supabaseAnonKey` | Supabase Anonymous Key | Yes |
| `pathToDockerCompose` | Path to the Docker Compose file | Yes |
| `frontendServiceName` | Name of the frontend service | Yes |
| `frontendContainerName` | Name of the frontend container | Yes |
| `backendServiceName` | Name of the backend service | Yes |
| `backendContainerName` | Name of the backend container | Yes |
| `statsServiceName` | Name of the stats service | Yes |
| `statsContainerName` | Name of the stats container | Yes |
| `table` | Name of the table to listen to | Yes |
| `chainId` | Chain ID to listen to | Yes |

Expand Down
3 changes: 3 additions & 0 deletions config/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ supabaseRealtimeUrl: "wss://localhost:5432/realtime/v1/websocket"
supabaseAnonKey: "replace-with-actual-anon-key"
pathToDockerCompose: "./config/docker-compose.yaml"
frontendServiceName: "frontend"
frontendContainerName: "frontend"
backendServiceName: "backend"
backendContainerName: "backend"
statsServiceName: "stats"
statsContainerName: "stats"
table: "silos"
chainId: "replace-with-actual-chain-id"
54 changes: 38 additions & 16 deletions internal/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,40 @@ func NewDocker() *Docker {
}
}

type Container struct {
Name string
ServiceName string
}

// RecreateContainers stops, removes and recreates specified containers
// It uses docker-compose to handle the container lifecycle
func (d *Docker) RecreateContainers(containerNames []string) error {
func (d *Docker) RecreateContainers(containers []Container) error {
pathToDockerCompose := viper.GetString("pathToDockerCompose")
uniqueContainers := d.UniqueContainerNames(containerNames)
uniqueContainers := d.UniqueContainers(containers)

// Define the sequence of commands to execute
containerNames := d.GetContainerNames(uniqueContainers)
serviceNames := d.GetServiceNames(uniqueContainers)
commands := []struct {
args []string
desc string
errMessage string
}{
{
args: []string{"rm", "-f"},
args: append([]string{"rm", "-f"}, containerNames...),
desc: "Stopping and removing containers",
errMessage: "Error stopping and removing containers",
},
{
args: []string{"compose", "-f", pathToDockerCompose, "up", "-d", "--force-recreate"},
args: append([]string{"compose", "-f", pathToDockerCompose, "up", "-d", "--force-recreate"}, serviceNames...),
desc: "Recreating containers",
errMessage: "Error recreating containers",
},
}

// Execute each command in sequence
for _, cmd := range commands {
args := append(cmd.args, uniqueContainers...)

execCmd := exec.Command("docker", args...)
execCmd := exec.Command("docker", cmd.args...)
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr

Expand Down Expand Up @@ -123,15 +128,32 @@ func (d *Docker) UpdateServiceEnv(compose map[string]interface{}, serviceName st
}

// UniqueContainerNames returns a sorted list of unique container names
func (d *Docker) UniqueContainerNames(containerNames []string) []string {
unique := make(map[string]bool)
for _, name := range containerNames {
unique[name] = true
func (d *Docker) UniqueContainers(containers []Container) []Container {
unique := make(map[string]Container)
for _, container := range containers {
unique[container.Name] = container
}
keys := make([]string, 0, len(unique))
for k := range unique {
keys = append(keys, k)
uniqueContainers := make([]Container, 0, len(unique))
for _, container := range unique {
uniqueContainers = append(uniqueContainers, container)
}
return uniqueContainers
}

func (d *Docker) GetContainerNames(containers []Container) []string {
names := make([]string, 0, len(containers))
for _, container := range containers {
names = append(names, container.Name)
}
sort.Strings(names)
return names
}

func (d *Docker) GetServiceNames(containers []Container) []string {
names := make([]string, 0, len(containers))
for _, container := range containers {
names = append(names, container.ServiceName)
}
sort.Strings(keys)
return keys
sort.Strings(names)
return names
}
45 changes: 29 additions & 16 deletions internal/handlers/coin.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handlers

import (
"blockscout-vc/internal/docker"
"fmt"

"github.com/spf13/viper"
Expand Down Expand Up @@ -33,34 +34,46 @@ func (h *CoinHandler) Handle(record *Record) HandlerResult {
result.Error = fmt.Errorf("failed to read compose file: %w", err)
return result
}
frontendService := viper.GetString("frontendServiceName")
backendService := viper.GetString("backendServiceName")
statsService := viper.GetString("statsServiceName")

// Define environment updates for each service
updates := map[string]map[string]interface{}{
frontendService: {
"NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL": record.Coin,
updates := []EnvUpdate{
{
ServiceName: viper.GetString("frontendServiceName"),
Key: "NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL",
Value: record.Coin,
ContainerName: viper.GetString("frontendContainerName"),
},
backendService: {
"COIN": record.Coin,
{
ServiceName: viper.GetString("backendServiceName"),
Key: "COIN",
Value: record.Coin,
ContainerName: viper.GetString("backendContainerName"),
},
statsService: {
"STATS_CHARTS__TEMPLATE_VALUES__NATIVE_COIN_SYMBOL": record.Coin,
{
ServiceName: viper.GetString("statsServiceName"),
Key: "STATS_CHARTS__TEMPLATE_VALUES__NATIVE_COIN_SYMBOL",
Value: record.Coin,
ContainerName: viper.GetString("statsContainerName"),
},
}

// Define environment updates for each service

// Apply updates to each service
for service, env := range updates {
for _, env := range updates {
var updated bool
compose, updated, err = h.docker.UpdateServiceEnv(compose, service, env)
compose, updated, err = h.docker.UpdateServiceEnv(compose, env.ServiceName, map[string]interface{}{
env.Key: env.Value,
})
if err != nil {
result.Error = fmt.Errorf("failed to update %s service environment: %w", service, err)
result.Error = fmt.Errorf("failed to update %s service environment: %w", env.ServiceName, err)
return result
}
if updated {
fmt.Printf("Updated %s service environment: %+v\n", service, env)
result.ContainersToRestart = append(result.ContainersToRestart, service)
fmt.Printf("Updated %s service environment: %+v\n", env.ServiceName, env)
result.ContainersToRestart = append(result.ContainersToRestart, docker.Container{
Name: env.ContainerName,
ServiceName: env.ServiceName,
})
}
}

Expand Down
69 changes: 54 additions & 15 deletions internal/handlers/image.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package handlers

import (
"blockscout-vc/internal/docker"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/spf13/viper"
)
Expand All @@ -11,11 +16,15 @@ const MaxImageLength = 2000

type ImageHandler struct {
BaseHandler
client *http.Client
}

func NewImageHandler() *ImageHandler {
return &ImageHandler{
BaseHandler: NewBaseHandler(),
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}

Expand All @@ -35,34 +44,32 @@ func (h *ImageHandler) Handle(record *Record) HandlerResult {
return result
}

frontendService := viper.GetString("frontendServiceName")
frontendServiceName := viper.GetString("frontendServiceName")
frontendContainerName := viper.GetString("frontendContainerName")

updates := map[string]map[string]interface{}{
frontendService: {},
frontendServiceName: {},
}

// Validate and update light logo URL
if err := h.validateImage(record.LightLogoURL); err != nil {
result.Error = fmt.Errorf("invalid light logo URL: %w", err)
return result
} else {
updates[frontendService]["NEXT_PUBLIC_NETWORK_LOGO"] = record.LightLogoURL
updates[frontendServiceName]["NEXT_PUBLIC_NETWORK_LOGO"] = record.LightLogoURL
}

// Validate and update dark logo URL
if err := h.validateImage(record.DarkLogoURL); err != nil {
result.Error = fmt.Errorf("invalid dark logo URL: %w", err)
return result
} else {
updates[frontendService]["NEXT_PUBLIC_NETWORK_LOGO_DARK"] = record.DarkLogoURL
updates[frontendServiceName]["NEXT_PUBLIC_NETWORK_LOGO_DARK"] = record.DarkLogoURL
}

// Validate and update favicon URL
if err := h.validateImage(record.FaviconURL); err != nil {
result.Error = fmt.Errorf("invalid favicon URL: %w", err)
return result
} else {
updates[frontendService]["NEXT_PUBLIC_NETWORK_ICON"] = record.FaviconURL
updates[frontendServiceName]["NEXT_PUBLIC_NETWORK_ICON"] = record.FaviconURL
}

// Apply updates to services
Expand All @@ -75,7 +82,12 @@ func (h *ImageHandler) Handle(record *Record) HandlerResult {
}
if updated {
fmt.Printf("Updated %s service environment: %+v\n", service, env)
result.ContainersToRestart = append(result.ContainersToRestart, service)
fmt.Printf("Frontend container name: %s\n", frontendContainerName)
fmt.Printf("Frontend service name: %s\n", frontendServiceName)
result.ContainersToRestart = append(result.ContainersToRestart, docker.Container{
Name: frontendContainerName,
ServiceName: frontendServiceName,
})
}
}

Expand All @@ -89,15 +101,42 @@ func (h *ImageHandler) Handle(record *Record) HandlerResult {
}

// validateImage checks if the image URL meets the required criteria
func (h *ImageHandler) validateImage(image string) error {
if image == "" {
return fmt.Errorf("image cannot be empty")
}
if len(image) == 0 {
func (h *ImageHandler) validateImage(imageURL string) error {
if imageURL == "" {
return fmt.Errorf("image cannot be empty")
}
if len(image) > MaxImageLength {

if len(imageURL) > MaxImageLength {
return fmt.Errorf("image length cannot exceed %d characters", MaxImageLength)
}

// Parse and validate URL
parsedURL, err := url.Parse(imageURL)
if err != nil {
return fmt.Errorf("invalid URL format: %w", err)
}

// Check if scheme is http or https
if !strings.HasPrefix(parsedURL.Scheme, "http") {
return fmt.Errorf("URL must start with http:// or https://")
}

// Check if image is accessible
resp, err := h.client.Head(imageURL)
if err != nil {
return fmt.Errorf("failed to access image: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("image not accessible, status code: %d", resp.StatusCode)
}

// Optionally verify content type
contentType := resp.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "image/") {
return fmt.Errorf("URL does not point to an image (content-type: %s)", contentType)
}

return nil
}
11 changes: 9 additions & 2 deletions internal/handlers/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ type Handler interface {

// HandlerResult represents the outcome of a handler's processing
type HandlerResult struct {
Error error // Any error that occurred during handling
ContainersToRestart []string // List of container names that need to be restarted
Error error // Any error that occurred during handling
ContainersToRestart []docker.Container // List of container names that need to be restarted
}

// Record represents the common data structure for all handlers
Expand All @@ -37,3 +37,10 @@ func NewBaseHandler() BaseHandler {
docker: docker.NewDocker(),
}
}

type EnvUpdate struct {
ServiceName string
Key string
Value string
ContainerName string
}
3 changes: 2 additions & 1 deletion internal/subscription/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package subscription

import (
"blockscout-vc/internal/client"
"blockscout-vc/internal/docker"
"blockscout-vc/internal/handlers"
"blockscout-vc/internal/worker"
"database/sql"
Expand Down Expand Up @@ -150,7 +151,7 @@ func (p *PostgresChanges) HandleMessage() error {
handlers.NewImageHandler(),
}

containersToRestart := []string{}
containersToRestart := []docker.Container{}
for _, handler := range handlers {
result := handler.Handle(&p.Payload.Data.Record)
if result.Error != nil {
Expand Down
Loading
Loading