github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/cmd/spicedb/serve_integration_test.go (about) 1 //go:build docker && image 2 // +build docker,image 3 4 package main 5 6 import ( 7 "context" 8 "fmt" 9 "strings" 10 "testing" 11 "time" 12 13 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 14 "github.com/authzed/grpcutil" 15 "github.com/google/uuid" 16 "github.com/ory/dockertest/v3" 17 "github.com/ory/dockertest/v3/docker" 18 "github.com/stretchr/testify/require" 19 "google.golang.org/grpc" 20 "google.golang.org/grpc/codes" 21 "google.golang.org/grpc/credentials/insecure" 22 healthpb "google.golang.org/grpc/health/grpc_health_v1" 23 "google.golang.org/grpc/status" 24 25 testdatastore "github.com/authzed/spicedb/internal/testserver/datastore" 26 "github.com/authzed/spicedb/pkg/datastore" 27 ) 28 29 func TestServe(t *testing.T) { 30 requireParent := require.New(t) 31 32 tester, err := newTester(t, 33 &dockertest.RunOptions{ 34 Repository: "authzed/spicedb", 35 Tag: "ci", 36 Cmd: []string{"serve", "--log-level", "debug", "--grpc-preshared-key", "firstkey", "--grpc-preshared-key", "secondkey"}, 37 ExposedPorts: []string{"50051/tcp"}, 38 }, 39 "firstkey", 40 false, 41 ) 42 requireParent.NoError(err) 43 defer tester.cleanup() 44 45 for key, expectedWorks := range map[string]bool{ 46 "": false, 47 "firstkey": true, 48 "secondkey": true, 49 "anotherkey": false, 50 } { 51 key := key 52 t.Run(key, func(t *testing.T) { 53 require := require.New(t) 54 55 opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} 56 if key != "" { 57 opts = append(opts, grpcutil.WithInsecureBearerToken(key)) 58 } 59 conn, err := grpc.Dial(fmt.Sprintf("localhost:%s", tester.port), opts...) 60 61 require.NoError(err) 62 defer conn.Close() 63 64 require.Eventually(func() bool { 65 resp, err := healthpb.NewHealthClient(conn).Check(context.Background(), &healthpb.HealthCheckRequest{Service: "authzed.api.v1.SchemaService"}) 66 if err != nil || resp.GetStatus() != healthpb.HealthCheckResponse_SERVING { 67 return false 68 } 69 70 return true 71 }, 5*time.Second, 1*time.Millisecond, "was unable to connect to running service") 72 73 client := v1.NewSchemaServiceClient(conn) 74 _, err = client.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ 75 Schema: `definition user {}`, 76 }) 77 78 if expectedWorks { 79 require.NoError(err) 80 } else { 81 s, ok := status.FromError(err) 82 require.True(ok) 83 84 if key == "" { 85 require.Equal(codes.Unauthenticated, s.Code()) 86 } else { 87 require.Equal(codes.PermissionDenied, s.Code()) 88 } 89 } 90 }) 91 } 92 } 93 94 func gracefulShutdown(pool *dockertest.Pool, serveResource *dockertest.Resource) bool { 95 closed := make(chan bool, 1) 96 go func() { 97 // Send SIGSTOP to have the container gracefully shutdown. 98 pool.Client.KillContainer(docker.KillContainerOptions{ 99 ID: serveResource.Container.ID, 100 Signal: docker.SIGSTOP, 101 Context: context.Background(), 102 }) 103 closed <- true 104 }() 105 106 select { 107 case <-closed: 108 return true 109 110 case <-time.After(10 * time.Second): 111 _ = pool.Purge(serveResource) 112 return false 113 } 114 } 115 116 func TestGracefulShutdownInMemory(t *testing.T) { 117 pool, err := dockertest.NewPool("") 118 require.NoError(t, err) 119 120 // Run a serve and immediately close, ensuring it shuts down gracefully. 121 serveResource, err := pool.RunWithOptions(&dockertest.RunOptions{ 122 Repository: "authzed/spicedb", 123 Tag: "ci", 124 Cmd: []string{"serve", "--grpc-preshared-key", "firstkey"}, 125 }, func(config *docker.HostConfig) { 126 config.RestartPolicy = docker.RestartPolicy{ 127 Name: "no", 128 } 129 }) 130 require.NoError(t, err) 131 132 require.True(t, gracefulShutdown(pool, serveResource)) 133 } 134 135 type watchingWriter struct { 136 c chan bool 137 expectedString string 138 } 139 140 func (ww *watchingWriter) Write(p []byte) (n int, err error) { 141 if strings.Contains(string(p), ww.expectedString) { 142 ww.c <- true 143 } 144 145 return len(p), nil 146 } 147 148 func TestGracefulShutdown(t *testing.T) { 149 engines := map[string]bool{ 150 "postgres": true, 151 "mysql": true, 152 "cockroachdb": false, 153 "spanner": false, 154 } 155 require.Equal(t, len(engines), len(datastore.Engines)) 156 157 for driverName, awaitGC := range engines { 158 t.Run(driverName, func(t *testing.T) { 159 bridgeNetworkName := fmt.Sprintf("bridge-%s", uuid.New().String()) 160 161 pool, err := dockertest.NewPool("") 162 require.NoError(t, err) 163 164 // Create a bridge network for testing. 165 network, err := pool.Client.CreateNetwork(docker.CreateNetworkOptions{ 166 Name: bridgeNetworkName, 167 }) 168 require.NoError(t, err) 169 t.Cleanup(func() { 170 pool.Client.RemoveNetwork(network.ID) 171 }) 172 173 engine := testdatastore.RunDatastoreEngineWithBridge(t, driverName, bridgeNetworkName) 174 175 envVars := []string{} 176 if wev, ok := engine.(testdatastore.RunningEngineForTestWithEnvVars); ok { 177 envVars = wev.ExternalEnvVars() 178 } 179 180 // Run the migrate command and wait for it to complete. 181 db := engine.NewDatabase(t) 182 migrateResource, err := pool.RunWithOptions(&dockertest.RunOptions{ 183 Repository: "authzed/spicedb", 184 Tag: "ci", 185 Cmd: []string{"migrate", "head", "--datastore-engine", driverName, "--datastore-conn-uri", db}, 186 NetworkID: bridgeNetworkName, 187 Env: envVars, 188 }, func(config *docker.HostConfig) { 189 config.RestartPolicy = docker.RestartPolicy{ 190 Name: "no", 191 } 192 }) 193 require.NoError(t, err) 194 195 waitCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 196 defer cancel() 197 198 // Ensure the command completed successfully. 199 status, err := pool.Client.WaitContainerWithContext(migrateResource.Container.ID, waitCtx) 200 require.NoError(t, err) 201 require.Equal(t, 0, status) 202 203 // Run a serve and immediately close, ensuring it shuts down gracefully. 204 serveResource, err := pool.RunWithOptions(&dockertest.RunOptions{ 205 Repository: "authzed/spicedb", 206 Tag: "ci", 207 Cmd: []string{"serve", "--grpc-preshared-key", "firstkey", "--datastore-engine", driverName, "--datastore-conn-uri", db, "--datastore-gc-interval", "1s", "--telemetry-endpoint", ""}, 208 NetworkID: bridgeNetworkName, 209 Env: envVars, 210 }, func(config *docker.HostConfig) { 211 config.RestartPolicy = docker.RestartPolicy{ 212 Name: "no", 213 } 214 }) 215 require.NoError(t, err) 216 t.Cleanup(func() { 217 _ = pool.Purge(serveResource) 218 }) 219 220 if awaitGC { 221 ww := &watchingWriter{make(chan bool, 1), "running garbage collection worker"} 222 223 // Grab logs and ensure GC has run before starting a graceful shutdown. 224 opts := docker.LogsOptions{ 225 Context: context.Background(), 226 Stderr: true, 227 Stdout: true, 228 Follow: true, 229 Timestamps: true, 230 RawTerminal: true, 231 Container: serveResource.Container.ID, 232 OutputStream: ww, 233 } 234 235 go (func() { 236 err = pool.Client.Logs(opts) 237 require.NoError(t, err) 238 })() 239 240 select { 241 case <-ww.c: 242 break 243 244 case <-time.After(10 * time.Second): 245 require.Fail(t, "timed out waiting for GC to run") 246 } 247 } 248 249 require.True(t, gracefulShutdown(pool, serveResource)) 250 }) 251 } 252 }