github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/tiltfile.go (about) 1 package tiltfile 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strconv" 9 "time" 10 11 "go.starlark.net/starlark" 12 13 "github.com/tilt-dev/clusterid" 14 "github.com/tilt-dev/tilt/internal/analytics" 15 "github.com/tilt-dev/tilt/internal/controllers/apiset" 16 "github.com/tilt-dev/tilt/internal/dockercompose" 17 "github.com/tilt-dev/tilt/internal/feature" 18 "github.com/tilt-dev/tilt/internal/k8s" 19 "github.com/tilt-dev/tilt/internal/localexec" 20 "github.com/tilt-dev/tilt/internal/ospath" 21 "github.com/tilt-dev/tilt/internal/sliceutils" 22 tiltfileanalytics "github.com/tilt-dev/tilt/internal/tiltfile/analytics" 23 "github.com/tilt-dev/tilt/internal/tiltfile/cisettings" 24 "github.com/tilt-dev/tilt/internal/tiltfile/config" 25 "github.com/tilt-dev/tilt/internal/tiltfile/dockerprune" 26 "github.com/tilt-dev/tilt/internal/tiltfile/hasher" 27 "github.com/tilt-dev/tilt/internal/tiltfile/io" 28 "github.com/tilt-dev/tilt/internal/tiltfile/k8scontext" 29 "github.com/tilt-dev/tilt/internal/tiltfile/secretsettings" 30 "github.com/tilt-dev/tilt/internal/tiltfile/starkit" 31 "github.com/tilt-dev/tilt/internal/tiltfile/telemetry" 32 "github.com/tilt-dev/tilt/internal/tiltfile/tiltextension" 33 "github.com/tilt-dev/tilt/internal/tiltfile/updatesettings" 34 "github.com/tilt-dev/tilt/internal/tiltfile/v1alpha1" 35 "github.com/tilt-dev/tilt/internal/tiltfile/value" 36 "github.com/tilt-dev/tilt/internal/tiltfile/version" 37 "github.com/tilt-dev/tilt/internal/tiltfile/watch" 38 corev1alpha1 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 39 "github.com/tilt-dev/tilt/pkg/model" 40 wmanalytics "github.com/tilt-dev/wmclient/pkg/analytics" 41 ) 42 43 const FileName = "Tiltfile" 44 45 type TiltfileLoadResult struct { 46 Manifests []model.Manifest 47 EnabledManifests []model.ManifestName 48 Tiltignore model.Dockerignore 49 ConfigFiles []string 50 FeatureFlags map[string]bool 51 TeamID string 52 TelemetrySettings model.TelemetrySettings 53 Secrets model.SecretSet 54 Error error 55 DockerPruneSettings model.DockerPruneSettings 56 AnalyticsOpt wmanalytics.Opt 57 VersionSettings model.VersionSettings 58 UpdateSettings model.UpdateSettings 59 WatchSettings model.WatchSettings 60 DefaultRegistry *corev1alpha1.RegistryHosting 61 ObjectSet apiset.ObjectSet 62 Hashes hasher.Hashes 63 CISettings *corev1alpha1.SessionCISpec 64 65 // For diagnostic purposes only 66 BuiltinCalls []starkit.BuiltinCall `json:"-"` 67 } 68 69 func (r TiltfileLoadResult) HasOrchestrator(orc model.Orchestrator) bool { 70 for _, manifest := range r.Manifests { 71 if manifest.IsK8s() && orc == model.OrchestratorK8s { 72 return true 73 } else if manifest.IsDC() && orc == model.OrchestratorDC { 74 return true 75 } 76 } 77 return false 78 } 79 80 func (r TiltfileLoadResult) WithAllManifestsEnabled() TiltfileLoadResult { 81 r.EnabledManifests = nil 82 for _, m := range r.Manifests { 83 r.EnabledManifests = append(r.EnabledManifests, m.Name) 84 } 85 return r 86 } 87 88 type TiltfileLoader interface { 89 // Load the Tiltfile. 90 // 91 // By design, Load() always returns a result. 92 // We want to be very careful not to treat non-zero exit codes like an error. 93 // Because even if the Tiltfile has errors, we might need to watch files 94 // or return partial results (like enabled features). 95 Load(ctx context.Context, tf *corev1alpha1.Tiltfile, prevResult *TiltfileLoadResult) TiltfileLoadResult 96 } 97 98 func ProvideTiltfileLoader( 99 analytics *analytics.TiltAnalytics, 100 k8sContextPlugin k8scontext.Plugin, 101 versionPlugin version.Plugin, 102 configPlugin *config.Plugin, 103 extensionPlugin *tiltextension.Plugin, 104 ciSettingsPlugin cisettings.Plugin, 105 dcCli dockercompose.DockerComposeClient, 106 webHost model.WebHost, 107 execer localexec.Execer, 108 fDefaults feature.Defaults, 109 env clusterid.Product) TiltfileLoader { 110 return tiltfileLoader{ 111 analytics: analytics, 112 k8sContextPlugin: k8sContextPlugin, 113 versionPlugin: versionPlugin, 114 configPlugin: configPlugin, 115 extensionPlugin: extensionPlugin, 116 ciSettingsPlugin: ciSettingsPlugin, 117 dcCli: dcCli, 118 webHost: webHost, 119 execer: execer, 120 fDefaults: fDefaults, 121 env: env, 122 } 123 } 124 125 type tiltfileLoader struct { 126 analytics *analytics.TiltAnalytics 127 dcCli dockercompose.DockerComposeClient 128 webHost model.WebHost 129 execer localexec.Execer 130 131 k8sContextPlugin k8scontext.Plugin 132 versionPlugin version.Plugin 133 configPlugin *config.Plugin 134 extensionPlugin *tiltextension.Plugin 135 ciSettingsPlugin cisettings.Plugin 136 fDefaults feature.Defaults 137 env clusterid.Product 138 } 139 140 var _ TiltfileLoader = &tiltfileLoader{} 141 142 // Load loads the Tiltfile in `filename` 143 func (tfl tiltfileLoader) Load(ctx context.Context, tf *corev1alpha1.Tiltfile, prevResult *TiltfileLoadResult) TiltfileLoadResult { 144 start := time.Now() 145 filename := tf.Spec.Path 146 absFilename, err := ospath.RealAbs(tf.Spec.Path) 147 if err != nil { 148 if os.IsNotExist(err) { 149 return TiltfileLoadResult{ 150 ConfigFiles: []string{filename}, 151 Error: fmt.Errorf("No Tiltfile found at paths '%s'. Check out https://docs.tilt.dev/tutorial.html", filename), 152 } 153 } 154 absFilename, _ = filepath.Abs(filename) 155 return TiltfileLoadResult{ 156 ConfigFiles: []string{absFilename}, 157 Error: err, 158 } 159 } 160 161 tiltignorePath := watch.TiltignorePath(absFilename) 162 tlr := TiltfileLoadResult{ 163 ConfigFiles: []string{absFilename, tiltignorePath}, 164 } 165 166 tiltignore, err := watch.ReadTiltignore(tiltignorePath) 167 168 // missing tiltignore is fine, but a filesystem error is not 169 if err != nil { 170 tlr.Error = err 171 return tlr 172 } 173 174 tlr.Tiltignore = tiltignore 175 176 s := newTiltfileState(ctx, tfl.dcCli, tfl.webHost, tfl.execer, tfl.k8sContextPlugin, tfl.versionPlugin, 177 tfl.configPlugin, tfl.extensionPlugin, tfl.ciSettingsPlugin, feature.FromDefaults(tfl.fDefaults)) 178 179 manifests, result, err := s.loadManifests(tf) 180 181 tlr.BuiltinCalls = result.BuiltinCalls 182 tlr.DefaultRegistry = s.defaultReg 183 184 // All data models are loaded with GetState. We ignore the error if the state 185 // isn't properly loaded. This is necessary for handling partial Tiltfile 186 // execution correctly, where some state is correctly assembled but other 187 // state is not (and should be assumed empty). 188 ws, _ := watch.GetState(result) 189 tlr.WatchSettings = ws 190 191 // NOTE(maia): if/when add secret settings that affect the engine, add them to tlr here 192 ss, _ := secretsettings.GetState(result) 193 s.secretSettings = ss 194 195 ioState, _ := io.GetState(result) 196 197 tlr.ConfigFiles = append(tlr.ConfigFiles, ioState.Paths...) 198 tlr.ConfigFiles = append(tlr.ConfigFiles, s.postExecReadFiles...) 199 tlr.ConfigFiles = sliceutils.DedupedAndSorted(tlr.ConfigFiles) 200 201 dps, _ := dockerprune.GetState(result) 202 tlr.DockerPruneSettings = dps 203 204 aSettings, _ := tiltfileanalytics.GetState(result) 205 tlr.AnalyticsOpt = aSettings.Opt 206 207 tlr.Secrets = s.extractSecrets() 208 tlr.FeatureFlags = s.features.ToEnabled() 209 tlr.Error = err 210 tlr.Manifests = manifests 211 tlr.TeamID = s.teamID 212 213 objectSet, _ := v1alpha1.GetState(result) 214 tlr.ObjectSet = objectSet 215 216 vs, _ := version.GetState(result) 217 tlr.VersionSettings = vs 218 219 telemetrySettings, _ := telemetry.GetState(result) 220 tlr.TelemetrySettings = telemetrySettings 221 222 us, _ := updatesettings.GetState(result) 223 tlr.UpdateSettings = us 224 225 ci, _ := cisettings.GetState(result) 226 tlr.CISettings = ci 227 228 configSettings, _ := config.GetState(result) 229 if tlr.Error == nil { 230 tlr.EnabledManifests, tlr.Error = configSettings.EnabledResources(tf, manifests) 231 } 232 233 duration := time.Since(start) 234 if tlr.Error == nil { 235 s.logger.Infof("Successfully loaded Tiltfile (%s)", duration) 236 } 237 extState, _ := tiltextension.GetState(result) 238 hashState, _ := hasher.GetState(result) 239 240 var prevHashes hasher.Hashes 241 if prevResult != nil { 242 prevHashes = prevResult.Hashes 243 } 244 tlr.Hashes = hashState.GetHashes() 245 246 tfl.reportTiltfileLoaded(s.builtinCallCounts, s.builtinArgCounts, duration, 247 extState.ExtsLoaded, prevHashes, tlr.Hashes) 248 249 if len(aSettings.CustomTagsToReport) > 0 { 250 reportCustomTags(tfl.analytics, aSettings.CustomTagsToReport) 251 } 252 253 return tlr 254 } 255 256 func starlarkValueOrSequenceToSlice(v starlark.Value) []starlark.Value { 257 return value.ValueOrSequenceToSlice(v) 258 } 259 260 func reportCustomTags(a *analytics.TiltAnalytics, tags map[string]string) { 261 a.Incr("tiltfile.custom.report", tags) 262 } 263 264 func (tfl *tiltfileLoader) reportTiltfileLoaded( 265 callCounts map[string]int, 266 argCounts map[string]map[string]int, 267 loadDur time.Duration, 268 pluginsLoaded map[string]bool, 269 prevHashes hasher.Hashes, 270 currHashes hasher.Hashes, 271 ) { 272 tags := make(map[string]string) 273 274 // env should really be a global tag, but there's a circular dependency 275 // between the global tags and env initialization, so we add it manually. 276 tags["env"] = k8s.AnalyticsEnv(tfl.env) 277 tags["tiltfile.changed"] = strconv.FormatBool(prevHashes.TiltfileSHA256 != "" && prevHashes.TiltfileSHA256 != currHashes.TiltfileSHA256) 278 tags["allfiles.changed"] = strconv.FormatBool(prevHashes.AllFilesSHA256 != "" && prevHashes.AllFilesSHA256 != currHashes.AllFilesSHA256) 279 280 for builtinName, count := range callCounts { 281 tags[fmt.Sprintf("tiltfile.invoked.%s", builtinName)] = strconv.Itoa(count) 282 } 283 for builtinName, counts := range argCounts { 284 for argName, count := range counts { 285 tags[fmt.Sprintf("tiltfile.invoked.%s.arg.%s", builtinName, argName)] = strconv.Itoa(count) 286 } 287 } 288 tfl.analytics.Incr("tiltfile.loaded", tags) 289 tfl.analytics.Timer("tiltfile.load", loadDur, nil) 290 for ext := range pluginsLoaded { 291 tags := map[string]string{ 292 "env": k8s.AnalyticsEnv(tfl.env), 293 "ext_name": ext, 294 } 295 tfl.analytics.Incr("tiltfile.loaded.plugin", tags) 296 } 297 }