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  }