feat(docker-watcher): phase 11 - frontend embed & SSE
Embed SvelteKit static build in Go binary via go:embed. Event bus for pub/sub with deploy log, instance status, and deploy status events. SSE endpoints for real-time streaming. Frontend SSE client with exponential backoff reconnection. Makefile for build pipeline. Update Phase 12 auth plan with OAuth2/OIDC support.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/alexei/docker-watcher/internal/crypto"
|
||||
"github.com/alexei/docker-watcher/internal/docker"
|
||||
"github.com/alexei/docker-watcher/internal/events"
|
||||
"github.com/alexei/docker-watcher/internal/health"
|
||||
"github.com/alexei/docker-watcher/internal/notify"
|
||||
"github.com/alexei/docker-watcher/internal/npm"
|
||||
@@ -25,9 +26,15 @@ type Deployer struct {
|
||||
store *store.Store
|
||||
health *health.Checker
|
||||
notifier *notify.Notifier
|
||||
eventBus EventPublisher
|
||||
encKey [32]byte
|
||||
}
|
||||
|
||||
// EventPublisher is the interface for publishing events to the event bus.
|
||||
type EventPublisher interface {
|
||||
Publish(evt events.Event)
|
||||
}
|
||||
|
||||
// New creates a new Deployer with all required dependencies.
|
||||
func New(
|
||||
dockerClient *docker.Client,
|
||||
@@ -35,6 +42,7 @@ func New(
|
||||
st *store.Store,
|
||||
checker *health.Checker,
|
||||
notifier *notify.Notifier,
|
||||
eventBus EventPublisher,
|
||||
encKey [32]byte,
|
||||
) *Deployer {
|
||||
return &Deployer{
|
||||
@@ -43,6 +51,7 @@ func New(
|
||||
store: st,
|
||||
health: checker,
|
||||
notifier: notifier,
|
||||
eventBus: eventBus,
|
||||
encKey: encKey,
|
||||
}
|
||||
}
|
||||
@@ -91,6 +100,7 @@ func (d *Deployer) TriggerDeploy(ctx context.Context, projectID, stageID, imageT
|
||||
|
||||
if deployErr != nil {
|
||||
d.logDeploy(deploy.ID, fmt.Sprintf("Deploy failed: %v", deployErr), "error")
|
||||
d.publishDeployStatus(deploy.ID, projectID, stageID, imageTag, "failed", deployErr.Error())
|
||||
d.rollback(ctx, deploy.ID, containerID, npmProxyID, instanceID)
|
||||
|
||||
d.notifier.Send(settings.NotificationURL, notify.Event{
|
||||
@@ -108,6 +118,7 @@ func (d *Deployer) TriggerDeploy(ctx context.Context, projectID, stageID, imageT
|
||||
if err := d.store.UpdateDeployStatus(deploy.ID, "success", ""); err != nil {
|
||||
log.Printf("deployer: update deploy status to success: %v", err)
|
||||
}
|
||||
d.publishDeployStatus(deploy.ID, projectID, stageID, imageTag, "success", "")
|
||||
|
||||
subdomain := d.buildSubdomain(project, stage, settings, imageTag)
|
||||
fullURL := fmt.Sprintf("https://%s.%s", subdomain, settings.Domain)
|
||||
@@ -144,6 +155,7 @@ func (d *Deployer) executeDeploy(
|
||||
if err := d.store.UpdateDeployStatus(deployID, "pulling", ""); err != nil {
|
||||
log.Printf("deployer: update deploy status: %v", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "pulling", "")
|
||||
d.logDeploy(deployID, fmt.Sprintf("Pulling image %s:%s", project.Image, imageTag), "info")
|
||||
|
||||
authConfig, err := d.buildRegistryAuth(project)
|
||||
@@ -167,6 +179,7 @@ func (d *Deployer) executeDeploy(
|
||||
if err := d.store.UpdateDeployStatus(deployID, "starting", ""); err != nil {
|
||||
log.Printf("deployer: update deploy status: %v", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "starting", "")
|
||||
|
||||
// Pre-generate instance ID so it can be set as a container label.
|
||||
instanceID = uuid.New().String()
|
||||
@@ -224,12 +237,14 @@ func (d *Deployer) executeDeploy(
|
||||
if err := d.store.UpdateInstanceStatus(instanceID, "running"); err != nil {
|
||||
log.Printf("deployer: update instance status to running: %v", err)
|
||||
}
|
||||
d.publishInstanceStatus(instanceID, project.ID, stage.ID, "running")
|
||||
d.logDeploy(deployID, "Container started", "info")
|
||||
|
||||
// Step 4: Configure NPM proxy.
|
||||
if err := d.store.UpdateDeployStatus(deployID, "configuring_proxy", ""); err != nil {
|
||||
log.Printf("deployer: update deploy status: %v", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "configuring_proxy", "")
|
||||
|
||||
npmProxyID, err = d.configureProxy(ctx, deployID, settings, containerName, project.Port, subdomain)
|
||||
if err != nil {
|
||||
@@ -248,6 +263,7 @@ func (d *Deployer) executeDeploy(
|
||||
if err := d.store.UpdateDeployStatus(deployID, "health_checking", ""); err != nil {
|
||||
log.Printf("deployer: update deploy status: %v", err)
|
||||
}
|
||||
d.publishDeployStatus(deployID, project.ID, stage.ID, imageTag, "health_checking", "")
|
||||
|
||||
healthURL := fmt.Sprintf("http://%s:%d%s", containerName, project.Port, project.Healthcheck)
|
||||
d.logDeploy(deployID, fmt.Sprintf("Running health check: %s", healthURL), "info")
|
||||
@@ -466,11 +482,54 @@ func (d *Deployer) parseEnvVars(envJSON string) []string {
|
||||
return vars
|
||||
}
|
||||
|
||||
// logDeploy appends a log entry for a deploy. Errors are logged to stderr but not propagated.
|
||||
// logDeploy appends a log entry for a deploy and publishes it on the event bus.
|
||||
// Errors are logged to stderr but not propagated.
|
||||
func (d *Deployer) logDeploy(deployID, message, level string) {
|
||||
if err := d.store.AppendDeployLog(deployID, message, level); err != nil {
|
||||
log.Printf("deployer: append deploy log: %v", err)
|
||||
}
|
||||
if d.eventBus != nil {
|
||||
d.eventBus.Publish(events.Event{
|
||||
Type: events.EventDeployLog,
|
||||
Payload: events.DeployLogPayload{
|
||||
DeployID: deployID,
|
||||
Message: message,
|
||||
Level: level,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// publishDeployStatus publishes a deploy status change event on the bus.
|
||||
func (d *Deployer) publishDeployStatus(deployID, projectID, stageID, imageTag, status, deployErr string) {
|
||||
if d.eventBus != nil {
|
||||
d.eventBus.Publish(events.Event{
|
||||
Type: events.EventDeployStatus,
|
||||
Payload: events.DeployStatusPayload{
|
||||
DeployID: deployID,
|
||||
ProjectID: projectID,
|
||||
StageID: stageID,
|
||||
ImageTag: imageTag,
|
||||
Status: status,
|
||||
Error: deployErr,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// publishInstanceStatus publishes an instance status change event on the bus.
|
||||
func (d *Deployer) publishInstanceStatus(instanceID, projectID, stageID, status string) {
|
||||
if d.eventBus != nil {
|
||||
d.eventBus.Publish(events.Event{
|
||||
Type: events.EventInstanceStatus,
|
||||
Payload: events.InstanceStatusPayload{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
StageID: stageID,
|
||||
Status: status,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// truncateID safely truncates a Docker ID to 12 characters for display.
|
||||
|
||||
Reference in New Issue
Block a user