porting all github actions from bluesky-social/indigo to tangled CI

support booting PDS on a random port in testing (#193)

This patch makes PDS boot on a random port in the integration tests.
It's a first step towards #165 ("Integration tests need randomly
assigned ports") and request for feedback on its strategy before I apply
it to BDS.

To do this work, we have to teach `pds.Server` how to boot with an
existing net.Listener and teach its Echo instance to do the same. That
required using `Echo#StartServer` instead of `Echo#Start`. Using
`Echo#StartServer` seems to be the accepted pattern for booting from an
existing Listener in Echo and both it and `Start` call the same
`Echo#configureServer` under the hood.

PDS isn't a production service right now, but I believe I've reproduced
the
configuration that echo would have done that there wouldn't be
production
impact. However, if BGS is, we could talk about what feature flagging
we'd
like to prevent problems when this is applied there.

Along the way, we refactor the uses of the `TestPDS.host` field into
more explicit methods on `TestPDS`. I'm not completely sold on those
methods' names, but they seem handy.

Updates #165

authored by Whyrusleeping and committed by GitHub a3118b4f 57a9f2e3

+28 -2
pds/server.go
··· 5 5 "encoding/json" 6 6 "errors" 7 7 "fmt" 8 + "net" 9 + "net/http" 8 10 "net/mail" 9 11 "net/url" 10 12 "strings" ··· 58 60 59 61 const UserActorDeclCid = "bafyreid27zk7lbis4zw5fz4podbvbs4fc5ivwji3dmrwa6zggnj4bnd57u" 60 62 const UserActorDeclType = "app.bsky.system.actorUser" 63 + 64 + // serverListenerBootTimeout is how long to wait for the requested server socket 65 + // to become available for use. This is an arbitrary timeout that should be safe 66 + // on any platform, but there's no great way to weave this timeout without 67 + // adding another parameter to the (at time of writing) long signature of 68 + // NewServer. 69 + const serverListenerBootTimeout = 5 * time.Second 61 70 62 71 func NewServer(db *gorm.DB, cs *carstore.CarStore, serkey *did.PrivKey, handleSuffix, serviceUrl string, didr plc.PLCClient, jwtkey []byte) (*Server, error) { 63 72 db.AutoMigrate(&User{}) ··· 265 274 return lexutil.CborDecodeValue(blk.RawData()) 266 275 } 267 276 268 - func (s *Server) RunAPI(listen string) error { 277 + func (s *Server) RunAPI(addr string) error { 278 + var lc net.ListenConfig 279 + ctx, cancel := context.WithTimeout(context.Background(), serverListenerBootTimeout) 280 + defer cancel() 281 + 282 + li, err := lc.Listen(ctx, "tcp", addr) 283 + if err != nil { 284 + return err 285 + } 286 + return s.RunAPIWithListener(li) 287 + } 288 + 289 + func (s *Server) RunAPIWithListener(listen net.Listener) error { 269 290 e := echo.New() 270 291 s.echo = e 271 292 e.HideBanner = true ··· 330 351 e.GET("/xrpc/com.atproto.sync.subscribeRepos", s.EventsHandler) 331 352 e.GET("/xrpc/_health", s.HandleHealthCheck) 332 353 333 - return e.Start(listen) 354 + // In order to support booting on random ports in tests, we need to tell the 355 + // Echo instance it's already got a port, and then use its StartServer 356 + // method to re-use that listener. 357 + e.Listener = listen 358 + srv := &http.Server{} 359 + return e.StartServer(srv) 334 360 } 335 361 336 362 type HealthStatus struct {
+8 -8
testing/integ_test.go
··· 26 26 } 27 27 assert := assert.New(t) 28 28 didr := TestPLC(t) 29 - p1 := MustSetupPDS(t, "localhost:5155", ".tpds", didr) 29 + p1 := MustSetupPDS(t, ".tpds", didr) 30 30 p1.Run(t) 31 31 32 32 b1 := MustSetupBGS(t, "localhost:8231", didr) ··· 119 119 assert := assert.New(t) 120 120 _ = assert 121 121 didr := TestPLC(t) 122 - p1 := MustSetupPDS(t, "localhost:5185", ".pdsuno", didr) 122 + p1 := MustSetupPDS(t, ".pdsuno", didr) 123 123 p1.Run(t) 124 124 125 - p2 := MustSetupPDS(t, "localhost:5186", ".pdsdos", didr) 125 + p2 := MustSetupPDS(t, ".pdsdos", didr) 126 126 p2.Run(t) 127 127 128 128 b1 := MustSetupBGS(t, "localhost:8281", didr) ··· 183 183 assert := assert.New(t) 184 184 _ = assert 185 185 didr := TestPLC(t) 186 - p1 := MustSetupPDS(t, "localhost:5195", ".pdsuno", didr) 186 + p1 := MustSetupPDS(t, ".pdsuno", didr) 187 187 p1.Run(t) 188 188 189 - p2 := MustSetupPDS(t, "localhost:5196", ".pdsdos", didr) 189 + p2 := MustSetupPDS(t, ".pdsdos", didr) 190 190 p2.Run(t) 191 191 192 192 b1 := MustSetupBGS(t, "localhost:8291", didr) ··· 240 240 assert := assert.New(t) 241 241 _ = assert 242 242 didr := TestPLC(t) 243 - p1 := MustSetupPDS(t, "localhost:5385", ".pdsuno", didr) 243 + p1 := MustSetupPDS(t, ".pdsuno", didr) 244 244 p1.Run(t) 245 245 246 246 b1 := MustSetupBGS(t, "localhost:8391", didr) ··· 273 273 _ = assert 274 274 275 275 didr := TestPLC(t) 276 - p1 := MustSetupPDS(t, "localhost:5151", ".tpds", didr) 276 + p1 := MustSetupPDS(t, ".tpds", didr) 277 277 p1.Run(t) 278 278 279 279 b1 := MustSetupBGS(t, "localhost:3231", didr) ··· 324 324 } 325 325 assert := assert.New(t) 326 326 didr := TestPLC(t) 327 - p1 := MustSetupPDS(t, "localhost:9155", ".tpds", didr) 327 + p1 := MustSetupPDS(t, ".tpds", didr) 328 328 p1.Run(t) 329 329 330 330 b1 := MustSetupBGS(t, "localhost:1531", didr)
+1 -1
testing/labelmaker_fakedata_test.go
··· 129 129 _ = assert 130 130 ctx := context.TODO() 131 131 didr := TestPLC(t) 132 - p1 := MustSetupPDS(t, "localhost:5115", ".tpds", didr) 132 + p1 := MustSetupPDS(t, ".tpds", didr) 133 133 p1.Run(t) 134 134 135 135 b1 := MustSetupBGS(t, "localhost:8322", didr)
+5 -6
testing/pds_fakedata_test.go
··· 12 12 ) 13 13 14 14 const ( 15 - pdsHost = "http://localhost:5159" 16 15 adminPassword = "admin" 17 16 celebCount = 2 18 17 regularCount = 5 ··· 60 59 } 61 60 assert := assert.New(t) 62 61 plcc := TestPLC(t) 63 - pds := MustSetupPDS(t, "localhost:5159", ".test", plcc) 62 + pds := MustSetupPDS(t, ".test", plcc) 64 63 pds.Run(t) 65 64 66 65 time.Sleep(time.Millisecond * 50) 67 66 68 - catalog := genTestCatalog(t, pdsHost) 67 + catalog := genTestCatalog(t, pds.HTTPHost()) 69 68 combined := catalog.Combined() 70 69 testClient := util.TestingHTTPClient() 71 70 72 71 // generate profile, graph, posts 73 72 for _, acc := range combined { 74 - xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 73 + xrpcc, err := fakedata.AccountXrpcClient(pds.HTTPHost(), &acc) 75 74 xrpcc.Client = testClient 76 75 if err != nil { 77 76 t.Fatal(err) ··· 84 83 85 84 // generate interactions (additional posts, etc) 86 85 for _, acc := range combined { 87 - xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 86 + xrpcc, err := fakedata.AccountXrpcClient(pds.HTTPHost(), &acc) 88 87 xrpcc.Client = testClient 89 88 if err != nil { 90 89 t.Fatal(err) ··· 95 94 96 95 // do browsing (read-only) 97 96 for _, acc := range combined { 98 - xrpcc, err := fakedata.AccountXrpcClient(pdsHost, &acc) 97 + xrpcc, err := fakedata.AccountXrpcClient(pds.HTTPHost(), &acc) 99 98 xrpcc.Client = testClient 100 99 if err != nil { 101 100 t.Fatal(err)
+34 -11
testing/utils.go
··· 8 8 "encoding/base32" 9 9 "fmt" 10 10 mathrand "math/rand" 11 + "net" 11 12 "net/http" 12 13 "os" 13 14 "path/filepath" ··· 35 36 "github.com/multiformats/go-multihash" 36 37 "github.com/whyrusleeping/go-did" 37 38 39 + "net/url" 40 + 38 41 "github.com/gorilla/websocket" 39 42 "gorm.io/driver/sqlite" 40 43 "gorm.io/gorm" ··· 45 48 server *pds.Server 46 49 plc *api.PLCServer 47 50 48 - host string 51 + listener net.Listener 49 52 50 53 shutdown func() 51 54 } 52 55 56 + // HTTPHost returns a host:port string that the PDS server is running at 57 + func (tp *TestPDS) RawHost() string { 58 + return tp.listener.Addr().String() 59 + } 60 + 61 + // HTTPHost returns a URL string that the PDS server is running at with the 62 + // scheme set for HTTP 63 + func (tp *TestPDS) HTTPHost() string { 64 + u := url.URL{Scheme: "http", Host: tp.listener.Addr().String()} 65 + return u.String() 66 + } 67 + 53 68 func (tp *TestPDS) Cleanup() { 54 69 if tp.shutdown != nil { 55 70 tp.shutdown() ··· 60 75 } 61 76 } 62 77 63 - func MustSetupPDS(t *testing.T, host, suffix string, plc plc.PLCClient) *TestPDS { 78 + func MustSetupPDS(t *testing.T, suffix string, plc plc.PLCClient) *TestPDS { 64 79 t.Helper() 65 - 66 - tpds, err := SetupPDS(host, suffix, plc) 80 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 81 + defer cancel() 82 + tpds, err := SetupPDS(ctx, suffix, plc) 67 83 if err != nil { 68 84 t.Fatal(err) 69 85 } ··· 71 87 return tpds 72 88 } 73 89 74 - func SetupPDS(host, suffix string, plc plc.PLCClient) (*TestPDS, error) { 90 + func SetupPDS(ctx context.Context, suffix string, plc plc.PLCClient) (*TestPDS, error) { 75 91 dir, err := os.MkdirTemp("", "integtest") 76 92 if err != nil { 77 93 return nil, err ··· 111 127 Type: did.KeyTypeP256, 112 128 } 113 129 130 + var lc net.ListenConfig 131 + li, err := lc.Listen(ctx, "tcp", "localhost:0") 132 + if err != nil { 133 + return nil, err 134 + } 135 + 136 + host := li.Addr().String() 114 137 srv, err := pds.NewServer(maindb, cs, serkey, suffix, host, plc, []byte(host+suffix)) 115 138 if err != nil { 116 139 return nil, err 117 140 } 118 141 119 142 return &TestPDS{ 120 - dir: dir, 121 - server: srv, 122 - host: host, 143 + dir: dir, 144 + server: srv, 145 + listener: li, 123 146 }, nil 124 147 } 125 148 126 149 func (tp *TestPDS) Run(t *testing.T) { 127 150 // TODO: rig this up so it t.Fatals if the RunAPI call fails immediately 128 151 go func() { 129 - if err := tp.server.RunAPI(tp.host); err != nil { 152 + if err := tp.server.RunAPIWithListener(tp.listener); err != nil { 130 153 fmt.Println(err) 131 154 } 132 155 }() ··· 141 164 t.Helper() 142 165 143 166 c := &xrpc.Client{Host: "http://" + b.host} 144 - if err := atproto.SyncRequestCrawl(context.TODO(), c, tp.host); err != nil { 167 + if err := atproto.SyncRequestCrawl(context.TODO(), c, tp.RawHost()); err != nil { 145 168 t.Fatal(err) 146 169 } 147 170 } ··· 169 192 ctx := context.TODO() 170 193 171 194 c := &xrpc.Client{ 172 - Host: "http://" + tp.host, 195 + Host: tp.HTTPHost(), 173 196 } 174 197 175 198 fmt.Println("HOST: ", c.Host)