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

     1  package build
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/distribution/reference"
    10  	"github.com/opencontainers/go-digest"
    11  	"github.com/pkg/errors"
    12  	ktypes "k8s.io/apimachinery/pkg/types"
    13  
    14  	"github.com/tilt-dev/tilt/internal/container"
    15  	"github.com/tilt-dev/tilt/internal/controllers/apis/imagemap"
    16  	"github.com/tilt-dev/tilt/internal/controllers/core/cmd"
    17  	"github.com/tilt-dev/tilt/internal/docker"
    18  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    19  	"github.com/tilt-dev/tilt/pkg/logger"
    20  	"github.com/tilt-dev/tilt/pkg/model"
    21  )
    22  
    23  type CustomBuilder struct {
    24  	dCli  docker.Client
    25  	clock Clock
    26  	cmds  *cmd.Controller
    27  }
    28  
    29  func NewCustomBuilder(dCli docker.Client, clock Clock, cmds *cmd.Controller) *CustomBuilder {
    30  	return &CustomBuilder{
    31  		dCli:  dCli,
    32  		clock: clock,
    33  		cmds:  cmds,
    34  	}
    35  }
    36  
    37  func (b *CustomBuilder) Build(ctx context.Context, refs container.RefSet,
    38  	spec v1alpha1.CmdImageSpec,
    39  	cmd *v1alpha1.Cmd,
    40  	imageMaps map[ktypes.NamespacedName]*v1alpha1.ImageMap) (container.TaggedRefs, error) {
    41  	expectedTag := spec.OutputTag
    42  	outputsImageRefTo := spec.OutputsImageRefTo
    43  	var registryHost string
    44  	reg := refs.Registry()
    45  	if reg != nil {
    46  		registryHost = reg.Host
    47  	}
    48  
    49  	var expectedBuildRefs container.TaggedRefs
    50  	var err error
    51  
    52  	// There are 3 modes for determining the output tag.
    53  	if outputsImageRefTo != "" {
    54  		// In outputs_image_ref_to mode, the user script MUST print the tag to a file,
    55  		// which we recover later. So no need to set expectedBuildRefs.
    56  
    57  		// Remove the output file, ignoring any errors.
    58  		_ = os.Remove(outputsImageRefTo)
    59  	} else if expectedTag != "" {
    60  		// If the tag is coming from the user script, we expect that the user script
    61  		// also doesn't know about the local registry. So we have to strip off
    62  		// the registry, and re-add it later.
    63  		expectedBuildRefs, err = refs.WithoutRegistry().AddTagSuffix(expectedTag)
    64  		if err != nil {
    65  			return container.TaggedRefs{}, errors.Wrap(err, "custom_build")
    66  		}
    67  	} else {
    68  		// In "normal" mode, the user's script should use whichever registry tag we give it.
    69  		expectedBuildRefs, err = refs.AddTagSuffix(fmt.Sprintf("tilt-build-%d", b.clock.Now().Unix()))
    70  		if err != nil {
    71  			return container.TaggedRefs{}, errors.Wrap(err, "custom_build")
    72  		}
    73  	}
    74  
    75  	expectedBuildResult := expectedBuildRefs.LocalRef
    76  
    77  	cmd = cmd.DeepCopy()
    78  
    79  	l := logger.Get(ctx)
    80  
    81  	extraEnvVars := []string{}
    82  	if expectedBuildResult != nil {
    83  		extraEnvVars = append(extraEnvVars,
    84  			fmt.Sprintf("EXPECTED_REF=%s", container.FamiliarString(expectedBuildResult)))
    85  		extraEnvVars = append(extraEnvVars,
    86  			fmt.Sprintf("EXPECTED_IMAGE=%s", reference.Path(expectedBuildResult)))
    87  		extraEnvVars = append(extraEnvVars,
    88  			fmt.Sprintf("EXPECTED_TAG=%s", expectedBuildResult.Tag()))
    89  	}
    90  	if registryHost != "" {
    91  		// kept for backwards compatibility
    92  		extraEnvVars = append(extraEnvVars,
    93  			fmt.Sprintf("REGISTRY_HOST=%s", registryHost))
    94  		// for consistency with other EXPECTED_* vars
    95  		extraEnvVars = append(extraEnvVars,
    96  			fmt.Sprintf("EXPECTED_REGISTRY=%s", registryHost))
    97  	}
    98  
    99  	extraEnvVars = append(extraEnvVars, b.dCli.Env().AsEnviron()...)
   100  
   101  	if len(extraEnvVars) == 0 {
   102  		l.Infof("Custom Build:")
   103  	} else {
   104  		l.Infof("Custom Build: Injecting Environment Variables")
   105  		for _, v := range extraEnvVars {
   106  			l.Infof("  %s", v)
   107  		}
   108  	}
   109  	cmd.Spec.Env = append(cmd.Spec.Env, spec.Env...)
   110  	cmd.Spec.Env = append(cmd.Spec.Env, extraEnvVars...)
   111  	cmd, err = imagemap.InjectIntoLocalEnv(cmd, spec.ImageMaps, imageMaps)
   112  	if err != nil {
   113  		return container.TaggedRefs{}, errors.Wrap(err, "custom_build")
   114  	}
   115  
   116  	status, err := b.cmds.ForceRun(ctx, cmd)
   117  	if err != nil {
   118  		return container.TaggedRefs{}, fmt.Errorf("Custom build %q failed: %v",
   119  			model.ArgListToString(cmd.Spec.Args), err)
   120  	} else if status.Terminated == nil {
   121  		return container.TaggedRefs{}, fmt.Errorf("Custom build didn't terminate")
   122  	} else if status.Terminated.ExitCode != 0 {
   123  		return container.TaggedRefs{}, fmt.Errorf("Custom build %q failed: %v",
   124  			model.ArgListToString(cmd.Spec.Args), status.Terminated.Reason)
   125  	}
   126  
   127  	if outputsImageRefTo != "" {
   128  		expectedBuildRefs, err = b.readImageRef(ctx, outputsImageRefTo, reg)
   129  		if err != nil {
   130  			return container.TaggedRefs{}, err
   131  		}
   132  		expectedBuildResult = expectedBuildRefs.LocalRef
   133  	}
   134  
   135  	// If the command skips the local docker registry, then we don't expect the image
   136  	// to be available (because the command has its own registry).
   137  	if spec.OutputMode == v1alpha1.CmdImageOutputRemote {
   138  		return expectedBuildRefs, nil
   139  	}
   140  
   141  	inspect, _, err := b.dCli.ImageInspectWithRaw(ctx, expectedBuildResult.String())
   142  	if err != nil {
   143  		return container.TaggedRefs{}, errors.Wrap(err, "Could not find image in Docker\n"+
   144  			"Did your custom_build script properly tag the image?\n"+
   145  			"If your custom_build doesn't use Docker, you might need to use skips_local_docker=True, "+
   146  			"see https://docs.tilt.dev/custom_build.html\n")
   147  	}
   148  
   149  	if outputsImageRefTo != "" {
   150  		// If we're using a custom_build-determined build ref, we don't use content-based tags.
   151  		return expectedBuildRefs, nil
   152  	}
   153  
   154  	dig := digest.Digest(inspect.ID)
   155  
   156  	tag, err := digestAsTag(dig)
   157  	if err != nil {
   158  		return container.TaggedRefs{}, errors.Wrap(err, "custom_build")
   159  	}
   160  
   161  	taggedWithDigest, err := refs.AddTagSuffix(tag)
   162  	if err != nil {
   163  		return container.TaggedRefs{}, errors.Wrap(err, "custom_build")
   164  	}
   165  
   166  	// Docker client only needs to care about the localImage
   167  	err = b.dCli.ImageTag(ctx, dig.String(), taggedWithDigest.LocalRef.String())
   168  	if err != nil {
   169  		return container.TaggedRefs{}, errors.Wrap(err, "custom_build")
   170  	}
   171  
   172  	return taggedWithDigest, nil
   173  }
   174  
   175  func (b *CustomBuilder) readImageRef(ctx context.Context, outputsImageRefTo string, reg *v1alpha1.RegistryHosting) (container.TaggedRefs, error) {
   176  	contents, err := os.ReadFile(outputsImageRefTo)
   177  	if err != nil {
   178  		return container.TaggedRefs{}, fmt.Errorf("Could not find image ref in output. Your custom_build script should have written to %s: %v", outputsImageRefTo, err)
   179  	}
   180  
   181  	refStr := strings.TrimSpace(string(contents))
   182  	ref, err := container.ParseNamedTagged(refStr)
   183  	if err != nil {
   184  		return container.TaggedRefs{}, fmt.Errorf("Output image ref in file %s was invalid: %v",
   185  			outputsImageRefTo, err)
   186  	}
   187  
   188  	clusterRef := ref
   189  	if reg != nil && reg.HostFromContainerRuntime != "" {
   190  		replacedName, err := container.ParseNamed(strings.Replace(ref.Name(), reg.Host, reg.HostFromContainerRuntime, 1))
   191  		if err != nil {
   192  			return container.TaggedRefs{}, fmt.Errorf("Error converting image ref for cluster: %w", err)
   193  		}
   194  		clusterRef, err = reference.WithTag(replacedName, ref.Tag())
   195  		if err != nil {
   196  			return container.TaggedRefs{}, fmt.Errorf("Error converting image ref for cluster: %w", err)
   197  		}
   198  	}
   199  
   200  	return container.TaggedRefs{
   201  		LocalRef:   ref,
   202  		ClusterRef: clusterRef,
   203  	}, nil
   204  }