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{}