github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/hud/server/controller.go (about)

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"net/http"
    10  	"regexp"
    11  
    12  	"github.com/gorilla/mux"
    13  	genericapiserver "k8s.io/apiserver/pkg/server"
    14  	"k8s.io/client-go/rest"
    15  	"k8s.io/client-go/tools/clientcmd"
    16  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    17  	"k8s.io/kubectl/pkg/proxy"
    18  
    19  	"github.com/tilt-dev/tilt-apiserver/pkg/server/start"
    20  	"github.com/tilt-dev/tilt/internal/filelock"
    21  	"github.com/tilt-dev/tilt/internal/store"
    22  	"github.com/tilt-dev/tilt/pkg/assets"
    23  	"github.com/tilt-dev/tilt/pkg/model"
    24  )
    25  
    26  // apiServerProxyPrefix routes web HUD requests to the apiserver as it cannot contact it directly.
    27  //
    28  // This prefix is stripped from the subsequent request to the apiserver, e.g. to list API versions,
    29  // `/proxy/apis` --> `/apis`.
    30  //
    31  // NOTE: The kubectl ProxyHandler code has some odd behavior in this regard in that it only strips
    32  //
    33  //	the prefix if it does not start with `/api`. As a result, something like a prefix of `/apiserver`
    34  //	will cause problems because `/apiserver/apis/foo` will be passed as-is, which is why `/proxy` was
    35  //	chosen here.
    36  const apiServerProxyPrefix = "/proxy"
    37  
    38  type HeadsUpServerController struct {
    39  	// configAccess may be nil in cases where we don't
    40  	// want to persist the config to disk.
    41  	configAccess clientcmd.ConfigAccess
    42  
    43  	apiServerName   model.APIServerName
    44  	webListener     WebListener
    45  	hudServer       *HeadsUpServer
    46  	assetServer     assets.Server
    47  	apiServer       *http.Server
    48  	webServer       *http.Server
    49  	webURL          model.WebURL
    50  	apiServerConfig *APIServerConfig
    51  
    52  	shutdown func()
    53  }
    54  
    55  func ProvideHeadsUpServerController(
    56  	configAccess clientcmd.ConfigAccess,
    57  	apiServerName model.APIServerName,
    58  	webListener WebListener,
    59  	apiServerConfig *APIServerConfig,
    60  	hudServer *HeadsUpServer,
    61  	assetServer assets.Server,
    62  	webURL model.WebURL) *HeadsUpServerController {
    63  
    64  	emptyCh := make(chan struct{})
    65  	close(emptyCh)
    66  
    67  	return &HeadsUpServerController{
    68  		configAccess:    configAccess,
    69  		apiServerName:   apiServerName,
    70  		webListener:     webListener,
    71  		hudServer:       hudServer,
    72  		assetServer:     assetServer,
    73  		webURL:          webURL,
    74  		apiServerConfig: apiServerConfig,
    75  		shutdown:        func() {},
    76  	}
    77  }
    78  
    79  func (s *HeadsUpServerController) TearDown(ctx context.Context) {
    80  	s.shutdown()
    81  	s.assetServer.TearDown(ctx)
    82  
    83  	// Close all active connections immediately.
    84  	// Tilt is deleting all its state, so there's no good
    85  	// reason to handle graceful shutdown.
    86  	_ = s.webServer.Close()
    87  	_ = s.apiServer.Close()
    88  
    89  	_ = s.removeFromAPIServerConfig()
    90  }
    91  
    92  func (s *HeadsUpServerController) OnChange(ctx context.Context, st store.RStore, _ store.ChangeSummary) error {
    93  	return nil
    94  }
    95  
    96  // Merge the APIServer and the Tilt Web server into a single handler,
    97  // and attach them both to the public listener.
    98  func (s *HeadsUpServerController) SetUp(ctx context.Context, st store.RStore) error {
    99  	ctx, cancel := context.WithCancel(ctx)
   100  	s.shutdown = cancel
   101  
   102  	err := s.setUpHelper(ctx, st)
   103  	if err != nil {
   104  		return fmt.Errorf("Cannot start the tilt-apiserver: %v", err)
   105  	}
   106  	err = s.addToAPIServerConfig()
   107  	if err != nil {
   108  		return fmt.Errorf("writing tilt api configs: %v", err)
   109  	}
   110  	return nil
   111  }
   112  
   113  func (s *HeadsUpServerController) setUpHelper(ctx context.Context, st store.RStore) error {
   114  	stopCh := ctx.Done()
   115  	config := s.apiServerConfig
   116  	server, err := config.Complete().New()
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	err = server.GenericAPIServer.AddPostStartHook("start-tilt-server-informers", func(context genericapiserver.PostStartHookContext) error {
   122  		if config.GenericConfig.SharedInformerFactory != nil {
   123  			config.GenericConfig.SharedInformerFactory.Start(context.StopCh)
   124  		}
   125  		return nil
   126  	})
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	prepared := server.GenericAPIServer.PrepareRun()
   132  	apiserverHandler := prepared.Handler
   133  	serving := config.ExtraConfig.ServingInfo
   134  
   135  	apiRouter := mux.NewRouter()
   136  	apiRouter.Path("/api").Handler(http.NotFoundHandler())
   137  	apiRouter.PathPrefix("/apis").Handler(apiserverHandler)
   138  	apiRouter.PathPrefix("/healthz").Handler(apiserverHandler)
   139  	apiRouter.PathPrefix("/livez").Handler(apiserverHandler)
   140  	apiRouter.PathPrefix("/metrics").Handler(apiserverHandler)
   141  	apiRouter.PathPrefix("/openapi").Handler(apiserverHandler)
   142  	apiRouter.PathPrefix("/readyz").Handler(apiserverHandler)
   143  	apiRouter.PathPrefix("/swagger").Handler(apiserverHandler)
   144  	apiRouter.PathPrefix("/version").Handler(apiserverHandler)
   145  	apiRouter.PathPrefix("/debug").Handler(http.DefaultServeMux) // for /debug/pprof
   146  
   147  	var apiTLSConfig *tls.Config
   148  	if serving.Cert != nil {
   149  		apiTLSConfig, err = start.TLSConfig(ctx, serving)
   150  		if err != nil {
   151  			return fmt.Errorf("Starting apiserver: %v", err)
   152  		}
   153  	}
   154  
   155  	proxyHandler, err := newAPIServerProxyHandler(config.GenericConfig.LoopbackClientConfig)
   156  	if err != nil {
   157  		return fmt.Errorf("failed to create apiserver proxy: %v", err)
   158  	}
   159  
   160  	webRouter := mux.NewRouter()
   161  	webRouter.PathPrefix("/debug").Handler(http.DefaultServeMux) // for /debug/pprof
   162  	// the path prefix here must be kept in sync with the prefix configured in the proxy handler
   163  	// (it needs to know what to strip before forwarding the request)
   164  	webRouter.PathPrefix(apiServerProxyPrefix).Handler(proxyHandler)
   165  	webRouter.PathPrefix("/").Handler(s.hudServer.Router())
   166  
   167  	s.webServer = &http.Server{
   168  		Addr:    s.webListener.Addr().String(),
   169  		Handler: webRouter,
   170  
   171  		// blackhole any server errors
   172  		ErrorLog: log.New(io.Discard, "", 0),
   173  	}
   174  	runServer(ctx, s.webServer, s.webListener)
   175  
   176  	s.apiServer = &http.Server{
   177  		Addr:           serving.Listener.Addr().String(),
   178  		Handler:        apiRouter,
   179  		MaxHeaderBytes: 1 << 20,
   180  		TLSConfig:      apiTLSConfig,
   181  
   182  		// blackhole any server errors
   183  		ErrorLog: log.New(io.Discard, "", 0),
   184  	}
   185  	runServer(ctx, s.apiServer, serving.Listener)
   186  	server.GenericAPIServer.RunPostStartHooks(stopCh)
   187  
   188  	go func() {
   189  		err := s.assetServer.Serve(ctx)
   190  		if err != nil && ctx.Err() == nil {
   191  			st.Dispatch(store.NewErrorAction(err))
   192  		}
   193  	}()
   194  
   195  	return nil
   196  }
   197  
   198  // Write the API server configs into the user settings directory.
   199  //
   200  // Usually shows up as ~/.windmill/config or ~/.tilt-dev/config.
   201  func (s *HeadsUpServerController) addToAPIServerConfig() error {
   202  	if s.configAccess == nil {
   203  		return nil
   204  	}
   205  
   206  	var newConfig *clientcmdapi.Config
   207  	err := filelock.WithRLock(s.configAccess, func() error {
   208  		var e error
   209  		newConfig, e = s.configAccess.GetStartingConfig()
   210  		return e
   211  	})
   212  	if err != nil {
   213  		return err
   214  	}
   215  	newConfig = newConfig.DeepCopy()
   216  
   217  	clientConfig := s.apiServerConfig.GenericConfig.LoopbackClientConfig
   218  	if err := model.ValidateAPIServerName(s.apiServerName); err != nil {
   219  		return err
   220  	}
   221  
   222  	name := string(s.apiServerName)
   223  	newConfig.Contexts[name] = &clientcmdapi.Context{
   224  		Cluster:  name,
   225  		AuthInfo: name,
   226  	}
   227  	newConfig.AuthInfos[name] = &clientcmdapi.AuthInfo{
   228  		Token: clientConfig.BearerToken,
   229  	}
   230  
   231  	newConfig.Clusters[name] = &clientcmdapi.Cluster{
   232  		Server:                   clientConfig.Host,
   233  		CertificateAuthorityData: clientConfig.TLSClientConfig.CAData,
   234  	}
   235  
   236  	return s.modifyConfig(*newConfig)
   237  }
   238  
   239  // Remove this API server's configs into the user settings directory.
   240  //
   241  // Usually shows up as ~/.windmill/config or ~/.tilt-dev/config.
   242  func (s *HeadsUpServerController) removeFromAPIServerConfig() error {
   243  	if s.configAccess == nil {
   244  		return nil
   245  	}
   246  
   247  	var newConfig *clientcmdapi.Config
   248  	err := filelock.WithRLock(s.configAccess, func() error {
   249  		var e error
   250  		newConfig, e = s.configAccess.GetStartingConfig()
   251  		return e
   252  	})
   253  	if err != nil {
   254  		return err
   255  	}
   256  	newConfig = newConfig.DeepCopy()
   257  	if err := model.ValidateAPIServerName(s.apiServerName); err != nil {
   258  		return err
   259  	}
   260  
   261  	name := string(s.apiServerName)
   262  	delete(newConfig.Contexts, name)
   263  	delete(newConfig.AuthInfos, name)
   264  	delete(newConfig.Clusters, name)
   265  
   266  	return s.modifyConfig(*newConfig)
   267  }
   268  
   269  func (s *HeadsUpServerController) modifyConfig(config clientcmdapi.Config) error {
   270  	return filelock.WithLock(s.configAccess, func() error {
   271  		return clientcmd.ModifyConfig(s.configAccess, config, true)
   272  	})
   273  }
   274  
   275  func newAPIServerProxyHandler(config *rest.Config) (http.Handler, error) {
   276  	// all requests to the proxy handler are same origin from the HUD server, so there is
   277  	// no CORS policy in place because we explicitly want to reject all other origin requests
   278  	// in the future, it's worth considering adding a CSRF token for a bit of extra robustness;
   279  	// but it's not critical because the content returned by the proxy for GETs is not embeddable
   280  	// and for POST cannot accept form data (only JSON or protobuf), so XHR same origin policy
   281  	// is sufficient
   282  	fs := &proxy.FilterServer{
   283  		AcceptHosts: []*regexp.Regexp{
   284  			// filtering by Host header is not useful, just ignore it
   285  			regexp.MustCompile(`.+`),
   286  		},
   287  		AcceptPaths: []*regexp.Regexp{
   288  			regexp.MustCompile(`^/apis/tilt\.dev/\w+/uibuttons`),
   289  		},
   290  	}
   291  
   292  	// the prefix here must be kept in sync with the route definition on the mux
   293  	return proxy.NewProxyHandler(apiServerProxyPrefix, fs, config, 0, false)
   294  }
   295  
   296  var _ store.SetUpper = &HeadsUpServerController{}
   297  var _ store.TearDowner = &HeadsUpServerController{}