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

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  	"strconv"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"k8s.io/client-go/tools/clientcmd"
    19  
    20  	"github.com/tilt-dev/tilt-apiserver/pkg/server/apiserver"
    21  
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  	"k8s.io/apimachinery/pkg/api/meta"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  
    28  	"github.com/tilt-dev/wmclient/pkg/dirs"
    29  
    30  	"github.com/tilt-dev/tilt-apiserver/pkg/server/testdata"
    31  	"github.com/tilt-dev/tilt/internal/k8s/testyaml"
    32  	"github.com/tilt-dev/tilt/internal/store"
    33  	"github.com/tilt-dev/tilt/internal/testutils"
    34  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    35  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    36  	"github.com/tilt-dev/tilt/pkg/assets"
    37  	"github.com/tilt-dev/tilt/pkg/model"
    38  )
    39  
    40  // Ensure creating objects works with the dynamic API clients.
    41  func TestAPIServerDynamicClient(t *testing.T) {
    42  	f := newAPIServerFixture(t)
    43  	f.start()
    44  
    45  	specs := map[string]interface{}{
    46  		"FileWatch": map[string]interface{}{
    47  			// this needs to include a valid absolute path for the current GOOS
    48  			"watchedPaths": []string{mustCwd(t)},
    49  		},
    50  		"Session": map[string]interface{}{
    51  			"tiltfilePath":  filepath.Join(mustCwd(t), "Tiltfile"),
    52  			"exitCondition": "manual",
    53  		},
    54  		"KubernetesDiscovery": map[string]interface{}{
    55  			"watches": []map[string]interface{}{
    56  				{"namespace": "my-namespace", "uid": "my-uid"},
    57  			},
    58  		},
    59  		"KubernetesApply": map[string]interface{}{
    60  			"yaml": testyaml.SanchoYAML,
    61  		},
    62  		"ImageMap": map[string]interface{}{
    63  			"selector": "busybox",
    64  		},
    65  		"UIButton": map[string]interface{}{
    66  			"text": "I'm a button!",
    67  			"location": map[string]interface{}{
    68  				"componentType": "Resource",
    69  				"componentID":   "my-resource",
    70  			},
    71  		},
    72  		"PortForward": map[string]interface{}{
    73  			"podName": "my-pod",
    74  			"forwards": []interface{}{
    75  				map[string]interface{}{
    76  					"localPort":     8080,
    77  					"containerPort": 8000,
    78  				},
    79  			},
    80  		},
    81  		"ExtensionRepo": map[string]interface{}{
    82  			"url": "https://github.com/tilt-dev/tilt-extensions",
    83  		},
    84  		"LiveUpdate": map[string]interface{}{
    85  			"syncs": []interface{}{
    86  				map[string]interface{}{
    87  					"localPath":     "./src",
    88  					"containerPath": "/app/src",
    89  				},
    90  			},
    91  		},
    92  		"ToggleButton": map[string]interface{}{
    93  			"stateSource": map[string]interface{}{
    94  				"configMap": map[string]interface{}{
    95  					"name":     "foo",
    96  					"key":      "bar",
    97  					"onValue":  "on",
    98  					"offValue": "off",
    99  				},
   100  			},
   101  		},
   102  	}
   103  
   104  	for _, obj := range v1alpha1.AllResourceObjects() {
   105  		typeName := reflect.TypeOf(obj).Elem().Name()
   106  		t.Run(typeName, func(t *testing.T) {
   107  			objName := fmt.Sprintf("dynamic-%s", strings.ToLower(typeName))
   108  			unstructured := &unstructured.Unstructured{
   109  				Object: map[string]interface{}{
   110  					"kind":       typeName,
   111  					"apiVersion": v1alpha1.SchemeGroupVersion.String(),
   112  					"metadata": map[string]interface{}{
   113  						"name": objName,
   114  						"annotations": map[string]string{
   115  							"my-random-key": "my-random-value",
   116  						},
   117  					},
   118  					"spec": specs[typeName],
   119  				},
   120  			}
   121  
   122  			objClient := f.dynamic.Resource(obj.GetGroupVersionResource())
   123  			_, err := objClient.Create(f.ctx, unstructured, metav1.CreateOptions{})
   124  			require.NoError(t, err)
   125  
   126  			newObj, err := objClient.Get(f.ctx, objName, metav1.GetOptions{})
   127  			require.NoError(t, err)
   128  
   129  			metadata, err := meta.Accessor(newObj)
   130  			require.NoError(t, err)
   131  
   132  			assert.Equal(t, objName, metadata.GetName())
   133  			assert.Equal(t, "my-random-value", metadata.GetAnnotations()["my-random-key"])
   134  		})
   135  	}
   136  }
   137  
   138  func TestAPIServerProxy(t *testing.T) {
   139  	f := newAPIServerFixture(t)
   140  	f.start()
   141  
   142  	reqURL := fmt.Sprintf("http://%s/proxy/apis/tilt.dev/v1alpha1/uibuttons", f.webListener.Addr())
   143  	req, err := http.NewRequestWithContext(f.ctx, http.MethodGet, reqURL, nil)
   144  	require.NoError(t, err, "Failed to create request")
   145  
   146  	resp, err := http.DefaultClient.Do(req)
   147  	require.NoError(t, err, "Request failed")
   148  	require.Equal(t, http.StatusOK, resp.StatusCode)
   149  
   150  	body, err := io.ReadAll(resp.Body)
   151  	require.NoError(t, err, "Failed to read response body")
   152  	// don't care about the full body of the response, but it should at least have
   153  	// "kind": "UIButtonList" so look for that as a magic word
   154  	require.Contains(t, string(body), "UIButtonList")
   155  }
   156  
   157  func mustCwd(t testing.TB) string {
   158  	t.Helper()
   159  	cwd, err := os.Getwd()
   160  	require.NoError(t, err, "Could not get current working directory")
   161  	return cwd
   162  }
   163  
   164  type apiserverFixture struct {
   165  	*tempdir.TempDirFixture
   166  	t               testing.TB
   167  	ctx             context.Context
   168  	conn            apiserver.ConnProvider
   169  	serverConfig    *APIServerConfig
   170  	configAccess    clientcmd.ConfigAccess
   171  	webListener     WebListener
   172  	webListenerHost string
   173  	webListenerPort int
   174  	webURL          model.WebURL
   175  	st              *store.TestingStore
   176  	dynamic         DynamicInterface
   177  }
   178  
   179  func newAPIServerFixture(t testing.TB) *apiserverFixture {
   180  	t.Helper()
   181  
   182  	tmpdir := tempdir.NewTempDirFixture(t)
   183  
   184  	dir := dirs.NewTiltDevDirAt(tmpdir.Path())
   185  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
   186  	// since these tests issue network requests, ensure that they don't get stuck perpetually
   187  	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
   188  	t.Cleanup(cancel)
   189  
   190  	memconn := ProvideMemConn()
   191  
   192  	cfg, err := ProvideTiltServerOptions(ctx, model.TiltBuild{}, memconn, "corgi-charge", testdata.CertKey(), 0)
   193  	require.NoError(t, err)
   194  
   195  	const host = "localhost"
   196  	webListener, err := ProvideWebListener(host, 0)
   197  	require.NoError(t, err)
   198  	t.Cleanup(func() {
   199  		_ = webListener.Close()
   200  	})
   201  
   202  	webListenerHost, port, err := net.SplitHostPort(webListener.Addr().String())
   203  	require.NoErrorf(t, err, "Invalid listener address: %s", webListener.Addr().String())
   204  	webListenerPort, err := strconv.Atoi(port)
   205  	require.NoErrorf(t, err, "Invalid listener port: %s", port)
   206  	webURL, err := url.Parse(fmt.Sprintf("http://%s:%s/", host, port))
   207  	require.NoError(t, err, "Unable to create WebURL")
   208  
   209  	configAccess := ProvideConfigAccess(dir)
   210  
   211  	// Dynamic type tests
   212  	dynamic, err := ProvideTiltDynamic(cfg)
   213  	require.NoError(t, err)
   214  
   215  	f := &apiserverFixture{
   216  		TempDirFixture:  tmpdir,
   217  		t:               t,
   218  		ctx:             ctx,
   219  		conn:            memconn,
   220  		serverConfig:    cfg,
   221  		configAccess:    configAccess,
   222  		webListener:     webListener,
   223  		webListenerHost: webListenerHost,
   224  		webListenerPort: webListenerPort,
   225  		webURL:          model.WebURL(*webURL),
   226  		st:              store.NewTestingStore(),
   227  		dynamic:         dynamic,
   228  	}
   229  	return f
   230  }
   231  
   232  func (f *apiserverFixture) start() *HeadsUpServerController {
   233  	f.t.Helper()
   234  	hudsc := ProvideHeadsUpServerController(f.configAccess, "tilt-default",
   235  		f.webListener, f.serverConfig, &HeadsUpServer{}, assets.NewFakeServer(), f.webURL)
   236  	require.NoError(f.t, hudsc.SetUp(f.ctx, f.st))
   237  	f.t.Cleanup(func() {
   238  		hudsc.TearDown(f.ctx)
   239  	})
   240  	return hudsc
   241  }