github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/dockercomposelogstream/project.go (about) 1 package dockercomposelogstream 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/docker/docker/client" 8 9 "github.com/tilt-dev/tilt/internal/controllers/apicmp" 10 "github.com/tilt-dev/tilt/internal/dockercompose" 11 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 12 "github.com/tilt-dev/tilt/pkg/logger" 13 ) 14 15 // Keeps track of the projects we're currently watching. 16 type ProjectWatch struct { 17 ctx context.Context 18 cancel func() 19 project v1alpha1.DockerComposeProject 20 hash string 21 } 22 23 // Sync all the project watches with the dockercompose objects 24 // we're currently tracking. 25 func (r *Reconciler) manageOwnedProjectWatches() { 26 running := map[string]bool{} 27 for key := range r.projectWatches { 28 running[key] = true 29 } 30 31 owned := map[string]bool{} 32 for _, result := range r.results { 33 hash := result.projectHash 34 owned[hash] = true 35 36 if hash != "" && !running[hash] { 37 ctx, cancel := context.WithCancel(result.loggerCtx) 38 pw := &ProjectWatch{ 39 ctx: ctx, 40 cancel: cancel, 41 project: result.spec.Project, 42 hash: hash, 43 } 44 r.projectWatches[hash] = pw 45 go r.runProjectWatch(pw) 46 running[hash] = true 47 } 48 } 49 50 for key := range r.projectWatches { 51 if !owned[key] { 52 r.projectWatches[key].cancel() 53 delete(r.projectWatches, key) 54 } 55 } 56 } 57 58 // Stream events from the docker-compose project and 59 // fan them out to each service in the project. 60 func (r *Reconciler) runProjectWatch(pw *ProjectWatch) { 61 defer func() { 62 r.mu.Lock() 63 delete(r.projectWatches, pw.hash) 64 r.mu.Unlock() 65 pw.cancel() 66 }() 67 68 ctx := pw.ctx 69 project := pw.project 70 ch, err := r.dcc.StreamEvents(ctx, project) 71 if err != nil { 72 // TODO(nick): Figure out where this error should be published. 73 return 74 } 75 76 for { 77 select { 78 case evtJson, ok := <-ch: 79 if !ok { 80 return 81 } 82 evt, err := dockercompose.EventFromJsonStr(evtJson) 83 if err != nil { 84 logger.Get(ctx).Debugf("[dcwatch] failed to unmarshal dc event '%s' with err: %v", evtJson, err) 85 continue 86 } 87 88 if evt.Type != dockercompose.TypeContainer { 89 continue 90 } 91 92 key := serviceKey{service: evt.Service, projectHash: pw.hash} 93 c, err := r.getContainerInfo(ctx, evt.ID) 94 if err != nil { 95 if !client.IsErrNotFound(err) { 96 logger.Get(ctx).Debugf("[dcwatch]: %v", err) 97 } 98 continue 99 } 100 101 r.mu.Lock() 102 if r.recordContainerInfo(key, c) { 103 r.requeueForServiceKey(key) 104 } 105 r.mu.Unlock() 106 107 case <-ctx.Done(): 108 return 109 } 110 } 111 } 112 113 // Fetch the state of the given container and convert it into our internal model. 114 func (r *Reconciler) getContainerInfo(ctx context.Context, id string) (*ContainerInfo, error) { 115 containerJSON, err := r.dc.ContainerInspect(ctx, id) 116 if err != nil { 117 return nil, err 118 } 119 120 if containerJSON.Config == nil || 121 containerJSON.ContainerJSONBase == nil || 122 containerJSON.ContainerJSONBase.State == nil { 123 return nil, fmt.Errorf("no state found") 124 } 125 126 cState := containerJSON.ContainerJSONBase.State 127 return &ContainerInfo{ 128 ID: id, 129 State: dockercompose.ToContainerState(cState), 130 TTY: containerJSON.Config.Tty, 131 }, nil 132 } 133 134 // Record the container event and re-reconcile. Caller must hold the lock. 135 // Returns true on change. 136 func (r *Reconciler) recordContainerInfo(key serviceKey, c *ContainerInfo) bool { 137 existing := r.containers[key] 138 if apicmp.DeepEqual(c, existing) { 139 return false 140 } 141 142 r.containers[key] = c 143 return true 144 } 145 146 // Find any results that depend on the given service, and ask the 147 // reconciler to re-concile. 148 func (r *Reconciler) requeueForServiceKey(key serviceKey) { 149 for _, result := range r.results { 150 if result.serviceKey() == key { 151 r.requeuer.Add(result.name) 152 } 153 } 154 }