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  }