github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/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 extensionPrefix = "ext://" 27 const defaultRepoName = "default" 28 29 type Plugin struct { 30 repoReconciler ExtRepoReconciler 31 extReconciler ExtReconciler 32 } 33 34 func NewPlugin(repoReconciler *extensionrepo.Reconciler, extReconciler *extension.Reconciler) *Plugin { 35 return &Plugin{ 36 repoReconciler: repoReconciler, 37 extReconciler: extReconciler, 38 } 39 } 40 41 func NewFakePlugin(repoReconciler ExtRepoReconciler, extReconciler ExtReconciler) *Plugin { 42 return &Plugin{ 43 repoReconciler: repoReconciler, 44 extReconciler: extReconciler, 45 } 46 } 47 48 type State struct { 49 ExtsLoaded map[string]bool 50 } 51 52 func (e Plugin) NewState() interface{} { 53 return State{ 54 ExtsLoaded: make(map[string]bool), 55 } 56 } 57 58 func (e *Plugin) OnStart(env *starkit.Environment) error { 59 env.AddLoadInterceptor(e) 60 return nil 61 } 62 63 func (e *Plugin) recordExtensionLoaded(ctx context.Context, t *starlark.Thread, moduleName string) { 64 err := starkit.SetState(t, func(existing State) (State, error) { 65 existing.ExtsLoaded[moduleName] = true 66 return existing, nil 67 }) 68 if err != nil { 69 logger.Get(ctx).Debugf("error updating state on Tilt extensions loader: %v", err) 70 } 71 } 72 73 func (e *Plugin) LocalPath(t *starlark.Thread, arg string) (localPath string, err error) { 74 if !strings.HasPrefix(arg, extensionPrefix) { 75 return "", nil 76 } 77 78 ctx, err := starkit.ContextFromThread(t) 79 if err != nil { 80 return "", err 81 } 82 83 moduleName := strings.TrimPrefix(arg, extensionPrefix) 84 defer func() { 85 if err == nil { 86 // NOTE(maia): Maybe in future we want to track if there was an error or not? 87 // For now, only record on successful load. 88 e.recordExtensionLoaded(ctx, t, moduleName) 89 } 90 }() 91 92 starkitModel, err := starkit.ModelFromThread(t) 93 if err != nil { 94 return "", err 95 } 96 97 objSet, err := tiltfilev1alpha1.GetState(starkitModel) 98 if err != nil { 99 return "", err 100 } 101 102 ext := e.ensureExtension(t, objSet, moduleName) 103 repo := e.ensureRepo(t, objSet, ext.Spec.RepoName) 104 repoStatus := e.repoReconciler.ForceApply(ctx, repo) 105 if repoStatus.Error != "" { 106 return "", fmt.Errorf("loading extension repo %s: %s", repo.Name, repoStatus.Error) 107 } 108 if repoStatus.Path == "" { 109 return "", fmt.Errorf("extension repo not resolved: %s", repo.Name) 110 } 111 112 repoResolved := repo.DeepCopy() 113 repoResolved.Status = repoStatus 114 extStatus := e.extReconciler.ForceApply(ext, repoResolved) 115 if extStatus.Error != "" { 116 return "", fmt.Errorf("loading extension %s: %s", ext.Name, extStatus.Error) 117 } 118 if extStatus.Path == "" { 119 return "", fmt.Errorf("extension not resolved: %s", ext.Name) 120 } 121 122 return extStatus.Path, nil 123 } 124 125 // Check to see if an extension has already been registered. 126 // 127 // If it has, returns the existing object (which should only have a spec). 128 // 129 // Otherwise, infers an extension object that points to the default repo. 130 func (e *Plugin) ensureExtension(t *starlark.Thread, objSet apiset.ObjectSet, moduleName string) *v1alpha1.Extension { 131 extName := apis.SanitizeName(moduleName) 132 defaultExt := &v1alpha1.Extension{ 133 ObjectMeta: metav1.ObjectMeta{ 134 Name: extName, 135 Annotations: map[string]string{ 136 v1alpha1.AnnotationManagedBy: "tiltfile.loader", 137 }, 138 }, 139 Spec: v1alpha1.ExtensionSpec{ 140 RepoName: defaultRepoName, 141 RepoPath: moduleName, 142 }, 143 } 144 145 typedSet := objSet.GetOrCreateTypedSet(defaultExt) 146 existing, exists := typedSet[extName] 147 if exists { 148 ext := existing.(*v1alpha1.Extension) 149 metav1.SetMetaDataAnnotation(&ext.ObjectMeta, v1alpha1.AnnotationManagedBy, "tiltfile.loader") 150 return ext 151 } 152 153 typedSet[extName] = defaultExt 154 return defaultExt 155 } 156 157 // Check to see if an extension repo has already been registered. 158 // 159 // If it has, returns the existing object (which should only have a spec). 160 // 161 // Otherwise, register the default repo. 162 func (e *Plugin) ensureRepo(t *starlark.Thread, objSet apiset.ObjectSet, repoName string) *v1alpha1.ExtensionRepo { 163 defaultRepo := &v1alpha1.ExtensionRepo{ 164 ObjectMeta: metav1.ObjectMeta{ 165 Name: repoName, 166 }, 167 Spec: v1alpha1.ExtensionRepoSpec{ 168 URL: "https://github.com/tilt-dev/tilt-extensions", 169 }, 170 } 171 172 typedSet := objSet.GetOrCreateTypedSet(defaultRepo) 173 existing, exists := typedSet[repoName] 174 if exists { 175 return existing.(*v1alpha1.ExtensionRepo) 176 } 177 178 typedSet[repoName] = defaultRepo 179 return defaultRepo 180 } 181 182 var _ starkit.LoadInterceptor = (*Plugin)(nil) 183 var _ starkit.StatefulPlugin = (*Plugin)(nil) 184 185 func MustState(model starkit.Model) State { 186 state, err := GetState(model) 187 if err != nil { 188 panic(err) 189 } 190 return state 191 } 192 193 func GetState(m starkit.Model) (State, error) { 194 var state State 195 err := m.Load(&state) 196 return state, err 197 }