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