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  }