Skip to content

Commit bbd0178

Browse files
Merge pull request #6 from akihikokuroda/nodeui
add node ui deploy
2 parents b28e78e + 0664d5c commit bbd0178

File tree

2 files changed

+221
-1
lines changed

2 files changed

+221
-1
lines changed

src/pkg/maestro/deploy.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
package maestro
55

66
import (
7+
"encoding/json"
78
"fmt"
9+
"net/http"
810
"os"
911
"os/exec"
1012
"path/filepath"
1113
"strings"
14+
"time"
1215

1316
"go.uber.org/zap"
1417
"gopkg.in/yaml.v3"
@@ -440,4 +443,210 @@ func CreateConfigMap(agentsYAML string, workflowYAML string) error {
440443
return nil
441444
}
442445

446+
// DeployDefault deploys the workflow with API server and UI (default deployment method).
447+
// This is similar to the Python __deploy_agents_workflow_node implementation.
448+
func (d *Deploy) DeployDefault() error {
449+
// Create temporary directory for deployment
450+
d.TmpDir = filepath.Join(os.TempDir(), "maestro")
451+
if err := os.MkdirAll(d.TmpDir, 0755); err != nil {
452+
return fmt.Errorf("failed to create temporary directory: %w", err)
453+
}
454+
455+
// Create src directory in the temporary directory
456+
srcDir := filepath.Join(d.TmpDir, "src")
457+
if err := os.MkdirAll(srcDir, 0755); err != nil {
458+
return fmt.Errorf("failed to create src directory: %w", err)
459+
}
460+
461+
// Write agent contents to file
462+
agentsFile := filepath.Join(srcDir, "agents.yaml")
463+
if err := os.WriteFile(agentsFile, []byte(d.Agent), 0644); err != nil {
464+
return fmt.Errorf("failed to write agents file: %w", err)
465+
}
466+
467+
// Write workflow contents to file
468+
workflowFile := filepath.Join(srcDir, "workflow.yaml")
469+
if err := os.WriteFile(workflowFile, []byte(d.Workflow), 0644); err != nil {
470+
return fmt.Errorf("failed to write workflow file: %w", err)
471+
}
472+
473+
// Get API host and port from environment or use defaults
474+
apiHost := os.Getenv("MAESTRO_API_HOST")
475+
if apiHost == "" {
476+
apiHost = "127.0.0.1"
477+
}
478+
479+
apiPortStr := os.Getenv("MAESTRO_API_PORT")
480+
if apiPortStr == "" {
481+
apiPortStr = "8000"
482+
}
483+
484+
uiPort := os.Getenv("MAESTRO_UI_PORT")
485+
if uiPort == "" {
486+
uiPort = "5173"
487+
}
488+
489+
// Convert port string to int
490+
apiPort := 8000
491+
if port, err := fmt.Sscanf(apiPortStr, "%d", &apiPort); err != nil || port != 1 {
492+
d.Logger.Warn("Invalid API port, using default 8000",
493+
zap.String("provided_port", apiPortStr))
494+
apiPort = 8000
495+
}
496+
497+
// Set CORS environment variable
498+
corsOrigins := os.Getenv("CORS_ALLOW_ORIGINS")
499+
if corsOrigins == "" {
500+
corsOrigins = fmt.Sprintf("http://%s:%s", apiHost, uiPort)
501+
os.Setenv("CORS_ALLOW_ORIGINS", corsOrigins)
502+
}
503+
504+
d.Logger.Info("Starting default deployment",
505+
zap.String("api_host", apiHost),
506+
zap.Int("api_port", apiPort),
507+
zap.String("ui_port", uiPort),
508+
zap.String("agents_file", agentsFile),
509+
zap.String("workflow_file", workflowFile))
510+
511+
// Start the API server using ServeWorkflow in a goroutine
512+
apiErrChan := make(chan error, 1)
513+
go func() {
514+
if err := ServeWorkflow(agentsFile, workflowFile, apiHost, apiPort); err != nil {
515+
d.Logger.Error("Failed to start API server",
516+
zap.Error(err))
517+
apiErrChan <- err
518+
}
519+
}()
520+
521+
// Wait for API server to be healthy
522+
if err := waitForAPIHealth(apiHost, apiPort, 60, 1, apiErrChan, d.Logger); err != nil {
523+
d.Logger.Error("Failed to start API server - health check failed",
524+
zap.Error(err))
525+
return fmt.Errorf("API server failed to become healthy: %w", err)
526+
}
527+
528+
// Start the UI server
529+
if err := startUIServer(uiPort, d.Logger); err != nil {
530+
d.Logger.Error("Failed to start UI server",
531+
zap.Error(err))
532+
return fmt.Errorf("failed to start UI server: %w", err)
533+
}
534+
535+
d.Logger.Info("Default deployment started successfully",
536+
zap.String("api_url", fmt.Sprintf("http://%s:%d", apiHost, apiPort)),
537+
zap.String("ui_url", fmt.Sprintf("http://localhost:%s", uiPort)))
538+
539+
return nil
540+
}
541+
542+
// waitForAPIHealth waits for the API server to be healthy by polling the /health endpoint.
543+
// Parameters:
544+
// - host: API server host
545+
// - port: API server port
546+
// - timeout: Maximum time to wait in seconds
547+
// - checkInterval: Time between health checks in seconds
548+
// - apiErrChan: Channel to receive errors from the API server goroutine
549+
// - logger: Logger instance
550+
//
551+
// Returns:
552+
// - error: nil if API is healthy, error if startup failed or timeout reached
553+
func waitForAPIHealth(host string, port int, timeout int, checkInterval int, apiErrChan <-chan error, logger *zap.Logger) error {
554+
url := fmt.Sprintf("http://%s:%d/health", host, port)
555+
startTime := time.Now()
556+
timeoutDuration := time.Duration(timeout) * time.Second
557+
ticker := time.NewTicker(time.Duration(checkInterval) * time.Second)
558+
defer ticker.Stop()
559+
560+
logger.Info("Waiting for API server to be ready", zap.String("url", url))
561+
562+
for {
563+
select {
564+
case err := <-apiErrChan:
565+
// API server failed to start
566+
logger.Error("API server startup failed", zap.Error(err))
567+
return fmt.Errorf("API server startup failed: %w", err)
568+
569+
case <-ticker.C:
570+
// Check if timeout has been reached
571+
if time.Since(startTime) >= timeoutDuration {
572+
logger.Error("API server failed to become ready", zap.Int("timeout_seconds", timeout))
573+
return fmt.Errorf("API server health check timeout after %d seconds", timeout)
574+
}
575+
576+
// Try to check health endpoint
577+
resp, err := http.Get(url)
578+
if err == nil {
579+
defer resp.Body.Close()
580+
if resp.StatusCode == 200 {
581+
var healthData map[string]interface{}
582+
if err := json.NewDecoder(resp.Body).Decode(&healthData); err == nil {
583+
if status, ok := healthData["status"].(string); ok && status == "healthy" {
584+
logger.Info("API server is ready!")
585+
return nil
586+
}
587+
}
588+
}
589+
}
590+
}
591+
}
592+
}
593+
594+
// startUIServer starts the UI server using npm.
595+
// Parameters:
596+
// - uiPort: Port for the UI server
597+
// - logger: Logger instance
598+
//
599+
// Returns:
600+
// - error if any
601+
func startUIServer(uiPort string, logger *zap.Logger) error {
602+
// Get the project root directory
603+
// Assuming the current working directory structure
604+
cwd, err := os.Getwd()
605+
if err != nil {
606+
return fmt.Errorf("failed to get current working directory: %w", err)
607+
}
608+
609+
// Try to find the UI directory
610+
// Look for src/maestro/ui or ui directory
611+
uiPaths := []string{
612+
filepath.Join(cwd, "src", "maestro", "ui"),
613+
filepath.Join(cwd, "ui"),
614+
filepath.Join(cwd, "..", "maestro", "src", "maestro", "ui"),
615+
}
616+
617+
var uiCwd string
618+
for _, path := range uiPaths {
619+
if _, err := os.Stat(path); err == nil {
620+
uiCwd = path
621+
break
622+
}
623+
}
624+
625+
if uiCwd == "" {
626+
logger.Warn("UI directory not found, skipping UI server startup")
627+
return nil
628+
}
629+
630+
// Set up environment for UI server
631+
uiEnv := os.Environ()
632+
uiEnv = append(uiEnv, fmt.Sprintf("PORT=%s", uiPort))
633+
634+
// Start the UI server
635+
npmCmd := exec.Command("npm", "run", "dev")
636+
npmCmd.Dir = uiCwd
637+
npmCmd.Env = uiEnv
638+
// Suppress stderr to avoid cluttering logs
639+
npmCmd.Stderr = nil
640+
641+
if err := npmCmd.Start(); err != nil {
642+
return fmt.Errorf("failed to start UI server: %w", err)
643+
}
644+
645+
logger.Info("UI server started",
646+
zap.String("ui_cwd", uiCwd),
647+
zap.String("port", uiPort))
648+
649+
return nil
650+
}
651+
443652
// Made with Bob

src/pkg/mcp/handlers.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -830,8 +830,19 @@ func handleDeployWorkflow(ctx context.Context, request mcp.CallToolRequest) (*mc
830830
}
831831
}()
832832
GlobalServerState.Logger.Info("Started asynchronous Kubernetes deployment")
833+
} else {
834+
// Default deployment: serve workflow with API and UI
835+
go func() {
836+
err := deploy.DeployDefault()
837+
if err != nil {
838+
GlobalServerState.Logger.Error("Failed to deploy workflow",
839+
zap.Error(err))
840+
} else {
841+
GlobalServerState.Logger.Info("Default deployment completed successfully")
842+
}
843+
}()
844+
GlobalServerState.Logger.Info("Started asynchronous default deployment")
833845
}
834-
// TODO: Implement workflow deployment logic
835846
// This would involve deploying the workflow to the specified target
836847

837848
GlobalServerState.Logger.Info("Deploying workflow",

0 commit comments

Comments
 (0)