github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/docker_compose_build_and_deployer.go (about)

     1  package buildcontrol
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"k8s.io/apimachinery/pkg/types"
     9  	ktypes "k8s.io/apimachinery/pkg/types"
    10  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    11  
    12  	"github.com/tilt-dev/tilt/internal/analytics"
    13  	"github.com/tilt-dev/tilt/internal/controllers/core/cmdimage"
    14  	"github.com/tilt-dev/tilt/internal/controllers/core/dockercomposeservice"
    15  	"github.com/tilt-dev/tilt/internal/controllers/core/dockerimage"
    16  
    17  	"github.com/tilt-dev/tilt/internal/build"
    18  	"github.com/tilt-dev/tilt/internal/docker"
    19  	"github.com/tilt-dev/tilt/internal/store"
    20  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    21  	"github.com/tilt-dev/tilt/pkg/model"
    22  )
    23  
    24  type DockerComposeBuildAndDeployer struct {
    25  	dr         *dockerimage.Reconciler
    26  	cr         *cmdimage.Reconciler
    27  	ib         *build.ImageBuilder
    28  	dcsr       *dockercomposeservice.Reconciler
    29  	clock      build.Clock
    30  	ctrlClient ctrlclient.Client
    31  }
    32  
    33  var _ BuildAndDeployer = &DockerComposeBuildAndDeployer{}
    34  
    35  func NewDockerComposeBuildAndDeployer(
    36  	dr *dockerimage.Reconciler,
    37  	cr *cmdimage.Reconciler,
    38  	ib *build.ImageBuilder,
    39  	dcsr *dockercomposeservice.Reconciler,
    40  	c build.Clock,
    41  	ctrlClient ctrlclient.Client,
    42  ) *DockerComposeBuildAndDeployer {
    43  	return &DockerComposeBuildAndDeployer{
    44  		dr:         dr,
    45  		cr:         cr,
    46  		ib:         ib,
    47  		dcsr:       dcsr,
    48  		clock:      c,
    49  		ctrlClient: ctrlClient,
    50  	}
    51  }
    52  
    53  // Extract the targets we can apply -- DCBaD supports ImageTargets and DockerComposeTargets.
    54  //
    55  // A given Docker Compose service can be built one of two ways:
    56  //   - Tilt-managed: Tiltfile includes a `docker_build` or `custom_build` directive for the service's image, so Tilt
    57  //     will handle the image lifecycle including building/tagging and Live Update (if configured)
    58  //   - Docker Compose-managed: Building is delegated to Docker Compose via the `--build` flag to the `up` call;
    59  //     Tilt is responsible for watching file changes but does not handle the builds.
    60  //
    61  // It's also possible for a service to reference an image but NOT have any corresponding build (e.g. public/registry
    62  // hosted images are common for infra deps like nginx). These will not have any ImageTarget.
    63  func (bd *DockerComposeBuildAndDeployer) extract(specs []model.TargetSpec) (buildPlan, error) {
    64  	var tiltManagedImageTargets []model.ImageTarget
    65  	var dockerComposeImageTarget *model.ImageTarget
    66  	var dcTargets []model.DockerComposeTarget
    67  
    68  	for _, s := range specs {
    69  		switch s := s.(type) {
    70  		case model.ImageTarget:
    71  			if s.IsDockerComposeBuild() {
    72  				if dockerComposeImageTarget != nil {
    73  					return buildPlan{}, DontFallBackErrorf(
    74  						"Target has more than one Docker Compose managed image target")
    75  				}
    76  				dcTarget := s
    77  				dockerComposeImageTarget = &dcTarget
    78  			} else {
    79  				tiltManagedImageTargets = append(tiltManagedImageTargets, s)
    80  			}
    81  		case model.DockerComposeTarget:
    82  			dcTargets = append(dcTargets, s)
    83  		default:
    84  			// unrecognized target
    85  			return buildPlan{}, SilentRedirectToNextBuilderf("DockerComposeBuildAndDeployer does not support target type %T", s)
    86  		}
    87  	}
    88  
    89  	if len(dcTargets) != 1 {
    90  		return buildPlan{}, SilentRedirectToNextBuilderf(
    91  			"DockerComposeBuildAndDeployer requires exactly one dcTarget (got %d)", len(dcTargets))
    92  	}
    93  
    94  	if len(tiltManagedImageTargets) != 0 && dockerComposeImageTarget != nil {
    95  		return buildPlan{}, DontFallBackErrorf(
    96  			"Docker Compose target cannot have both Tilt-managed and Docker Compose-managed image targets")
    97  	}
    98  
    99  	return buildPlan{
   100  		dockerComposeTarget:      dcTargets[0],
   101  		tiltManagedImageTargets:  tiltManagedImageTargets,
   102  		dockerComposeImageTarget: dockerComposeImageTarget,
   103  	}, nil
   104  }
   105  
   106  func (bd *DockerComposeBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, currentState store.BuildStateSet) (res store.BuildResultSet, err error) {
   107  	plan, err := bd.extract(specs)
   108  	if err != nil {
   109  		return store.BuildResultSet{}, err
   110  	}
   111  
   112  	startTime := time.Now()
   113  	defer func() {
   114  		analytics.Get(ctx).Timer("build.docker-compose", time.Since(startTime), map[string]string{
   115  			"hasError": fmt.Sprintf("%t", err != nil),
   116  		})
   117  	}()
   118  
   119  	dcTarget := plan.dockerComposeTarget
   120  	dcTargetNN := types.NamespacedName{Name: dcTarget.ID().Name.String()}
   121  	ctx = docker.WithOrchestrator(ctx, model.OrchestratorDC)
   122  
   123  	iTargets := plan.tiltManagedImageTargets
   124  	q, err := NewImageTargetQueue(ctx, plan.tiltManagedImageTargets, currentState, bd.ib.CanReuseRef)
   125  	if err != nil {
   126  		return store.BuildResultSet{}, err
   127  	}
   128  
   129  	// base number of stages is the Tilt-managed image builds + the Docker Compose up step (which might be launching
   130  	// a Tilt-built image OR might build+launch a Docker Compose-managed image)
   131  	numStages := q.CountBuilds() + 1
   132  
   133  	hasDeleteStep := currentState.FullBuildTriggered()
   134  	if hasDeleteStep {
   135  		numStages++
   136  	}
   137  
   138  	reused := q.ReusedResults()
   139  	hasReusedStep := len(reused) > 0
   140  	if hasReusedStep {
   141  		numStages++
   142  	}
   143  
   144  	ps := build.NewPipelineState(ctx, numStages, bd.clock)
   145  	defer func() { ps.End(ctx, err) }()
   146  
   147  	if hasDeleteStep {
   148  		ps.StartPipelineStep(ctx, "Force update")
   149  		err = bd.dcsr.ForceDelete(ps.AttachLogger(ctx), dcTargetNN, dcTarget.Spec, "force update")
   150  		if err != nil {
   151  			return store.BuildResultSet{}, WrapDontFallBackError(err)
   152  		}
   153  		ps.EndPipelineStep(ctx)
   154  	}
   155  
   156  	if hasReusedStep {
   157  		ps.StartPipelineStep(ctx, "Loading cached images")
   158  		for _, result := range reused {
   159  			ps.Printf(ctx, "- %s", store.LocalImageRefFromBuildResult(result))
   160  		}
   161  		ps.EndPipelineStep(ctx)
   162  	}
   163  
   164  	imageMapSet := make(map[ktypes.NamespacedName]*v1alpha1.ImageMap, len(plan.dockerComposeTarget.Spec.ImageMaps))
   165  	for _, iTarget := range iTargets {
   166  		if iTarget.IsLiveUpdateOnly {
   167  			continue
   168  		}
   169  
   170  		var im v1alpha1.ImageMap
   171  		nn := ktypes.NamespacedName{Name: iTarget.ImageMapName()}
   172  		err := bd.ctrlClient.Get(ctx, nn, &im)
   173  		if err != nil {
   174  			return nil, err
   175  		}
   176  		imageMapSet[nn] = im.DeepCopy()
   177  	}
   178  
   179  	err = q.RunBuilds(func(target model.TargetSpec, depResults []store.ImageBuildResult) (store.ImageBuildResult, error) {
   180  		iTarget, ok := target.(model.ImageTarget)
   181  		if !ok {
   182  			return store.ImageBuildResult{}, fmt.Errorf("Not an image target: %T", target)
   183  		}
   184  
   185  		var cmd *v1alpha1.Cmd = nil
   186  		if iTarget.CmdImageName != "" {
   187  			nn := types.NamespacedName{Name: iTarget.CmdImageName}
   188  			cmd = &v1alpha1.Cmd{}
   189  			err := bd.ctrlClient.Get(ctx, nn, cmd)
   190  			if err != nil {
   191  				return store.ImageBuildResult{}, err
   192  			}
   193  		}
   194  
   195  		cluster := currentState[target.ID()].ClusterOrEmpty()
   196  		return bd.build(ctx, iTarget, cmd, cluster, imageMapSet, ps)
   197  	})
   198  
   199  	newResults := q.NewResults().ToBuildResultSet()
   200  	if err != nil {
   201  		return newResults, err
   202  	}
   203  
   204  	dcManagedBuild := plan.dockerComposeImageTarget != nil
   205  	var stepName string
   206  	if dcManagedBuild {
   207  		stepName = "Building & deploying"
   208  	} else {
   209  		stepName = "Deploying"
   210  	}
   211  	ps.StartPipelineStep(ctx, stepName)
   212  
   213  	status := bd.dcsr.ForceApply(ctx, dcTargetNN, dcTarget.Spec, imageMapSet, dcManagedBuild)
   214  	ps.EndPipelineStep(ctx)
   215  	if status.ApplyError != "" {
   216  		return newResults, fmt.Errorf("%s", status.ApplyError)
   217  	}
   218  
   219  	dcTargetID := plan.dockerComposeTarget.ID()
   220  	newResults[dcTargetID] = store.NewDockerComposeDeployResult(dcTargetID, status)
   221  	return newResults, nil
   222  }
   223  
   224  func (bd *DockerComposeBuildAndDeployer) build(
   225  	ctx context.Context,
   226  	iTarget model.ImageTarget,
   227  	customBuildCmd *v1alpha1.Cmd,
   228  	cluster *v1alpha1.Cluster,
   229  	imageMaps map[types.NamespacedName]*v1alpha1.ImageMap,
   230  	ps *build.PipelineState) (store.ImageBuildResult, error) {
   231  	switch iTarget.BuildDetails.(type) {
   232  	case model.DockerBuild:
   233  		return bd.dr.ForceApply(ctx, iTarget, cluster, imageMaps, ps)
   234  	case model.CustomBuild:
   235  		return bd.cr.ForceApply(ctx, iTarget, customBuildCmd, cluster, imageMaps, ps)
   236  	}
   237  	return store.ImageBuildResult{}, fmt.Errorf("invalid image spec")
   238  }
   239  
   240  type buildPlan struct {
   241  	dockerComposeTarget model.DockerComposeTarget
   242  
   243  	tiltManagedImageTargets []model.ImageTarget
   244  
   245  	dockerComposeImageTarget *model.ImageTarget
   246  }