github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/buildcontrol/image_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  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    10  
    11  	"github.com/tilt-dev/tilt/internal/analytics"
    12  	"github.com/tilt-dev/tilt/internal/build"
    13  	"github.com/tilt-dev/tilt/internal/controllers/core/cmdimage"
    14  	"github.com/tilt-dev/tilt/internal/controllers/core/dockerimage"
    15  	"github.com/tilt-dev/tilt/internal/controllers/core/kubernetesapply"
    16  	"github.com/tilt-dev/tilt/internal/store"
    17  	"github.com/tilt-dev/tilt/internal/store/k8sconv"
    18  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    19  	"github.com/tilt-dev/tilt/pkg/model"
    20  )
    21  
    22  var _ BuildAndDeployer = &ImageBuildAndDeployer{}
    23  
    24  type ImageBuildAndDeployer struct {
    25  	dr         *dockerimage.Reconciler
    26  	cr         *cmdimage.Reconciler
    27  	ib         *build.ImageBuilder
    28  	analytics  *analytics.TiltAnalytics
    29  	clock      build.Clock
    30  	ctrlClient ctrlclient.Client
    31  	r          *kubernetesapply.Reconciler
    32  }
    33  
    34  func NewImageBuildAndDeployer(
    35  	dr *dockerimage.Reconciler,
    36  	cr *cmdimage.Reconciler,
    37  	ib *build.ImageBuilder,
    38  	analytics *analytics.TiltAnalytics,
    39  	c build.Clock,
    40  	ctrlClient ctrlclient.Client,
    41  	r *kubernetesapply.Reconciler,
    42  ) *ImageBuildAndDeployer {
    43  	return &ImageBuildAndDeployer{
    44  		dr:         dr,
    45  		cr:         cr,
    46  		ib:         ib,
    47  		analytics:  analytics,
    48  		clock:      c,
    49  		ctrlClient: ctrlClient,
    50  		r:          r,
    51  	}
    52  }
    53  
    54  func (ibd *ImageBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, stateSet store.BuildStateSet) (resultSet store.BuildResultSet, err error) {
    55  	iTargets, kTargets := extractImageAndK8sTargets(specs)
    56  	if len(kTargets) != 1 {
    57  		return store.BuildResultSet{}, SilentRedirectToNextBuilderf("ImageBuildAndDeployer does not support these specs")
    58  	}
    59  
    60  	kTarget := kTargets[0]
    61  	kCluster := stateSet[kTarget.ID()].ClusterOrEmpty()
    62  
    63  	startTime := time.Now()
    64  	defer func() {
    65  		ibd.analytics.Timer("build.image", time.Since(startTime), map[string]string{
    66  			"hasError": fmt.Sprintf("%t", err != nil),
    67  		})
    68  	}()
    69  
    70  	q, err := NewImageTargetQueue(ctx, iTargets, stateSet, ibd.ib.CanReuseRef)
    71  	if err != nil {
    72  		return store.BuildResultSet{}, err
    73  	}
    74  
    75  	// each image target has two stages: one for build, and one for push
    76  	numStages := q.CountBuilds()*2 + 1
    77  
    78  	reused := q.ReusedResults()
    79  	hasReusedStep := len(reused) > 0
    80  	if hasReusedStep {
    81  		numStages++
    82  	}
    83  
    84  	hasDeleteStep := stateSet.FullBuildTriggered()
    85  	if hasDeleteStep {
    86  		numStages++
    87  	}
    88  
    89  	ps := build.NewPipelineState(ctx, numStages, ibd.clock)
    90  	defer func() { ps.End(ctx, err) }()
    91  
    92  	if hasDeleteStep {
    93  		ps.StartPipelineStep(ctx, "Force update")
    94  		err = ibd.delete(ps.AttachLogger(ctx), kTarget, kCluster)
    95  		if err != nil {
    96  			return store.BuildResultSet{}, WrapDontFallBackError(err)
    97  		}
    98  		ps.EndPipelineStep(ctx)
    99  	}
   100  
   101  	if hasReusedStep {
   102  		ps.StartPipelineStep(ctx, "Loading cached images")
   103  		for _, result := range reused {
   104  			ps.Printf(ctx, "- %s", store.LocalImageRefFromBuildResult(result))
   105  		}
   106  		ps.EndPipelineStep(ctx)
   107  	}
   108  
   109  	imageMapSet := make(map[types.NamespacedName]*v1alpha1.ImageMap, len(kTarget.ImageMaps))
   110  	for _, iTarget := range iTargets {
   111  		if iTarget.IsLiveUpdateOnly {
   112  			continue
   113  		}
   114  
   115  		var im v1alpha1.ImageMap
   116  		nn := types.NamespacedName{Name: iTarget.ImageMapName()}
   117  		err := ibd.ctrlClient.Get(ctx, nn, &im)
   118  		if err != nil {
   119  			return nil, err
   120  		}
   121  		imageMapSet[nn] = im.DeepCopy()
   122  	}
   123  
   124  	err = q.RunBuilds(func(target model.TargetSpec, depResults []store.ImageBuildResult) (store.ImageBuildResult, error) {
   125  		iTarget, ok := target.(model.ImageTarget)
   126  		if !ok {
   127  			return store.ImageBuildResult{}, fmt.Errorf("Not an image target: %T", target)
   128  		}
   129  
   130  		var cmd *v1alpha1.Cmd = nil
   131  		if iTarget.CmdImageName != "" {
   132  			nn := types.NamespacedName{Name: iTarget.CmdImageName}
   133  			cmd = &v1alpha1.Cmd{}
   134  			err := ibd.ctrlClient.Get(ctx, nn, cmd)
   135  			if err != nil {
   136  				return store.ImageBuildResult{}, err
   137  			}
   138  		}
   139  
   140  		cluster := stateSet[target.ID()].ClusterOrEmpty()
   141  		return ibd.build(ctx, iTarget, cmd, cluster, imageMapSet, ps)
   142  	})
   143  
   144  	newResults := q.NewResults().ToBuildResultSet()
   145  	if err != nil {
   146  		return newResults, WrapDontFallBackError(err)
   147  	}
   148  
   149  	// (If we pass an empty list of refs here (as we will do if only deploying
   150  	// yaml), we just don't inject any image refs into the yaml, nbd.
   151  	k8sResult, err := ibd.deploy(ctx, st, ps, kTarget.ID(), kTarget.KubernetesApplySpec, kCluster, imageMapSet)
   152  	if err != nil {
   153  		return newResults, WrapDontFallBackError(err)
   154  	}
   155  	newResults[kTarget.ID()] = k8sResult
   156  	return newResults, nil
   157  }
   158  
   159  func (ibd *ImageBuildAndDeployer) build(
   160  	ctx context.Context,
   161  	iTarget model.ImageTarget,
   162  	customBuildCmd *v1alpha1.Cmd,
   163  	cluster *v1alpha1.Cluster,
   164  	imageMaps map[types.NamespacedName]*v1alpha1.ImageMap,
   165  	ps *build.PipelineState) (store.ImageBuildResult, error) {
   166  	switch iTarget.BuildDetails.(type) {
   167  	case model.DockerBuild:
   168  		return ibd.dr.ForceApply(ctx, iTarget, cluster, imageMaps, ps)
   169  	case model.CustomBuild:
   170  		return ibd.cr.ForceApply(ctx, iTarget, customBuildCmd, cluster, imageMaps, ps)
   171  	}
   172  	return store.ImageBuildResult{}, fmt.Errorf("invalid image spec")
   173  }
   174  
   175  // Returns: the entities deployed and the namespace of the pod with the given image name/tag.
   176  func (ibd *ImageBuildAndDeployer) deploy(
   177  	ctx context.Context,
   178  	st store.RStore,
   179  	ps *build.PipelineState,
   180  	kTargetID model.TargetID,
   181  	spec v1alpha1.KubernetesApplySpec,
   182  	cluster *v1alpha1.Cluster,
   183  	imageMaps map[types.NamespacedName]*v1alpha1.ImageMap) (store.K8sBuildResult, error) {
   184  	ps.StartPipelineStep(ctx, "Deploying")
   185  	defer ps.EndPipelineStep(ctx)
   186  
   187  	kTargetNN := types.NamespacedName{Name: kTargetID.Name.String()}
   188  	status := ibd.r.ForceApply(ctx, kTargetNN, spec, cluster, imageMaps)
   189  	if status.Error != "" {
   190  		return store.K8sBuildResult{}, fmt.Errorf("%s", status.Error)
   191  	}
   192  
   193  	filter, err := k8sconv.NewKubernetesApplyFilter(status.ResultYAML)
   194  	if err != nil {
   195  		return store.K8sBuildResult{}, err
   196  	}
   197  	return store.NewK8sDeployResult(kTargetID, filter), nil
   198  }
   199  
   200  // Delete all the resources in the Kubernetes target, to ensure that they restart when
   201  // we re-apply them.
   202  func (ibd *ImageBuildAndDeployer) delete(ctx context.Context, k8sTarget model.K8sTarget, cluster *v1alpha1.Cluster) error {
   203  	kTargetNN := types.NamespacedName{Name: k8sTarget.ID().Name.String()}
   204  	return ibd.r.ForceDelete(ctx, kTargetNN, k8sTarget.KubernetesApplySpec, cluster, "force update")
   205  }