github.com/tilt-dev/tilt@v0.36.0/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  	config := s.apiServerConfig
   115  	server, err := config.Complete().New()
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	err = server.GenericAPIServer.AddPostStartHook("start-tilt-server-informers", func(context genericapiserver.PostStartHookContext) error {
   121  		if config.GenericConfig.SharedInformerFactory != nil {
   122  			config.GenericConfig.SharedInformerFactory.Start(context.Context.Done())
   123  		}
   124  		return nil
   125  	})
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	prepared := server.GenericAPIServer.PrepareRun()
   131  	apiserverHandler := prepared.Handler
   132  	serving := config.ExtraConfig.ServingInfo
   133  
   134  	apiRouter := mux.NewRouter()
   135  	apiRouter.Path("/api").Handler(http.NotFoundHandler())
   136  	apiRouter.PathPrefix("/apis").Handler(apiserverHandler)
   137  	apiRouter.PathPrefix("/healthz").Handler(apiserverHandler)
   138  	apiRouter.PathPrefix("/livez").Handler(apiserverHandler)
   139  	apiRouter.PathPrefix("/metrics").Handler(apiserverHandler)
   140  	apiRouter.PathPrefix("/openapi").Handler(apiserverHandler)
   141  	apiRouter.PathPrefix("/readyz").Handler(apiserverHandler)
   142  	apiRouter.PathPrefix("/swagger").Handler(apiserverHandler)
   143  	apiRouter.PathPrefix("/version").Handler(apiserverHandler)
   144  	apiRouter.PathPrefix("/debug").Handler(http.DefaultServeMux) // for /debug/pprof
   145  
   146  	var apiTLSConfig *tls.Config
   147  	if serving.Cert != nil {
   148  		apiTLSConfig, err = start.TLSConfig(ctx, serving)
   149  		if err != nil {
   150  			return fmt.Errorf("Starting apiserver: %v", err)
   151  		}
   152  	}
   153  
   154  	proxyHandler, err := newAPIServerProxyHandler(config.GenericConfig.LoopbackClientConfig)
   155  	if err != nil {
   156  		return fmt.Errorf("failed to create apiserver proxy: %v", err)
   157  	}
   158  
   159  	webRouter := mux.NewRouter()
   160  	webRouter.PathPrefix("/debug").Handler(http.DefaultServeMux) // for /debug/pprof
   161  	// the path prefix here must be kept in sync with the prefix configured in the proxy handler
   162  	// (it needs to know what to strip before forwarding the request)
   163  	webRouter.PathPrefix(apiServerProxyPrefix).Handler(proxyHandler)
   164  	webRouter.PathPrefix("/").Handler(s.hudServer.Router())
   165  
   166  	s.webServer = &http.Server{
   167  		Addr:    s.webListener.Addr().String(),
   168  		Handler: webRouter,
   169  
   170  		// blackhole any server errors
   171  		ErrorLog: log.New(io.Discard, "", 0),
   172  	}
   173  	runServer(ctx, s.webServer, s.webListener)
   174  
   175  	s.apiServer = &http.Server{
   176  		Addr:           serving.Listener.Addr().String(),
   177  		Handler:        apiRouter,
   178  		MaxHeaderBytes: 1 << 20,
   179  		TLSConfig:      apiTLSConfig,
   180  
   181  		// blackhole any server errors
   182  		ErrorLog: log.New(io.Discard, "", 0),
   183  	}
   184  	runServer(ctx, s.apiServer, serving.Listener)
   185  	server.GenericAPIServer.RunPostStartHooks(ctx)
   186  
   187  	go func() {
   188  		err := s.assetServer.Serve(ctx)
   189  		if err != nil && ctx.Err() == nil {
   190  			st.Dispatch(store.NewErrorAction(err))
   191  		}
   192  	}()
   193  
   194  	return nil
   195  }
   196  
   197  // Write the API server configs into the user settings directory.
   198  //
   199  // Usually shows up as ~/.windmill/config or ~/.tilt-dev/config.
   200  func (s *HeadsUpServerController) addToAPIServerConfig() error {
   201  	if s.configAccess == nil {
   202  		return nil
   203  	}
   204  
   205  	var newConfig *clientcmdapi.Config
   206  	err := filelock.WithRLock(s.configAccess, func() error {
   207  		var e error
   208  		newConfig, e = s.configAccess.GetStartingConfig()
   209  		return e
   210  	})
   211  	if err != nil {
   212  		return err
   213  	}
   214  	newConfig = newConfig.DeepCopy()
   215  
   216  	clientConfig := s.apiServerConfig.GenericConfig.LoopbackClientConfig
   217  	if err := model.ValidateAPIServerName(s.apiServerName); err != nil {
   218  		return err
   219  	}
   220  
   221  	name := string(s.apiServerName)
   222  	newConfig.Contexts[name] = &clientcmdapi.Context{
   223  		Cluster:  name,
   224  		AuthInfo: name,
   225  	}
   226  	newConfig.AuthInfos[name] = &clientcmdapi.AuthInfo{
   227  		Token: clientConfig.BearerToken,
   228  	}
   229  
   230  	newConfig.Clusters[name] = &clientcmdapi.Cluster{
   231  		Server:                   clientConfig.Host,
   232  		CertificateAuthorityData: clientConfig.TLSClientConfig.CAData,
   233  	}
   234  
   235  	return s.modifyConfig(*newConfig)
   236  }
   237  
   238  // Remove this API server's configs into the user settings directory.
   239  //
   240  // Usually shows up as ~/.windmill/config or ~/.tilt-dev/config.
   241  func (s *HeadsUpServerController) removeFromAPIServerConfig() error {
   242  	if s.configAccess == nil {
   243  		return nil
   244  	}
   245  
   246  	var newConfig *clientcmdapi.Config
   247  	err := filelock.WithRLock(s.configAccess, func() error {
   248  		var e error
   249  		newConfig, e = s.configAccess.GetStartingConfig()
   250  		return e
   251  	})
   252  	if err != nil {
   253  		return err
   254  	}
   255  	newConfig = newConfig.DeepCopy()
   256  	if err := model.ValidateAPIServerName(s.apiServerName); err != nil {
   257  		return err
   258  	}
   259  
   260  	name := string(s.apiServerName)
   261  	delete(newConfig.Contexts, name)
   262  	delete(newConfig.AuthInfos, name)
   263  	delete(newConfig.Clusters, name)
   264  
   265  	return s.modifyConfig(*newConfig)
   266  }
   267  
   268  func (s *HeadsUpServerController) modifyConfig(config clientcmdapi.Config) error {
   269  	return filelock.WithLock(s.configAccess, func() error {
   270  		return clientcmd.ModifyConfig(s.configAccess, config, true)
   271  	})
   272  }
   273  
   274  func newAPIServerProxyHandler(config *rest.Config) (http.Handler, error) {
   275  	// all requests to the proxy handler are same origin from the HUD server, so there is
   276  	// no CORS policy in place because we explicitly want to reject all other origin requests
   277  	// in the future, it's worth considering adding a CSRF token for a bit of extra robustness;
   278  	// but it's not critical because the content returned by the proxy for GETs is not embeddable
   279  	// and for POST cannot accept form data (only JSON or protobuf), so XHR same origin policy
   280  	// is sufficient
   281  	fs := &proxy.FilterServer{
   282  		AcceptHosts: []*regexp.Regexp{
   283  			// filtering by Host header is not useful, just ignore it
   284  			regexp.MustCompile(`.+`),
   285  		},
   286  		AcceptPaths: []*regexp.Regexp{
   287  			regexp.MustCompile(`^/apis/tilt\.dev/\w+/uibuttons`),
   288  		},
   289  	}
   290  
   291  	// the prefix here must be kept in sync with the route definition on the mux
   292  	return proxy.NewProxyHandler(apiServerProxyPrefix, fs, config, 0, false)
   293  }
   294  
   295  var _ store.SetUpper = &HeadsUpServerController{}
   296  var _ store.TearDowner = &HeadsUpServerController{}