github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/build/image_builder.go (about)

     1  package build
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"github.com/distribution/reference"
     9  	"k8s.io/apimachinery/pkg/types"
    10  
    11  	"github.com/tilt-dev/clusterid"
    12  	"github.com/tilt-dev/tilt/internal/container"
    13  	"github.com/tilt-dev/tilt/internal/ignore"
    14  	"github.com/tilt-dev/tilt/internal/k8s"
    15  	"github.com/tilt-dev/tilt/pkg/apis"
    16  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    17  	"github.com/tilt-dev/tilt/pkg/model"
    18  )
    19  
    20  type ImageBuilder struct {
    21  	db    *DockerBuilder
    22  	custb *CustomBuilder
    23  	kl    KINDLoader
    24  }
    25  
    26  func NewImageBuilder(db *DockerBuilder, custb *CustomBuilder, kl KINDLoader) *ImageBuilder {
    27  	return &ImageBuilder{
    28  		db:    db,
    29  		custb: custb,
    30  		kl:    kl,
    31  	}
    32  }
    33  
    34  func (ib *ImageBuilder) CanReuseRef(ctx context.Context, iTarget model.ImageTarget, ref reference.NamedTagged) (bool, error) {
    35  	switch iTarget.BuildDetails.(type) {
    36  	case model.DockerBuild:
    37  		return ib.db.ImageExists(ctx, ref)
    38  	case model.CustomBuild:
    39  		// Custom build doesn't have a good way to check if the ref still exists in the image
    40  		// store, so just assume we can.
    41  		return true, nil
    42  	}
    43  	return false, fmt.Errorf("image %q has no valid buildDetails (neither "+
    44  		"DockerBuild nor CustomBuild)", iTarget.ImageMapSpec.Selector)
    45  }
    46  
    47  // Build the image, and push it if necessary.
    48  //
    49  // Note that this function can return partial results on an error.
    50  //
    51  // The error is simply the "main" build failure reason.
    52  func (ib *ImageBuilder) Build(ctx context.Context,
    53  	iTarget model.ImageTarget,
    54  	customBuildCmd *v1alpha1.Cmd,
    55  	cluster *v1alpha1.Cluster,
    56  	imageMaps map[types.NamespacedName]*v1alpha1.ImageMap,
    57  	ps *PipelineState) (container.TaggedRefs, []v1alpha1.DockerImageStageStatus, error) {
    58  	refs, stages, err := ib.buildOnly(ctx, iTarget, customBuildCmd, cluster, imageMaps, ps)
    59  	if err != nil {
    60  		return refs, stages, err
    61  	}
    62  
    63  	pushStage := ib.push(ctx, refs, ps, iTarget, cluster)
    64  	if pushStage != nil {
    65  		stages = append(stages, *pushStage)
    66  	}
    67  
    68  	if pushStage != nil && pushStage.Error != "" {
    69  		err = errors.New(pushStage.Error)
    70  	}
    71  
    72  	return refs, stages, err
    73  }
    74  
    75  // Build the image, but don't do any push.
    76  func (ib *ImageBuilder) buildOnly(ctx context.Context,
    77  	iTarget model.ImageTarget,
    78  	customBuildCmd *v1alpha1.Cmd,
    79  	cluster *v1alpha1.Cluster,
    80  	imageMaps map[types.NamespacedName]*v1alpha1.ImageMap,
    81  	ps *PipelineState,
    82  ) (container.TaggedRefs, []v1alpha1.DockerImageStageStatus, error) {
    83  	refs, err := iTarget.Refs(cluster)
    84  	if err != nil {
    85  		return container.TaggedRefs{}, nil, err
    86  	}
    87  
    88  	userFacingRefName := container.FamiliarString(refs.ConfigurationRef)
    89  
    90  	switch bd := iTarget.BuildDetails.(type) {
    91  	case model.DockerBuild:
    92  		ps.StartPipelineStep(ctx, "Building Dockerfile: [%s]", userFacingRefName)
    93  		defer ps.EndPipelineStep(ctx)
    94  
    95  		filter := ignore.CreateBuildContextFilter(bd.DockerImageSpec.ContextIgnores)
    96  		return ib.db.BuildImage(ctx, ps, refs, bd.DockerImageSpec,
    97  			cluster,
    98  			imageMaps,
    99  			filter)
   100  
   101  	case model.CustomBuild:
   102  		ps.StartPipelineStep(ctx, "Building Custom Build: [%s]", userFacingRefName)
   103  		defer ps.EndPipelineStep(ctx)
   104  		refs, err := ib.custb.Build(ctx, refs, bd.CmdImageSpec, customBuildCmd, imageMaps)
   105  		return refs, nil, err
   106  	}
   107  
   108  	// Theoretically this should never trip b/c we `validate` the manifest beforehand...?
   109  	// If we get here, something is very wrong.
   110  	return container.TaggedRefs{}, nil, fmt.Errorf("image %q has no valid buildDetails (neither "+
   111  		"DockerBuild nor CustomBuild)", refs.ConfigurationRef)
   112  }
   113  
   114  // Push the image if the cluster requires it.
   115  func (ib *ImageBuilder) push(ctx context.Context, refs container.TaggedRefs, ps *PipelineState, iTarget model.ImageTarget, cluster *v1alpha1.Cluster) *v1alpha1.DockerImageStageStatus {
   116  	// Skip the push phase entirely if we're on Docker Compose.
   117  	isDC := cluster != nil &&
   118  		cluster.Spec.Connection != nil &&
   119  		cluster.Spec.Connection.Docker != nil
   120  	if isDC {
   121  		return nil
   122  	}
   123  
   124  	// On Kubernetes, we count each push() as a stage, and need to print why
   125  	// we're skipping if we don't need to push.
   126  	ps.StartPipelineStep(ctx, "Pushing %s", container.FamiliarString(refs.LocalRef))
   127  	defer ps.EndPipelineStep(ctx)
   128  
   129  	cbSkip := false
   130  	if iTarget.IsCustomBuild() {
   131  		cbSkip = iTarget.CustomBuildInfo().SkipsPush()
   132  	}
   133  
   134  	if cbSkip {
   135  		ps.Printf(ctx, "Skipping push: custom_build() configured to handle push itself")
   136  		return nil
   137  	}
   138  
   139  	// We can also skip the push of the image if it isn't used
   140  	// in any k8s resources! (e.g., it's consumed by another image).
   141  	if iTarget.ClusterNeeds() != v1alpha1.ClusterImageNeedsPush {
   142  		ps.Printf(ctx, "Skipping push: base image does not need deploy")
   143  		return nil
   144  	}
   145  
   146  	if ib.db.WillBuildToKubeContext(k8s.KubeContext(k8sConnStatus(cluster).Context)) {
   147  		ps.Printf(ctx, "Skipping push: building on cluster's container runtime")
   148  		return nil
   149  	}
   150  
   151  	startTime := apis.NowMicro()
   152  	var err error
   153  	if ib.shouldUseKINDLoad(refs, cluster) {
   154  		ps.Printf(ctx, "Loading image to KIND")
   155  		err := ib.kl.LoadToKIND(ps.AttachLogger(ctx), cluster, refs.LocalRef)
   156  		endTime := apis.NowMicro()
   157  		stage := &v1alpha1.DockerImageStageStatus{
   158  			Name:       "kind load",
   159  			StartedAt:  &startTime,
   160  			FinishedAt: &endTime,
   161  		}
   162  		if err != nil {
   163  			stage.Error = fmt.Sprintf("Error loading image to KIND: %v", err)
   164  		}
   165  		return stage
   166  	}
   167  
   168  	ps.Printf(ctx, "Pushing with Docker client")
   169  	err = ib.db.PushImage(ps.AttachLogger(ctx), refs.LocalRef)
   170  
   171  	endTime := apis.NowMicro()
   172  	stage := &v1alpha1.DockerImageStageStatus{
   173  		Name:       "docker push",
   174  		StartedAt:  &startTime,
   175  		FinishedAt: &endTime,
   176  	}
   177  	if err != nil {
   178  		stage.Error = fmt.Sprintf("docker push: %v", err)
   179  	}
   180  	return stage
   181  }
   182  
   183  func (ib *ImageBuilder) shouldUseKINDLoad(refs container.TaggedRefs, cluster *v1alpha1.Cluster) bool {
   184  	isKIND := k8sConnStatus(cluster).Product == string(clusterid.ProductKIND)
   185  	if !isKIND {
   186  		return false
   187  	}
   188  
   189  	// if we're using KIND and the image has a separate ref by which it's referred to
   190  	// in the cluster, that implies that we have a local registry in place, and should
   191  	// push to that instead of using KIND load.
   192  	if refs.LocalRef.String() != refs.ClusterRef.String() {
   193  		return false
   194  	}
   195  
   196  	hasRegistry := cluster.Status.Registry != nil && cluster.Status.Registry.Host != ""
   197  	if hasRegistry {
   198  		return false
   199  	}
   200  
   201  	return true
   202  }
   203  
   204  func k8sConnStatus(cluster *v1alpha1.Cluster) *v1alpha1.KubernetesClusterConnectionStatus {
   205  	if cluster != nil &&
   206  		cluster.Status.Connection != nil &&
   207  		cluster.Status.Connection.Kubernetes != nil {
   208  		return cluster.Status.Connection.Kubernetes
   209  	}
   210  	return &v1alpha1.KubernetesClusterConnectionStatus{}
   211  }