-
Notifications
You must be signed in to change notification settings - Fork 1
Fix: improve Docker startup and compose validation #78
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
2ad91e8
6a3e0e6
0ee7627
0c3283e
dd4200b
cf82f53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||
| "log" | ||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||
| "os/exec" | ||||||||||||||||||||||||||||||
| "os/signal" | ||||||||||||||||||||||||||||||
| "syscall" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -42,6 +43,15 @@ func main() { | |||||||||||||||||||||||||||||
| log.Fatalf("Failed to load config from %s: %v", resolvedConfigPath, err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| os.Setenv("DOCKER_HOST", cfg.DockerSocket) | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting
Suggested change
|
||||||||||||||||||||||||||||||
| log.Printf("Using Docker socket from config: %s", cfg.DockerSocket) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| ensureDockerReachable(cfg.DockerSocket) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if err := os.MkdirAll(cfg.DeploymentsPath, 0755); err != nil { | ||||||||||||||||||||||||||||||
| log.Fatalf("Failed to create deployments directory '%s': %v", cfg.DeploymentsPath, err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| log.Printf("Starting Flatrun Agent v%s", version.Version) | ||||||||||||||||||||||||||||||
| log.Printf("Config loaded from: %s", resolvedConfigPath) | ||||||||||||||||||||||||||||||
| log.Printf("Deployments path: %s", cfg.DeploymentsPath) | ||||||||||||||||||||||||||||||
|
|
@@ -70,6 +80,15 @@ func main() { | |||||||||||||||||||||||||||||
| _ = apiServer.Stop() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func ensureDockerReachable(_ string) { | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Let's use the Docker Go SDK instead:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous review suggested using the Moby client library's
Suggested change
|
||||||||||||||||||||||||||||||
| log.Println("Checking if Docker is reachable...") | ||||||||||||||||||||||||||||||
| cmd := exec.Command("docker", "info") | ||||||||||||||||||||||||||||||
| if _, err := cmd.CombinedOutput(); err != nil { | ||||||||||||||||||||||||||||||
| log.Fatalf("Docker is not reachable: Ensure the Docker daemon is running and docker socket in config is correct (e.g. unix:///var/run/docker.sock).") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| log.Println("Docker is reachable") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func printVersion() { | ||||||||||||||||||||||||||||||
| info := version.Get() | ||||||||||||||||||||||||||||||
| fmt.Printf("Flatrun Agent\n") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| deployments_path: /home/nfebe/work/deployments | ||
| deployments_path: ./deployment | ||
|
||
| docker_socket: unix:///var/run/docker.sock | ||
| api: | ||
| host: 0.0.0.0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os/exec" | ||
| "strings" | ||
| "sync" | ||
| "testing" | ||
|
|
||
| "github.com/flatrun/agent/pkg/config" | ||
| ) | ||
|
|
||
| var ( | ||
| dockerCheckOnce sync.Once | ||
| dockerCheckErr error | ||
| ) | ||
|
|
||
| // dockerAvailable ensures docker is available and the proxy network exists for CLI validation tests. | ||
| // Runs the check once; subsequent calls use the cached result. Fails when Docker is unavailable. | ||
| func dockerAvailable(t *testing.T) { | ||
| t.Helper() | ||
| dockerCheckOnce.Do(func() { | ||
| dockerCheckErr = checkDocker() | ||
| }) | ||
| if dockerCheckErr != nil { | ||
| t.Fatal(dockerCheckErr) | ||
| } | ||
| } | ||
|
|
||
| func checkDocker() error { | ||
| if testing.Short() { | ||
| return fmt.Errorf("compose CLI validation tests require Docker; do not use -short") | ||
| } | ||
| if _, err := exec.LookPath("docker"); err != nil { | ||
| return fmt.Errorf("docker not in PATH: %w", err) | ||
| } | ||
| if err := exec.Command("docker", "info").Run(); err != nil { | ||
| return fmt.Errorf("docker daemon not reachable: %w", err) | ||
| } | ||
| _ = exec.Command("docker", "network", "create", "proxy").Run() | ||
| if err := exec.Command("docker", "network", "inspect", "proxy").Run(); err != nil { | ||
| return fmt.Errorf("proxy network not found (run: docker network create proxy): %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func TestValidateComposeWithCLI_ValidCompose_Integration(t *testing.T) { | ||
| dockerAvailable(t) | ||
| validCompose := `name: test | ||
| services: | ||
| app: | ||
| image: nginx:alpine | ||
| ` | ||
| err := validateComposeWithCLI(validCompose) | ||
| if err != nil { | ||
| t.Errorf("validateComposeWithCLI(valid compose) = %v, want nil", err) | ||
| } | ||
| } | ||
|
|
||
| func TestValidateComposeWithCLI_InvalidCompose_Integration(t *testing.T) { | ||
| dockerAvailable(t) | ||
| // Invalid: service references network not defined in top-level networks | ||
| invalidCompose := `name: test | ||
| services: | ||
| app: | ||
| image: nginx | ||
| networks: | ||
| - undefined_network_xyz | ||
| ` | ||
| err := validateComposeWithCLI(invalidCompose) | ||
| if err == nil { | ||
| t.Error("validateComposeWithCLI(invalid compose) = nil, want error") | ||
| } | ||
| if err != nil && !strings.Contains(err.Error(), "invalid compose") { | ||
| t.Errorf("validateComposeWithCLI error should mention 'invalid compose', got: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestValidateComposeWithCLI_InvalidYAML_Integration(t *testing.T) { | ||
| dockerAvailable(t) | ||
| // Invalid YAML - docker compose config will reject it | ||
| invalidCompose := `name: test | ||
| services: | ||
| app: | ||
| image: [broken yaml | ||
| ` | ||
| err := validateComposeWithCLI(invalidCompose) | ||
| if err == nil { | ||
| t.Error("validateComposeWithCLI(invalid YAML) = nil, want error") | ||
| } | ||
| } | ||
|
|
||
| func TestValidateComposeContent_ValidCompose_Integration(t *testing.T) { | ||
| dockerAvailable(t) | ||
| cfg := &config.Config{ | ||
| Infrastructure: config.InfrastructureConfig{ | ||
| DefaultProxyNetwork: "proxy", | ||
| }, | ||
| } | ||
| s := &Server{config: cfg} | ||
|
|
||
| validCompose := `name: test | ||
| services: | ||
| app: | ||
| image: nginx:alpine | ||
| networks: | ||
| - proxy | ||
| networks: | ||
| proxy: | ||
| external: true | ||
| ` | ||
| err := s.validateComposeContent(validCompose, "test") | ||
| if err != nil { | ||
| t.Errorf("validateComposeContent(valid) = %v, want nil", err) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1255,6 +1255,13 @@ func (s *Server) updateDeployment(c *gin.Context) { | |||||||||||||||||
| return | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if err := s.validateComposeContent(req.ComposeContent, name); err != nil { | ||||||||||||||||||
| c.JSON(http.StatusBadRequest, gin.H{ | ||||||||||||||||||
| "error": err.Error(), | ||||||||||||||||||
| }) | ||||||||||||||||||
| return | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if err := s.manager.UpdateDeployment(name, req.ComposeContent); err != nil { | ||||||||||||||||||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||||||||||||||||||
| "error": err.Error(), | ||||||||||||||||||
|
|
@@ -2777,7 +2784,7 @@ type composeNetwork struct { | |||||||||||||||||
| Name string `yaml:"name"` | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (s *Server) validateComposeContent(content, deploymentName string) error { | ||||||||||||||||||
| func (s *Server) validateComposeContent(content, _ string) error { | ||||||||||||||||||
| var compose composeFile | ||||||||||||||||||
| if err := yaml.Unmarshal([]byte(content), &compose); err != nil { | ||||||||||||||||||
| return fmt.Errorf("invalid YAML syntax: %w", err) | ||||||||||||||||||
|
|
@@ -2815,6 +2822,41 @@ func (s *Server) validateComposeContent(content, deploymentName string) error { | |||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if err := validateComposeWithCLI(content); err != nil { | ||||||||||||||||||
|
||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func validateComposeWithCLI(content string) error { | ||||||||||||||||||
|
||||||||||||||||||
| func validateComposeWithCLI(content string) error { | |
| tmp, err := os.CreateTemp("", "compose-*.yml") | |
| if err != nil { | |
| log.Printf("Warning: skipping CLI validation, failed to create temp file: %v", err) | |
| return nil | |
| } | |
| defer os.Remove(tmp.Name()) | |
| defer tmp.Close() |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation writes to the file, syncs, closes it, and then re-opens it via the docker compose CLI. While functional, it's cleaner to handle errors on WriteString and Sync more strictly if you want to ensure validation occurs. More importantly, since the file is already closed on line 2853, the deferred tmp.Close() on line 2841 will return an error (which is ignored), but it's redundant.
| if _, err := tmp.WriteString(content); err != nil { | |
| if _, err := tmp.WriteString(content); err != nil { | |
| return fmt.Errorf("failed to write temp file for validation: %w", err) | |
| } | |
| if err := tmp.Close(); err != nil { | |
| return fmt.Errorf("failed to close temp file: %w", err) | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
exec.Command("docker", "info")relies on thedockerCLI being installed in the PATH. Since you've added themoby/mobyclient dependency, it is more efficient and reliable to check connectivity using the library'sPing()orInfo()methods instead of spawning a shell process.