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

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"k8s.io/client-go/dynamic"
    11  	"k8s.io/client-go/rest"
    12  	"k8s.io/client-go/tools/clientcmd"
    13  
    14  	"github.com/tilt-dev/wmclient/pkg/dirs"
    15  
    16  	"github.com/tilt-dev/tilt-apiserver/pkg/server/apiserver"
    17  	"github.com/tilt-dev/tilt-apiserver/pkg/server/builder"
    18  	"github.com/tilt-dev/tilt-apiserver/pkg/server/options"
    19  	"github.com/tilt-dev/tilt-apiserver/pkg/server/testdata"
    20  	"github.com/tilt-dev/tilt/internal/xdg"
    21  	"github.com/tilt-dev/tilt/pkg/logger"
    22  	"github.com/tilt-dev/tilt/pkg/model"
    23  
    24  	"github.com/akutz/memconn"
    25  
    26  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    27  	"github.com/tilt-dev/tilt/pkg/openapi"
    28  )
    29  
    30  // https://twitter.com/ow/status/1356978380198072321
    31  //
    32  // By default, the API server request limit is 3MB.  Certain Helm Charts with
    33  // CRDs have bigger payloads than this, so we bumped it to 20MB.
    34  //
    35  // (Some custom API servers set it to 100MB, see
    36  // https://github.com/kubernetes/kubernetes/pull/73805)
    37  //
    38  // This doesn't mean large 20MB payloads are fine. Iteratively applying a 20MB
    39  // payload over and over will slow down the overall system, simply on copying
    40  // and encoding/decoding costs alone.
    41  //
    42  // The underlying apiserver libraries have the ability to set this limit on a
    43  // per-resource level (rather than a per-server level). If that's ever exposed,
    44  // we should adjust this limit to be higher for KubernetesApply and lower for
    45  // other resource types.
    46  //
    47  // It also might make sense to help the user break up large payloads.  For
    48  // example, we could automatically split large CRDs into their own resources.
    49  const maxRequestBodyBytes = int64(20 * 1024 * 1024)
    50  
    51  type WebListener net.Listener
    52  type APIServerPort int
    53  
    54  type APIServerConfig = apiserver.Config
    55  
    56  type DynamicInterface = dynamic.Interface
    57  
    58  func ProvideDefaultConnProvider() apiserver.ConnProvider {
    59  	return nil
    60  }
    61  
    62  func ProvideMemConn() apiserver.ConnProvider {
    63  	return apiserver.NetworkConnProvider(&memconn.Provider{}, "memu")
    64  }
    65  
    66  func ProvideKeyCert(apiServerName model.APIServerName, host model.WebHost, port model.WebPort, base xdg.Base) (options.GeneratableKeyCert, error) {
    67  	pairName := strings.ReplaceAll(fmt.Sprintf("%s_%d", host, port), string(filepath.Separator), "_")
    68  	exampleCert, err := base.CacheFile(filepath.Join("certs", string(apiServerName), pairName))
    69  	if err != nil {
    70  		return options.GeneratableKeyCert{}, err
    71  	}
    72  
    73  	return options.GeneratableKeyCert{CertDirectory: filepath.Dir(exampleCert), PairName: pairName}, nil
    74  }
    75  
    76  // Uses the kubernetes config-loading library to load
    77  // api configs from disk.
    78  //
    79  // Usually loads from ~/.windmill/config or ~/tilt-dev/config.
    80  //
    81  // Also allows overriding with the TILT_CONFIG env variable, like
    82  // TILT_CONFIG=./path/to/my/config
    83  // which is useful when testing CLIs.
    84  func ProvideConfigAccess(dir *dirs.TiltDevDir) clientcmd.ConfigAccess {
    85  	ret := &clientcmd.PathOptions{
    86  		GlobalFile:        filepath.Join(dir.Root(), "config"),
    87  		GlobalFileSubpath: filepath.Join(filepath.Dir(dir.Root()), "config"),
    88  		EnvVar:            "TILT_CONFIG",
    89  		LoadingRules:      clientcmd.NewDefaultClientConfigLoadingRules(),
    90  	}
    91  	ret.LoadingRules.DoNotResolvePaths = true
    92  	return ret
    93  }
    94  
    95  // Creates a listener for the plain http web server.
    96  func ProvideWebListener(host model.WebHost, port model.WebPort) (WebListener, error) {
    97  	webListener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", string(host), int(port)))
    98  	if err != nil {
    99  		if strings.HasSuffix(err.Error(), "address already in use") {
   100  			return nil, fmt.Errorf("Tilt cannot start because you already have another process on port %d\n"+
   101  				"If you want to run multiple Tilt instances simultaneously,\n"+
   102  				"use the --port flag or TILT_PORT env variable to set a custom port\nOriginal error: %v",
   103  				port, err)
   104  		}
   105  		return nil, err
   106  	}
   107  	return WebListener(webListener), nil
   108  }
   109  
   110  // Picks a random port for the APIServer.
   111  //
   112  // TODO(nick): In the future, we should be able to have the apiserver listen
   113  // on other network interfaces, not just loopback. But then we would have to
   114  // also setup the KeyCert to identify the server.
   115  func ProvideAPIServerPort() (APIServerPort, error) {
   116  	addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
   117  	if err != nil {
   118  		return 0, err
   119  	}
   120  
   121  	l, err := net.ListenTCP("tcp", addr)
   122  	if err != nil {
   123  		return 0, err
   124  	}
   125  	defer l.Close()
   126  	return APIServerPort(l.Addr().(*net.TCPAddr).Port), nil
   127  }
   128  
   129  // Configures the Tilt API server.
   130  func ProvideTiltServerOptions(
   131  	ctx context.Context,
   132  	tiltBuild model.TiltBuild,
   133  	connProvider apiserver.ConnProvider,
   134  	token BearerToken,
   135  	certKey options.GeneratableKeyCert,
   136  	apiPort APIServerPort) (*APIServerConfig, error) {
   137  	w := logger.Get(ctx).Writer(logger.DebugLvl)
   138  	builder := builder.NewServerBuilder().
   139  		WithOutputWriter(w).
   140  		WithBearerToken(string(token)).
   141  		WithCertKey(certKey)
   142  
   143  	for _, obj := range v1alpha1.AllResourceObjects() {
   144  		builder = builder.WithResourceMemoryStorage(obj, "data")
   145  	}
   146  	builder = builder.WithOpenAPIDefinitions("tilt", tiltBuild.Version, openapi.GetOpenAPIDefinitions)
   147  
   148  	if connProvider != nil {
   149  		builder = builder.WithConnProvider(connProvider)
   150  	}
   151  
   152  	if apiPort != 0 {
   153  		builder = builder.WithBindPort(int(apiPort))
   154  	}
   155  
   156  	o, err := builder.ToServerOptions()
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	if apiPort == 0 {
   162  		// Fake bind port
   163  		o.ServingOptions.BindPort = 1
   164  	}
   165  
   166  	err = o.Complete()
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	err = o.Validate(nil)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	config, err := o.Config()
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	// Shout-out to kubectl-tree woop woop.
   181  	// https://github.com/ahmetb/kubectl-tree/blob/3561e74922d29f576698a820b4c003f1dcf691be/cmd/kubectl-tree/rootcmd.go#L75
   182  	config.GenericConfig.LoopbackClientConfig.QPS = 1000
   183  	config.GenericConfig.LoopbackClientConfig.Burst = 1000
   184  
   185  	config.GenericConfig.MaxRequestBodyBytes = maxRequestBodyBytes
   186  	return config, nil
   187  }
   188  
   189  // Generate the server config, removing options that are not needed for testing.
   190  //
   191  // 1) Changes http -> https
   192  // 2) Skips OpenAPI installation
   193  func ProvideTiltServerOptionsForTesting(ctx context.Context) (*APIServerConfig, error) {
   194  	config, err := ProvideTiltServerOptions(ctx,
   195  		model.TiltBuild{}, ProvideMemConn(), "corgi-charge", testdata.CertKey(), 0)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	config.GenericConfig.Config.SkipOpenAPIInstallation = true
   201  	config.GenericConfig.LoopbackClientConfig.TLSClientConfig = rest.TLSClientConfig{}
   202  	config.GenericConfig.LoopbackClientConfig.Host =
   203  		strings.Replace(config.GenericConfig.LoopbackClientConfig.Host, "https://", "http://", 1)
   204  	config.ExtraConfig.ServingInfo.Cert = nil
   205  
   206  	return config, nil
   207  }
   208  
   209  // Generate the server config, removing options that are not needed for headless mode
   210  // (where we don't open up any webserver or apiserver).
   211  func ProvideTiltServerOptionsForHeadless(ctx context.Context, keyCert options.GeneratableKeyCert, memconn apiserver.ConnProvider, version model.TiltBuild) (*APIServerConfig, error) {
   212  	config, err := ProvideTiltServerOptions(ctx,
   213  		version, memconn, "corgi-charge", keyCert, 0)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	config.GenericConfig.LoopbackClientConfig.TLSClientConfig = rest.TLSClientConfig{}
   219  	config.GenericConfig.LoopbackClientConfig.Host =
   220  		strings.Replace(config.GenericConfig.LoopbackClientConfig.Host, "https://", "http://", 1)
   221  	config.ExtraConfig.ServingInfo.Cert = nil
   222  
   223  	return config, nil
   224  }
   225  
   226  // Provide a dynamic API client for the Tilt server.
   227  func ProvideTiltDynamic(config *APIServerConfig) (DynamicInterface, error) {
   228  	return dynamic.NewForConfig(config.GenericConfig.LoopbackClientConfig)
   229  }