@@ -7,25 +7,88 @@ import (
77 "os/exec"
88 "path/filepath"
99 "strings"
10+ "sync"
1011
1112 "github.com/docker/docker/api/types/container"
1213 "github.com/docker/docker/api/types/filters"
1314 "github.com/docker/docker/client"
1415 "github.com/gdamore/tcell/v2"
1516 "github.com/jr-k/d4s/internal/dao/common"
17+ "github.com/jr-k/d4s/internal/sshutil"
1618 "github.com/jr-k/d4s/internal/ui/styles"
1719 "golang.org/x/net/context"
1820)
1921
2022type Manager struct {
2123 cli * client.Client
2224 ctx context.Context
25+
26+ // Where docker compose CLI commands must run. For SSH contexts the
27+ // compose files only exist on the remote host, so commands are
28+ // executed there through ssh.
29+ targetMu sync.RWMutex
30+ contextName string
31+ remoteHost string // empty = run locally
2332}
2433
2534func NewManager (cli * client.Client , ctx context.Context ) * Manager {
2635 return & Manager {cli : cli , ctx : ctx }
2736}
2837
38+ // SetExecTarget configures where compose CLI commands run.
39+ // remoteHost is empty for local contexts, "user@ip" for SSH contexts.
40+ func (m * Manager ) SetExecTarget (contextName , remoteHost string ) {
41+ m .targetMu .Lock ()
42+ m .contextName = contextName
43+ m .remoteHost = remoteHost
44+ m .targetMu .Unlock ()
45+ }
46+
47+ func (m * Manager ) execTarget () (string , string ) {
48+ m .targetMu .RLock ()
49+ defer m .targetMu .RUnlock ()
50+ return m .contextName , m .remoteHost
51+ }
52+
53+ // dockerCmd builds a docker CLI invocation that runs on the host where
54+ // the compose files live: locally (pinned to the right docker context)
55+ // or on the remote SSH host.
56+ func (m * Manager ) dockerCmd (args []string , workDir string ) * exec.Cmd {
57+ contextName , remoteHost := m .execTarget ()
58+
59+ if remoteHost != "" {
60+ quoted := make ([]string , 0 , len (args )+ 1 )
61+ quoted = append (quoted , "docker" )
62+ for _ , a := range args {
63+ quoted = append (quoted , sshutil .ShellQuote (a ))
64+ }
65+ remoteCmd := strings .Join (quoted , " " )
66+ if workDir != "" {
67+ remoteCmd = fmt .Sprintf ("cd %s && %s" , sshutil .ShellQuote (workDir ), remoteCmd )
68+ }
69+ return sshutil .SSHCommand (contextName , remoteHost , remoteCmd )
70+ }
71+
72+ cmd := exec .Command ("docker" , args ... )
73+ if workDir != "" {
74+ cmd .Dir = workDir
75+ }
76+ if contextName != "" && contextName != "default" && contextName != "env" {
77+ cmd .Env = append (os .Environ (), "DOCKER_CONTEXT=" + contextName )
78+ }
79+ return cmd
80+ }
81+
82+ // readConfigFile reads a compose file from where it actually lives.
83+ func (m * Manager ) readConfigFile (path string ) ([]byte , error ) {
84+ contextName , remoteHost := m .execTarget ()
85+ if remoteHost == "" {
86+ return os .ReadFile (path )
87+ }
88+ cmd := sshutil .SSHCommand (contextName , remoteHost , "cat " + sshutil .ShellQuote (path ))
89+ return cmd .Output ()
90+ }
91+
2992// ComposeProject Model
3093type ComposeProject struct {
3194 Name string
@@ -209,7 +272,7 @@ func (m *Manager) GetConfig(projectName string) (string, error) {
209272 path := strings .TrimSpace (f )
210273 if path == "" { continue }
211274
212- content , err := os . ReadFile (path )
275+ content , err := m . readConfigFile (path )
213276 if err != nil {
214277 sb .WriteString (fmt .Sprintf ("# Error reading %s: %v\n " , path , err ))
215278 continue
@@ -223,28 +286,6 @@ func (m *Manager) GetConfig(projectName string) (string, error) {
223286 return sb .String (), nil
224287}
225288
226- func (m * Manager ) countServices (projectName string , configPaths []string ) int {
227- args := []string {"compose" , "-p" , projectName }
228- for _ , path := range configPaths {
229- args = append (args , "-f" , path )
230- }
231- args = append (args , "config" , "--services" )
232-
233- cmd := exec .Command ("docker" , args ... )
234- output , err := cmd .Output ()
235- if err != nil {
236- return 0
237- }
238-
239- count := 0
240- for _ , line := range strings .Split (strings .TrimSpace (string (output )), "\n " ) {
241- if line != "" {
242- count ++
243- }
244- }
245- return count
246- }
247-
248289func (m * Manager ) getConfigPaths (projectName string ) ([]string , error ) {
249290 args := filters .NewArgs ()
250291 args .Add ("label" , fmt .Sprintf ("com.docker.compose.project=%s" , projectName ))
@@ -285,7 +326,7 @@ func (m *Manager) Logs(projectName string, since string, tail string, timestamps
285326 args = append (args , "--since" , since )
286327 }
287328
288- cmd := exec . Command ( "docker" , args ... )
329+ cmd := m . dockerCmd ( args , "" )
289330
290331 stdout , err := cmd .StdoutPipe ()
291332 if err != nil {
@@ -303,10 +344,7 @@ func (m *Manager) Logs(projectName string, since string, tail string, timestamps
303344}
304345
305346func (m * Manager ) Down (projectName string ) error {
306- var args []string
307- args = append (args , "compose" , "-p" , projectName , "down" )
308-
309- cmd := exec .Command ("docker" , args ... )
347+ cmd := m .dockerCmd ([]string {"compose" , "-p" , projectName , "down" }, "" )
310348 output , err := cmd .CombinedOutput ()
311349 if err != nil {
312350 return fmt .Errorf ("error running docker compose down: %v, output: %s" , err , string (output ))
@@ -324,59 +362,41 @@ func (m *Manager) Redeploy(projectName string) error {
324362 return err
325363 }
326364
327- args := []string {"compose" , "-p" , projectName }
328- for _ , path := range paths {
329- args = append (args , "-f" , path )
330- }
331- args = append (args , "up" , "-d" , "--force-recreate" )
332-
333- cmd := exec .Command ("docker" , args ... )
334- if len (paths ) > 0 {
335- cmd .Dir = filepath .Dir (paths [0 ])
336- }
337- output , err := cmd .CombinedOutput ()
338- if err != nil {
339- return fmt .Errorf ("error running docker compose up: %v\n Output: %s" , err , string (output ))
340- }
341- return nil
365+ return m .up (projectName , paths , "--force-recreate" )
342366}
343367
344368func (m * Manager ) Up (projectName string ) error {
345369 paths , err := m .getConfigPaths (projectName )
346370 if err != nil {
347371 return fmt .Errorf ("failed to up project: %v" , err )
348372 }
349-
350- args := []string {"compose" , "-p" , projectName }
351- for _ , path := range paths {
352- args = append (args , "-f" , path )
353- }
354- args = append (args , "up" , "-d" , "--force-recreate" )
355-
356- cmd := exec .Command ("docker" , args ... )
357- output , err := cmd .CombinedOutput ()
358- if err != nil {
359- return fmt .Errorf ("error running docker compose up: %v\n Output: %s" , err , string (output ))
360- }
361- return nil
373+ return m .up (projectName , paths , "--force-recreate" )
362374}
363375
364376func (m * Manager ) Build (projectName string ) error {
365377 paths , err := m .getConfigPaths (projectName )
366378 if err != nil {
367379 return fmt .Errorf ("failed to build project: %v" , err )
368380 }
381+ return m .up (projectName , paths , "--build" )
382+ }
369383
384+ func (m * Manager ) up (projectName string , paths []string , extraFlag string ) error {
370385 args := []string {"compose" , "-p" , projectName }
371386 for _ , path := range paths {
372387 args = append (args , "-f" , path )
373388 }
374- args = append (args , "up" , "-d" , "--build" )
389+ args = append (args , "up" , "-d" , extraFlag )
375390
376- cmd := exec .Command ("docker" , args ... )
391+ workDir := ""
392+ if len (paths ) > 0 {
393+ workDir = filepath .Dir (paths [0 ])
394+ }
395+
396+ cmd := m .dockerCmd (args , workDir )
377397 output , err := cmd .CombinedOutput ()
378398 if err != nil {
379- return fmt .Errorf ("error running docker compose up --build : %v\n Output: %s" , err , string (output ))
399+ return fmt .Errorf ("error running docker compose up: %v\n Output: %s" , err , string (output ))
380400 }
381401 return nil
382402}
0 commit comments