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 }