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 }