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  }