github.com/tilt-dev/tilt@v0.36.0/internal/tiltfile/tiltextension/plugin.go (about) 1 // Package extension implements Tilt extensions. 2 // This is not the internal Starkit abstraction, but the user-visible feature. 3 // In a Tiltfile, you can write `load("ext://foo", "bar")` to load the function bar 4 // from the extension foo. 5 package tiltextension 6 7 import ( 8 "context" 9 "fmt" 10 "strings" 11 12 "go.starlark.net/starlark" 13 14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 16 "github.com/tilt-dev/tilt/internal/controllers/apiset" 17 "github.com/tilt-dev/tilt/internal/controllers/core/extension" 18 "github.com/tilt-dev/tilt/internal/controllers/core/extensionrepo" 19 "github.com/tilt-dev/tilt/internal/tiltfile/starkit" 20 tiltfilev1alpha1 "github.com/tilt-dev/tilt/internal/tiltfile/v1alpha1" 21 "github.com/tilt-dev/tilt/pkg/apis" 22 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 23 "github.com/tilt-dev/tilt/pkg/logger" 24 ) 25 26 const ( 27 extensionPrefix = "ext://" 28 defaultRepoName = "default" 29 ) 30 31 type Plugin struct { 32 repoReconciler ExtRepoReconciler 33 extReconciler ExtReconciler 34 } 35 36 func NewPlugin(repoReconciler *extensionrepo.Reconciler, extReconciler *extension.Reconciler) *Plugin { 37 return &Plugin{ 38 repoReconciler: repoReconciler, 39 extReconciler: extReconciler, 40 } 41 } 42 43 func NewFakePlugin(repoReconciler ExtRepoReconciler, extReconciler ExtReconciler) *Plugin { 44 return &Plugin{ 45 repoReconciler: repoReconciler, 46 extReconciler: extReconciler, 47 } 48 } 49 50 type State struct { 51 ExtsLoaded map[string]bool 52 } 53 54 func (e Plugin) NewState() interface{} { 55 return State{ 56 ExtsLoaded: make(map[string]bool), 57 } 58 } 59 60 func (e *Plugin) OnStart(env *starkit.Environment) error { 61 env.AddLoadInterceptor(e) 62 return nil 63 } 64 65 func (e *Plugin) recordExtensionLoaded(ctx context.Context, t *starlark.Thread, moduleName string) { 66 err := starkit.SetState(t, func(existing State) (State, error) { 67 existing.ExtsLoaded[moduleName] = true 68 return existing, nil 69 }) 70 if err != nil { 71 logger.Get(ctx).Debugf("error updating state on Tilt extensions loader: %v", err) 72 } 73 } 74 75 func (e *Plugin) LocalPath(t *starlark.Thread, arg string) (localPath string, err error) { 76 if !strings.HasPrefix(arg, extensionPrefix) { 77 return "", nil 78 } 79 80 ctx, err := starkit.ContextFromThread(t) 81 if err != nil { 82 return "", err 83 } 84 85 moduleName := strings.TrimPrefix(arg, extensionPrefix) 86 defer func() { 87 if err == nil { 88 // NOTE(maia): Maybe in future we want to track if there was an error or not? 89 // For now, only record on successful load. 90 e.recordExtensionLoaded(ctx, t, moduleName) 91 } 92 }() 93 94 starkitModel, err := starkit.ModelFromThread(t) 95 if err != nil { 96 return "", err 97 } 98 99 objSet, err := tiltfilev1alpha1.GetState(starkitModel) 100 if err != nil { 101 return "", err 102 } 103 104 ext := e.ensureExtension(t, objSet, moduleName) 105 repo := e.ensureRepo(t, objSet, ext.Spec.RepoName) 106 repoStatus := e.repoReconciler.ForceApply(ctx, repo) 107 if repoStatus.Error != "" { 108 return "", fmt.Errorf("loading extension repo %s: %s", repo.Name, repoStatus.Error) 109 } 110 if repoStatus.Path == "" { 111 return "", fmt.Errorf("extension repo not resolved: %s", repo.Name) 112 } 113 114 repoResolved := repo.DeepCopy() 115 repoResolved.Status = repoStatus 116 extStatus := e.extReconciler.ForceApply(ext, repoResolved) 117 if extStatus.Error != "" { 118 return "", fmt.Errorf("loading extension %s: %s", ext.Name, extStatus.Error) 119 } 120 if extStatus.Path == "" { 121 return "", fmt.Errorf("extension not resolved: %s", ext.Name) 122 } 123 124 return extStatus.Path, nil 125 } 126 127 // Check to see if an extension has already been registered. 128 // 129 // If it has, returns the existing object (which should only have a spec). 130 // 131 // Otherwise, infers an extension object that points to the default repo. 132 func (e *Plugin) ensureExtension(t *starlark.Thread, objSet apiset.ObjectSet, moduleName string) *v1alpha1.Extension { 133 extName := apis.SanitizeName(moduleName) 134 135 extSet := objSet.GetOrCreateTypedSet(&v1alpha1.Extension{}) 136 if existing, exists := extSet[extName]; exists { 137 ext := existing.(*v1alpha1.Extension) 138 metav1.SetMetaDataAnnotation(&ext.ObjectMeta, v1alpha1.AnnotationManagedBy, "tiltfile.loader") 139 return ext 140 } 141 142 repoSet := objSet.GetOrCreateTypedSet(&v1alpha1.ExtensionRepo{}) 143 144 return e.registerExtension(t, extSet, repoSet, extName, moduleName) 145 } 146 147 // In cases where an extension is not already registered, this function will search for an extension 148 // repo that can satisfy the requested extension, with a fallback to an extension in the default 149 // repository. 150 func (e *Plugin) registerExtension(t *starlark.Thread, extSet, repoSet apiset.TypedObjectSet, extName, moduleName string) *v1alpha1.Extension { 151 loadHost, extPath, tryRegister := strings.Cut(moduleName, "/") 152 153 // Safety fallback (in case this is called without already previously calling ensureExtension) 154 existing := extSet[extName] 155 if existing != nil { 156 return existing.(*v1alpha1.Extension) 157 } 158 159 // If the supplied module name does not contain a / then there's no point looking for matching 160 // extension repositories. We can just return an extension named extName in the default 161 // repository 162 if !tryRegister { 163 return e.registerDefaultExtension(t, extSet, extName, moduleName) 164 } 165 166 // Otherwise, look for a repository that can satisfy this lookup 167 for _, v := range repoSet { 168 repo := v.(*v1alpha1.ExtensionRepo) 169 if repo.Spec.LoadHost == "" || repo.Spec.LoadHost != loadHost { 170 continue 171 } 172 173 // This repo load_host matches the first component of the module name 174 // So we can register this as an extension on that repo 175 ext := &v1alpha1.Extension{ 176 ObjectMeta: metav1.ObjectMeta{ 177 Name: extName, 178 Annotations: map[string]string{ 179 v1alpha1.AnnotationManagedBy: "tiltfile.loader", 180 }, 181 }, 182 Spec: v1alpha1.ExtensionSpec{ 183 RepoName: repo.Name, 184 RepoPath: extPath, 185 }, 186 } 187 188 extSet[extName] = ext 189 return ext 190 } 191 192 return e.registerDefaultExtension(t, extSet, extName, moduleName) 193 } 194 195 // Registers an extension named moduleName in the default extension repository. Used as a fallback. 196 func (e *Plugin) registerDefaultExtension(t *starlark.Thread, extSet apiset.TypedObjectSet, extName, moduleName string) *v1alpha1.Extension { 197 // Safety fallback 198 existing := extSet[extName] 199 if existing != nil { 200 return existing.(*v1alpha1.Extension) 201 } 202 203 defaultExt := &v1alpha1.Extension{ 204 ObjectMeta: metav1.ObjectMeta{ 205 Name: extName, 206 Annotations: map[string]string{ 207 v1alpha1.AnnotationManagedBy: "tiltfile.loader", 208 }, 209 }, 210 Spec: v1alpha1.ExtensionSpec{ 211 RepoName: defaultRepoName, 212 RepoPath: moduleName, 213 }, 214 } 215 216 extSet[extName] = defaultExt 217 return defaultExt 218 } 219 220 // Check to see if an extension repo has already been registered. 221 // 222 // If it has, returns the existing object (which should only have a spec). 223 // 224 // Otherwise, register the default repo. 225 func (e *Plugin) ensureRepo(t *starlark.Thread, objSet apiset.ObjectSet, repoName string) *v1alpha1.ExtensionRepo { 226 defaultRepo := &v1alpha1.ExtensionRepo{ 227 ObjectMeta: metav1.ObjectMeta{ 228 Name: repoName, 229 }, 230 Spec: v1alpha1.ExtensionRepoSpec{ 231 URL: "https://github.com/tilt-dev/tilt-extensions", 232 }, 233 } 234 235 typedSet := objSet.GetOrCreateTypedSet(defaultRepo) 236 existing, exists := typedSet[repoName] 237 if exists { 238 return existing.(*v1alpha1.ExtensionRepo) 239 } 240 241 typedSet[repoName] = defaultRepo 242 return defaultRepo 243 } 244 245 var ( 246 _ starkit.LoadInterceptor = (*Plugin)(nil) 247 _ starkit.StatefulPlugin = (*Plugin)(nil) 248 ) 249 250 func MustState(model starkit.Model) State { 251 state, err := GetState(model) 252 if err != nil { 253 panic(err) 254 } 255 return state 256 } 257 258 func GetState(m starkit.Model) (State, error) { 259 var state State 260 err := m.Load(&state) 261 return state, err 262 }