get.pme.sh/pnats@v0.0.0-20240304004023-26bb5a137ed0/server/jetstream_leafnode_test.go (about) 1 // Copyright 2020-2022 The NATS Authors 2 // Licensed under the Apache License, Version 2.0 (the "License"); 3 // you may not use this file except in compliance with the License. 4 // You may obtain a copy of the License at 5 // 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 //go:build !skip_js_tests 15 // +build !skip_js_tests 16 17 package server 18 19 import ( 20 "fmt" 21 "os" 22 "strings" 23 "testing" 24 "time" 25 26 jwt "github.com/nats-io/jwt/v2" 27 "github.com/nats-io/nats.go" 28 "github.com/nats-io/nkeys" 29 ) 30 31 func TestJetStreamLeafNodeUniqueServerNameCrossJSDomain(t *testing.T) { 32 name := "NOT-UNIQUE" 33 test := func(s *Server, sIdExpected string, srvs ...*Server) { 34 ids := map[string]string{} 35 for _, srv := range srvs { 36 checkLeafNodeConnectedCount(t, srv, 2) 37 ids[srv.ID()] = srv.opts.JetStreamDomain 38 } 39 // ensure that an update for every server was received 40 sysNc := natsConnect(t, fmt.Sprintf("nats://admin:s3cr3t!@127.0.0.1:%d", s.opts.Port)) 41 defer sysNc.Close() 42 sub, err := sysNc.SubscribeSync(fmt.Sprintf(serverStatsSubj, "*")) 43 require_NoError(t, err) 44 for { 45 m, err := sub.NextMsg(time.Second) 46 require_NoError(t, err) 47 tk := strings.Split(m.Subject, ".") 48 if domain, ok := ids[tk[2]]; ok { 49 delete(ids, tk[2]) 50 require_Contains(t, string(m.Data), fmt.Sprintf(`"domain":"%s"`, domain)) 51 } 52 if len(ids) == 0 { 53 break 54 } 55 } 56 cnt := 0 57 s.nodeToInfo.Range(func(key, value interface{}) bool { 58 cnt++ 59 require_Equal(t, value.(nodeInfo).name, name) 60 require_Equal(t, value.(nodeInfo).id, sIdExpected) 61 return true 62 }) 63 require_True(t, cnt == 1) 64 } 65 tmplA := ` 66 listen: -1 67 server_name: %s 68 jetstream { 69 max_mem_store: 256MB, 70 max_file_store: 2GB, 71 store_dir: '%s', 72 domain: hub 73 } 74 accounts { 75 JSY { users = [ { user: "y", pass: "p" } ]; jetstream: true } 76 $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } 77 } 78 leaf { 79 port: -1 80 } 81 ` 82 tmplL := ` 83 listen: -1 84 server_name: %s 85 jetstream { 86 max_mem_store: 256MB, 87 max_file_store: 2GB, 88 store_dir: '%s', 89 domain: %s 90 } 91 accounts { 92 JSY { users = [ { user: "y", pass: "p" } ]; jetstream: true } 93 $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } 94 } 95 leaf { 96 remotes [ 97 { urls: [ %s ], account: "JSY" } 98 { urls: [ %s ], account: "$SYS" } 99 ] 100 } 101 ` 102 t.Run("same-domain", func(t *testing.T) { 103 confA := createConfFile(t, []byte(fmt.Sprintf(tmplA, name, t.TempDir()))) 104 sA, oA := RunServerWithConfig(confA) 105 defer sA.Shutdown() 106 // using same domain as sA 107 confL := createConfFile(t, []byte(fmt.Sprintf(tmplL, name, t.TempDir(), "hub", 108 fmt.Sprintf("nats://y:p@127.0.0.1:%d", oA.LeafNode.Port), 109 fmt.Sprintf("nats://admin:s3cr3t!@127.0.0.1:%d", oA.LeafNode.Port)))) 110 sL, _ := RunServerWithConfig(confL) 111 defer sL.Shutdown() 112 // as server name uniqueness is violates, sL.ID() is the expected value 113 test(sA, sL.ID(), sA, sL) 114 }) 115 t.Run("different-domain", func(t *testing.T) { 116 confA := createConfFile(t, []byte(fmt.Sprintf(tmplA, name, t.TempDir()))) 117 sA, oA := RunServerWithConfig(confA) 118 defer sA.Shutdown() 119 // using different domain as sA 120 confL := createConfFile(t, []byte(fmt.Sprintf(tmplL, name, t.TempDir(), "spoke", 121 fmt.Sprintf("nats://y:p@127.0.0.1:%d", oA.LeafNode.Port), 122 fmt.Sprintf("nats://admin:s3cr3t!@127.0.0.1:%d", oA.LeafNode.Port)))) 123 sL, _ := RunServerWithConfig(confL) 124 defer sL.Shutdown() 125 checkLeafNodeConnectedCount(t, sL, 2) 126 checkLeafNodeConnectedCount(t, sA, 2) 127 // ensure sA contains only sA.ID 128 test(sA, sA.ID(), sA, sL) 129 }) 130 } 131 132 func TestJetStreamLeafNodeJwtPermsAndJSDomains(t *testing.T) { 133 createAcc := func(js bool) (string, string, nkeys.KeyPair) { 134 kp, _ := nkeys.CreateAccount() 135 aPub, _ := kp.PublicKey() 136 claim := jwt.NewAccountClaims(aPub) 137 if js { 138 claim.Limits.JetStreamLimits = jwt.JetStreamLimits{ 139 MemoryStorage: 1024 * 1024, 140 DiskStorage: 1024 * 1024, 141 Streams: 1, Consumer: 2} 142 } 143 aJwt, err := claim.Encode(oKp) 144 require_NoError(t, err) 145 return aPub, aJwt, kp 146 } 147 sysPub, sysJwt, sysKp := createAcc(false) 148 accPub, accJwt, accKp := createAcc(true) 149 noExpiration := time.Now().Add(time.Hour) 150 // create user for acc to be used in leaf node. 151 lnCreds := createUserWithLimit(t, accKp, noExpiration, func(j *jwt.UserPermissionLimits) { 152 j.Sub.Deny.Add("subdeny") 153 j.Pub.Deny.Add("pubdeny") 154 }) 155 unlimitedCreds := createUserWithLimit(t, accKp, noExpiration, nil) 156 157 sysCreds := createUserWithLimit(t, sysKp, noExpiration, nil) 158 159 tmplA := ` 160 operator: %s 161 system_account: %s 162 resolver: MEMORY 163 resolver_preload: { 164 %s: %s 165 %s: %s 166 } 167 listen: 127.0.0.1:-1 168 leafnodes: { 169 listen: 127.0.0.1:-1 170 } 171 jetstream :{ 172 domain: "cluster" 173 store_dir: '%s' 174 max_mem: 100Mb 175 max_file: 100Mb 176 } 177 ` 178 179 tmplL := ` 180 listen: 127.0.0.1:-1 181 accounts :{ 182 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 183 SYS:{ users:[ {user:s1,password:s1}]}, 184 } 185 system_account = SYS 186 jetstream: { 187 domain: ln1 188 store_dir: '%s' 189 max_mem: 50Mb 190 max_file: 50Mb 191 } 192 leafnodes:{ 193 remotes:[{ url:nats://127.0.0.1:%d, account: A, credentials: '%s'}, 194 { url:nats://127.0.0.1:%d, account: SYS, credentials: '%s'}] 195 } 196 ` 197 198 confA := createConfFile(t, []byte(fmt.Sprintf(tmplA, ojwt, sysPub, 199 sysPub, sysJwt, accPub, accJwt, 200 t.TempDir()))) 201 sA, _ := RunServerWithConfig(confA) 202 defer sA.Shutdown() 203 204 confL := createConfFile(t, []byte(fmt.Sprintf(tmplL, t.TempDir(), 205 sA.opts.LeafNode.Port, lnCreds, sA.opts.LeafNode.Port, sysCreds))) 206 sL, _ := RunServerWithConfig(confL) 207 defer sL.Shutdown() 208 209 checkLeafNodeConnectedCount(t, sA, 2) 210 checkLeafNodeConnectedCount(t, sL, 2) 211 212 ncA := natsConnect(t, sA.ClientURL(), nats.UserCredentials(unlimitedCreds)) 213 defer ncA.Close() 214 215 ncL := natsConnect(t, fmt.Sprintf("nats://a1:a1@127.0.0.1:%d", sL.opts.Port)) 216 defer ncL.Close() 217 218 test := func(subject string, cSub, cPub *nats.Conn, remoteServerForSub *Server, accName string, pass bool) { 219 t.Helper() 220 sub, err := cSub.SubscribeSync(subject) 221 require_NoError(t, err) 222 require_NoError(t, cSub.Flush()) 223 // ensure the subscription made it across, or if not sent due to sub deny, make sure it could have made it. 224 if remoteServerForSub == nil { 225 time.Sleep(200 * time.Millisecond) 226 } else { 227 checkSubInterest(t, remoteServerForSub, accName, subject, time.Second) 228 } 229 require_NoError(t, cPub.Publish(subject, []byte("hello world"))) 230 require_NoError(t, cPub.Flush()) 231 m, err := sub.NextMsg(500 * time.Millisecond) 232 if pass { 233 require_NoError(t, err) 234 require_True(t, m.Subject == subject) 235 require_Equal(t, string(m.Data), "hello world") 236 } else { 237 require_True(t, err == nats.ErrTimeout) 238 } 239 } 240 241 t.Run("sub-on-ln-pass", func(t *testing.T) { 242 test("sub", ncL, ncA, sA, accPub, true) 243 }) 244 t.Run("sub-on-ln-fail", func(t *testing.T) { 245 test("subdeny", ncL, ncA, nil, "", false) 246 }) 247 t.Run("pub-on-ln-pass", func(t *testing.T) { 248 test("pub", ncA, ncL, sL, "A", true) 249 }) 250 t.Run("pub-on-ln-fail", func(t *testing.T) { 251 test("pubdeny", ncA, ncL, nil, "A", false) 252 }) 253 } 254 255 func TestJetStreamLeafNodeClusterExtensionWithSystemAccount(t *testing.T) { 256 /* 257 Topologies tested here 258 same == true 259 A <-> B 260 ^ |\ 261 | \ 262 | proxy 263 | \ 264 LA <-> LB 265 266 same == false 267 A <-> B 268 ^ ^ 269 | | 270 | proxy 271 | | 272 LA <-> LB 273 274 The proxy is turned on later, such that the system account connection can be started later, in a controlled way 275 This explicitly tests the system state before and after this happens. 276 */ 277 278 tmplA := ` 279 listen: 127.0.0.1:-1 280 accounts :{ 281 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 282 SYS:{ users:[ {user:s1,password:s1}]}, 283 } 284 system_account: SYS 285 leafnodes: { 286 listen: 127.0.0.1:-1 287 no_advertise: true 288 authorization: { 289 timeout: 0.5 290 } 291 } 292 jetstream :{ 293 domain: "cluster" 294 store_dir: '%s' 295 max_mem: 100Mb 296 max_file: 100Mb 297 } 298 server_name: A 299 cluster: { 300 name: clust1 301 listen: 127.0.0.1:20104 302 routes=[nats-route://127.0.0.1:20105] 303 no_advertise: true 304 } 305 ` 306 307 tmplB := ` 308 listen: 127.0.0.1:-1 309 accounts :{ 310 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 311 SYS:{ users:[ {user:s1,password:s1}]}, 312 } 313 system_account: SYS 314 leafnodes: { 315 listen: 127.0.0.1:-1 316 no_advertise: true 317 authorization: { 318 timeout: 0.5 319 } 320 } 321 jetstream: { 322 domain: "cluster" 323 store_dir: '%s' 324 max_mem: 100Mb 325 max_file: 100Mb 326 } 327 server_name: B 328 cluster: { 329 name: clust1 330 listen: 127.0.0.1:20105 331 routes=[nats-route://127.0.0.1:20104] 332 no_advertise: true 333 } 334 ` 335 336 tmplLA := ` 337 listen: 127.0.0.1:-1 338 accounts :{ 339 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 340 SYS:{ users:[ {user:s1,password:s1}]}, 341 } 342 system_account = SYS 343 jetstream: { 344 domain: "cluster" 345 store_dir: '%s' 346 max_mem: 50Mb 347 max_file: 50Mb 348 %s 349 } 350 server_name: LA 351 cluster: { 352 name: clustL 353 listen: 127.0.0.1:20106 354 routes=[nats-route://127.0.0.1:20107] 355 no_advertise: true 356 } 357 leafnodes:{ 358 no_advertise: true 359 remotes:[{url:nats://a1:a1@127.0.0.1:%d, account: A}, 360 {url:nats://s1:s1@127.0.0.1:%d, account: SYS}] 361 } 362 ` 363 364 tmplLB := ` 365 listen: 127.0.0.1:-1 366 accounts :{ 367 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 368 SYS:{ users:[ {user:s1,password:s1}]}, 369 } 370 system_account = SYS 371 jetstream: { 372 domain: "cluster" 373 store_dir: '%s' 374 max_mem: 50Mb 375 max_file: 50Mb 376 %s 377 } 378 server_name: LB 379 cluster: { 380 name: clustL 381 listen: 127.0.0.1:20107 382 routes=[nats-route://127.0.0.1:20106] 383 no_advertise: true 384 } 385 leafnodes:{ 386 no_advertise: true 387 remotes:[{url:nats://a1:a1@127.0.0.1:%d, account: A}, 388 {url:nats://s1:s1@127.0.0.1:%d, account: SYS}] 389 } 390 ` 391 392 for _, testCase := range []struct { 393 // which topology to pick 394 same bool 395 // If leaf server should be operational and form a Js cluster prior to joining. 396 // In this setup this would be an error as you give the wrong hint. 397 // But this should work itself out regardless 398 leafFunctionPreJoin bool 399 }{ 400 {true, true}, 401 {true, false}, 402 {false, true}, 403 {false, false}} { 404 t.Run(fmt.Sprintf("%t-%t", testCase.same, testCase.leafFunctionPreJoin), func(t *testing.T) { 405 sd1 := t.TempDir() 406 confA := createConfFile(t, []byte(fmt.Sprintf(tmplA, sd1))) 407 sA, _ := RunServerWithConfig(confA) 408 defer sA.Shutdown() 409 410 sd2 := t.TempDir() 411 confB := createConfFile(t, []byte(fmt.Sprintf(tmplB, sd2))) 412 sB, _ := RunServerWithConfig(confB) 413 defer sB.Shutdown() 414 415 checkClusterFormed(t, sA, sB) 416 417 c := cluster{t: t, servers: []*Server{sA, sB}} 418 c.waitOnLeader() 419 420 // starting this will allow the second remote in tmplL to successfully connect. 421 port := sB.opts.LeafNode.Port 422 if testCase.same { 423 port = sA.opts.LeafNode.Port 424 } 425 p := &proxyAcceptDetectFailureLate{acceptPort: port} 426 defer p.close() 427 lPort := p.runEx(t, true) 428 429 hint := "" 430 if testCase.leafFunctionPreJoin { 431 hint = fmt.Sprintf("extension_hint: %s", strings.ToUpper(jsNoExtend)) 432 } 433 434 sd3 := t.TempDir() 435 // deliberately pick server sA and proxy 436 confLA := createConfFile(t, []byte(fmt.Sprintf(tmplLA, sd3, hint, sA.opts.LeafNode.Port, lPort))) 437 sLA, _ := RunServerWithConfig(confLA) 438 defer sLA.Shutdown() 439 440 sd4 := t.TempDir() 441 // deliberately pick server sA and proxy 442 confLB := createConfFile(t, []byte(fmt.Sprintf(tmplLB, sd4, hint, sA.opts.LeafNode.Port, lPort))) 443 sLB, _ := RunServerWithConfig(confLB) 444 defer sLB.Shutdown() 445 446 checkClusterFormed(t, sLA, sLB) 447 448 strmCfg := func(name, placementCluster string) *nats.StreamConfig { 449 if placementCluster == "" { 450 return &nats.StreamConfig{Name: name, Replicas: 1, Subjects: []string{name}} 451 } 452 return &nats.StreamConfig{Name: name, Replicas: 1, Subjects: []string{name}, 453 Placement: &nats.Placement{Cluster: placementCluster}} 454 } 455 // Only after the system account is fully connected can streams be placed anywhere. 456 testJSFunctions := func(pass bool) { 457 ncA := natsConnect(t, fmt.Sprintf("nats://a1:a1@127.0.0.1:%d", sA.opts.Port)) 458 defer ncA.Close() 459 jsA, err := ncA.JetStream() 460 require_NoError(t, err) 461 _, err = jsA.AddStream(strmCfg(fmt.Sprintf("fooA1-%t", pass), "")) 462 require_NoError(t, err) 463 _, err = jsA.AddStream(strmCfg(fmt.Sprintf("fooA2-%t", pass), "clust1")) 464 require_NoError(t, err) 465 _, err = jsA.AddStream(strmCfg(fmt.Sprintf("fooA3-%t", pass), "clustL")) 466 if pass { 467 require_NoError(t, err) 468 } else { 469 require_Error(t, err) 470 require_Contains(t, err.Error(), "no suitable peers for placement") 471 } 472 ncL := natsConnect(t, fmt.Sprintf("nats://a1:a1@127.0.0.1:%d", sLA.opts.Port)) 473 defer ncL.Close() 474 jsL, err := ncL.JetStream() 475 require_NoError(t, err) 476 _, err = jsL.AddStream(strmCfg(fmt.Sprintf("fooL1-%t", pass), "")) 477 require_NoError(t, err) 478 _, err = jsL.AddStream(strmCfg(fmt.Sprintf("fooL2-%t", pass), "clustL")) 479 require_NoError(t, err) 480 _, err = jsL.AddStream(strmCfg(fmt.Sprintf("fooL3-%t", pass), "clust1")) 481 if pass { 482 require_NoError(t, err) 483 } else { 484 require_Error(t, err) 485 require_Contains(t, err.Error(), "no suitable peers for placement") 486 } 487 } 488 clusterLnCnt := func(expected int) error { 489 cnt := 0 490 for _, s := range c.servers { 491 cnt += s.NumLeafNodes() 492 } 493 if cnt == expected { 494 return nil 495 } 496 return fmt.Errorf("not enought leaf node connections, got %d needed %d", cnt, expected) 497 } 498 499 // Even though there are two remotes defined in tmplL, only one will be able to connect. 500 checkFor(t, 10*time.Second, time.Second/4, func() error { return clusterLnCnt(2) }) 501 checkLeafNodeConnectedCount(t, sLA, 1) 502 checkLeafNodeConnectedCount(t, sLB, 1) 503 c.waitOnPeerCount(2) 504 505 if testCase.leafFunctionPreJoin { 506 cl := cluster{t: t, servers: []*Server{sLA, sLB}} 507 cl.waitOnLeader() 508 cl.waitOnPeerCount(2) 509 testJSFunctions(false) 510 } else { 511 // In cases where the leaf nodes have to wait for the system account to connect, 512 // JetStream should not be operational during that time 513 ncA := natsConnect(t, fmt.Sprintf("nats://a1:a1@127.0.0.1:%d", sLA.opts.Port)) 514 defer ncA.Close() 515 jsA, err := ncA.JetStream() 516 require_NoError(t, err) 517 _, err = jsA.AddStream(strmCfg("fail-false", "")) 518 require_Error(t, err) 519 } 520 // Starting the proxy will connect the system accounts. 521 // After they are connected the clusters are merged. 522 // Once this happened, all streams in test can be placed anywhere in the cluster. 523 // Before that only the cluster the client is connected to can be used for placement 524 p.start() 525 526 // Even though there are two remotes defined in tmplL, only one will be able to connect. 527 checkFor(t, 10*time.Second, time.Second/4, func() error { return clusterLnCnt(4) }) 528 checkLeafNodeConnectedCount(t, sLA, 2) 529 checkLeafNodeConnectedCount(t, sLB, 2) 530 531 // The leader will reside in the main cluster only 532 c.waitOnPeerCount(4) 533 testJSFunctions(true) 534 }) 535 } 536 } 537 538 func TestJetStreamLeafNodeClusterMixedModeExtensionWithSystemAccount(t *testing.T) { 539 /* Topology used in this test: 540 CLUSTER(A <-> B <-> C (NO JS)) 541 ^ 542 | 543 LA 544 */ 545 546 // once every server is up, we expect these peers to be part of the JetStream meta cluster 547 expectedJetStreamPeers := map[string]struct{}{ 548 "A": {}, 549 "B": {}, 550 "LA": {}, 551 } 552 553 tmplA := ` 554 listen: 127.0.0.1:-1 555 accounts :{ 556 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 557 SYS:{ users:[ {user:s1,password:s1}]}, 558 } 559 system_account: SYS 560 leafnodes: { 561 listen: 127.0.0.1:-1 562 no_advertise: true 563 authorization: { 564 timeout: 0.5 565 } 566 } 567 jetstream: { %s store_dir: '%s'; max_mem: 50Mb, max_file: 50Mb } 568 server_name: A 569 cluster: { 570 name: clust1 571 listen: 127.0.0.1:20114 572 routes=[nats-route://127.0.0.1:20115,nats-route://127.0.0.1:20116] 573 no_advertise: true 574 } 575 ` 576 577 tmplB := ` 578 listen: 127.0.0.1:-1 579 accounts :{ 580 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 581 SYS:{ users:[ {user:s1,password:s1}]}, 582 } 583 system_account: SYS 584 leafnodes: { 585 listen: 127.0.0.1:-1 586 no_advertise: true 587 authorization: { 588 timeout: 0.5 589 } 590 } 591 jetstream: { %s store_dir: '%s'; max_mem: 50Mb, max_file: 50Mb } 592 server_name: B 593 cluster: { 594 name: clust1 595 listen: 127.0.0.1:20115 596 routes=[nats-route://127.0.0.1:20114,nats-route://127.0.0.1:20116] 597 no_advertise: true 598 } 599 ` 600 601 tmplC := ` 602 listen: 127.0.0.1:-1 603 accounts :{ 604 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 605 SYS:{ users:[ {user:s1,password:s1}]}, 606 } 607 system_account: SYS 608 leafnodes: { 609 listen: 127.0.0.1:-1 610 no_advertise: true 611 authorization: { 612 timeout: 0.5 613 } 614 } 615 jetstream: { 616 enabled: false 617 %s 618 } 619 server_name: C 620 cluster: { 621 name: clust1 622 listen: 127.0.0.1:20116 623 routes=[nats-route://127.0.0.1:20114,nats-route://127.0.0.1:20115] 624 no_advertise: true 625 } 626 ` 627 628 tmplLA := ` 629 listen: 127.0.0.1:-1 630 accounts :{ 631 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 632 SYS:{ users:[ {user:s1,password:s1}]}, 633 } 634 system_account = SYS 635 # the extension hint is to simplify this test. without it present we would need a cluster of size 2 636 jetstream: { %s store_dir: '%s'; max_mem: 50Mb, max_file: 50Mb, extension_hint: will_extend } 637 server_name: LA 638 leafnodes:{ 639 no_advertise: true 640 remotes:[{url:nats://a1:a1@127.0.0.1:%d, account: A}, 641 {url:nats://s1:s1@127.0.0.1:%d, account: SYS}] 642 } 643 # add the cluster here so we can test placement 644 cluster: { name: clustL } 645 ` 646 for _, withDomain := range []bool{true, false} { 647 t.Run(fmt.Sprintf("with-domain:%t", withDomain), func(t *testing.T) { 648 var jsDisabledDomainString string 649 var jsEnabledDomainString string 650 if withDomain { 651 jsEnabledDomainString = `domain: "domain", ` 652 jsDisabledDomainString = `domain: "domain"` 653 } else { 654 // in case no domain name is set, fall back to the extension hint. 655 // since JS is disabled, the value of this does not clash with other uses. 656 jsDisabledDomainString = "extension_hint: will_extend" 657 } 658 659 sd1 := t.TempDir() 660 confA := createConfFile(t, []byte(fmt.Sprintf(tmplA, jsEnabledDomainString, sd1))) 661 sA, _ := RunServerWithConfig(confA) 662 defer sA.Shutdown() 663 664 sd2 := t.TempDir() 665 confB := createConfFile(t, []byte(fmt.Sprintf(tmplB, jsEnabledDomainString, sd2))) 666 sB, _ := RunServerWithConfig(confB) 667 defer sB.Shutdown() 668 669 confC := createConfFile(t, []byte(fmt.Sprintf(tmplC, jsDisabledDomainString))) 670 sC, _ := RunServerWithConfig(confC) 671 defer sC.Shutdown() 672 673 checkClusterFormed(t, sA, sB, sC) 674 c := cluster{t: t, servers: []*Server{sA, sB, sC}} 675 c.waitOnPeerCount(2) 676 677 sd3 := t.TempDir() 678 // deliberately pick server sC (no JS) to connect to 679 confLA := createConfFile(t, []byte(fmt.Sprintf(tmplLA, jsEnabledDomainString, sd3, sC.opts.LeafNode.Port, sC.opts.LeafNode.Port))) 680 sLA, _ := RunServerWithConfig(confLA) 681 defer sLA.Shutdown() 682 683 checkLeafNodeConnectedCount(t, sC, 2) 684 checkLeafNodeConnectedCount(t, sLA, 2) 685 c.waitOnPeerCount(3) 686 peers := c.leader().JetStreamClusterPeers() 687 for _, peer := range peers { 688 if _, ok := expectedJetStreamPeers[peer]; !ok { 689 t.Fatalf("Found unexpected peer %q", peer) 690 } 691 } 692 693 // helper to create stream config with uniqe name and subject 694 cnt := 0 695 strmCfg := func(placementCluster string) *nats.StreamConfig { 696 name := fmt.Sprintf("s-%d", cnt) 697 cnt++ 698 if placementCluster == "" { 699 return &nats.StreamConfig{Name: name, Replicas: 1, Subjects: []string{name}} 700 } 701 return &nats.StreamConfig{Name: name, Replicas: 1, Subjects: []string{name}, 702 Placement: &nats.Placement{Cluster: placementCluster}} 703 } 704 705 test := func(port int, expectedDefPlacement string) { 706 ncA := natsConnect(t, fmt.Sprintf("nats://a1:a1@127.0.0.1:%d", port)) 707 defer ncA.Close() 708 jsA, err := ncA.JetStream() 709 require_NoError(t, err) 710 si, err := jsA.AddStream(strmCfg("")) 711 require_NoError(t, err) 712 require_Contains(t, si.Cluster.Name, expectedDefPlacement) 713 si, err = jsA.AddStream(strmCfg("clust1")) 714 require_NoError(t, err) 715 require_Contains(t, si.Cluster.Name, "clust1") 716 si, err = jsA.AddStream(strmCfg("clustL")) 717 require_NoError(t, err) 718 require_Contains(t, si.Cluster.Name, "clustL") 719 } 720 721 test(sA.opts.Port, "clust1") 722 test(sB.opts.Port, "clust1") 723 test(sC.opts.Port, "clust1") 724 test(sLA.opts.Port, "clustL") 725 }) 726 } 727 } 728 729 func TestJetStreamLeafNodeCredsDenies(t *testing.T) { 730 tmplL := ` 731 listen: 127.0.0.1:-1 732 accounts :{ 733 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 734 SYS:{ users:[ {user:s1,password:s1}]}, 735 } 736 system_account = SYS 737 jetstream: { 738 domain: "cluster" 739 store_dir: '%s' 740 max_mem: 50Mb 741 max_file: 50Mb 742 } 743 leafnodes:{ 744 remotes:[{url:nats://a1:a1@127.0.0.1:20125, account: A, credentials: '%s' }, 745 {url:nats://s1:s1@127.0.0.1:20125, account: SYS, credentials: '%s', deny_imports: foo, deny_exports: bar}] 746 } 747 ` 748 akp, err := nkeys.CreateAccount() 749 require_NoError(t, err) 750 creds := createUserWithLimit(t, akp, time.Time{}, func(pl *jwt.UserPermissionLimits) { 751 pl.Pub.Deny.Add(jsAllAPI) 752 pl.Sub.Deny.Add(jsAllAPI) 753 }) 754 755 sd := t.TempDir() 756 757 confL := createConfFile(t, []byte(fmt.Sprintf(tmplL, sd, creds, creds))) 758 opts := LoadConfig(confL) 759 sL, err := NewServer(opts) 760 require_NoError(t, err) 761 762 l := captureNoticeLogger{} 763 sL.SetLogger(&l, false, false) 764 765 go sL.Start() 766 defer sL.Shutdown() 767 768 // wait till the notices got printed 769 UNTIL_READY: 770 for { 771 <-time.After(50 * time.Millisecond) 772 l.Lock() 773 for _, n := range l.notices { 774 if strings.Contains(n, "Server is ready") { 775 l.Unlock() 776 break UNTIL_READY 777 } 778 } 779 l.Unlock() 780 } 781 782 l.Lock() 783 cnt := 0 784 for _, n := range l.notices { 785 if strings.Contains(n, "LeafNode Remote for Account A uses credentials file") || 786 strings.Contains(n, "LeafNode Remote for System Account uses") || 787 strings.Contains(n, "Remote for System Account uses restricted export permissions") || 788 strings.Contains(n, "Remote for System Account uses restricted import permissions") { 789 cnt++ 790 } 791 } 792 l.Unlock() 793 require_True(t, cnt == 4) 794 } 795 796 func TestJetStreamLeafNodeDefaultDomainCfg(t *testing.T) { 797 tmplHub := ` 798 listen: 127.0.0.1:%d 799 accounts :{ 800 A:{ jetstream: %s, users:[ {user:a1,password:a1}]}, 801 SYS:{ users:[ {user:s1,password:s1}]}, 802 } 803 system_account: SYS 804 jetstream : %s 805 server_name: HUB 806 leafnodes: { 807 listen: 127.0.0.1:%d 808 } 809 %s 810 ` 811 812 tmplL := ` 813 listen: 127.0.0.1:-1 814 accounts :{ 815 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 816 SYS:{ users:[ {user:s1,password:s1}]}, 817 } 818 system_account: SYS 819 jetstream: { domain: "%s", store_dir: '%s', max_mem: 100Mb, max_file: 100Mb } 820 server_name: LEAF 821 leafnodes: { 822 remotes:[{url:nats://a1:a1@127.0.0.1:%d, account: A},%s] 823 } 824 %s 825 ` 826 827 test := func(domain string, sysShared bool) { 828 confHub := createConfFile(t, []byte(fmt.Sprintf(tmplHub, -1, "disabled", "disabled", -1, ""))) 829 sHub, _ := RunServerWithConfig(confHub) 830 defer sHub.Shutdown() 831 832 noDomainFix := "" 833 if domain == _EMPTY_ { 834 noDomainFix = `default_js_domain:{A:""}` 835 } 836 837 sys := "" 838 if sysShared { 839 sys = fmt.Sprintf(`{url:nats://s1:s1@127.0.0.1:%d, account: SYS}`, sHub.opts.LeafNode.Port) 840 } 841 842 sdLeaf := t.TempDir() 843 confL := createConfFile(t, []byte(fmt.Sprintf(tmplL, domain, sdLeaf, sHub.opts.LeafNode.Port, sys, noDomainFix))) 844 sLeaf, _ := RunServerWithConfig(confL) 845 defer sLeaf.Shutdown() 846 847 lnCnt := 1 848 if sysShared { 849 lnCnt++ 850 } 851 852 checkLeafNodeConnectedCount(t, sHub, lnCnt) 853 checkLeafNodeConnectedCount(t, sLeaf, lnCnt) 854 855 ncA := natsConnect(t, fmt.Sprintf("nats://a1:a1@127.0.0.1:%d", sHub.opts.Port)) 856 defer ncA.Close() 857 jsA, err := ncA.JetStream() 858 require_NoError(t, err) 859 860 _, err = jsA.AddStream(&nats.StreamConfig{Name: "foo", Replicas: 1, Subjects: []string{"foo"}}) 861 require_True(t, err == nats.ErrNoResponders) 862 863 // Add in default domain and restart server 864 require_NoError(t, os.WriteFile(confHub, []byte(fmt.Sprintf(tmplHub, 865 sHub.opts.Port, 866 "disabled", 867 "disabled", 868 sHub.opts.LeafNode.Port, 869 fmt.Sprintf(`default_js_domain: {A:"%s"}`, domain))), 0664)) 870 871 sHub.Shutdown() 872 sHub.WaitForShutdown() 873 checkLeafNodeConnectedCount(t, sLeaf, 0) 874 sHubUpd1, _ := RunServerWithConfig(confHub) 875 defer sHubUpd1.Shutdown() 876 877 checkLeafNodeConnectedCount(t, sHubUpd1, lnCnt) 878 checkLeafNodeConnectedCount(t, sLeaf, lnCnt) 879 880 _, err = jsA.AddStream(&nats.StreamConfig{Name: "foo", Replicas: 1, Subjects: []string{"foo"}}) 881 require_NoError(t, err) 882 883 // Enable jetstream in hub. 884 sdHub := t.TempDir() 885 jsEnabled := fmt.Sprintf(`{ domain: "%s", store_dir: '%s', max_mem: 100Mb, max_file: 100Mb }`, domain, sdHub) 886 require_NoError(t, os.WriteFile(confHub, []byte(fmt.Sprintf(tmplHub, 887 sHubUpd1.opts.Port, 888 "disabled", 889 jsEnabled, 890 sHubUpd1.opts.LeafNode.Port, 891 fmt.Sprintf(`default_js_domain: {A:"%s"}`, domain))), 0664)) 892 893 sHubUpd1.Shutdown() 894 sHubUpd1.WaitForShutdown() 895 checkLeafNodeConnectedCount(t, sLeaf, 0) 896 sHubUpd2, _ := RunServerWithConfig(confHub) 897 defer sHubUpd2.Shutdown() 898 899 checkLeafNodeConnectedCount(t, sHubUpd2, lnCnt) 900 checkLeafNodeConnectedCount(t, sLeaf, lnCnt) 901 902 _, err = jsA.AddStream(&nats.StreamConfig{Name: "bar", Replicas: 1, Subjects: []string{"bar"}}) 903 require_NoError(t, err) 904 905 // Enable jetstream in account A of hub 906 // This is a mis config, as you can't have it both ways, local jetstream but default to another one 907 require_NoError(t, os.WriteFile(confHub, []byte(fmt.Sprintf(tmplHub, 908 sHubUpd2.opts.Port, 909 "enabled", 910 jsEnabled, 911 sHubUpd2.opts.LeafNode.Port, 912 fmt.Sprintf(`default_js_domain: {A:"%s"}`, domain))), 0664)) 913 914 if domain != _EMPTY_ { 915 // in case no domain name exists there are no additional guard rails, hence no error 916 // It is the users responsibility to get this edge case right 917 sHubUpd2.Shutdown() 918 sHubUpd2.WaitForShutdown() 919 checkLeafNodeConnectedCount(t, sLeaf, 0) 920 sHubUpd3, err := NewServer(LoadConfig(confHub)) 921 sHubUpd3.Shutdown() 922 923 require_Error(t, err) 924 require_Contains(t, err.Error(), `default_js_domain contains account name "A" with enabled JetStream`) 925 } 926 } 927 928 t.Run("with-domain-sys", func(t *testing.T) { 929 test("domain", true) 930 }) 931 t.Run("with-domain-nosys", func(t *testing.T) { 932 test("domain", false) 933 }) 934 t.Run("no-domain", func(t *testing.T) { 935 test("", true) 936 }) 937 t.Run("no-domain", func(t *testing.T) { 938 test("", false) 939 }) 940 } 941 942 func TestJetStreamLeafNodeDefaultDomainJwtExplicit(t *testing.T) { 943 tmplHub := ` 944 listen: 127.0.0.1:%d 945 operator: %s 946 system_account: %s 947 resolver: MEM 948 resolver_preload: { 949 %s:%s 950 %s:%s 951 } 952 jetstream : disabled 953 server_name: HUB 954 leafnodes: { 955 listen: 127.0.0.1:%d 956 } 957 %s 958 ` 959 960 tmplL := ` 961 listen: 127.0.0.1:-1 962 accounts :{ 963 A:{ jetstream: enable, users:[ {user:a1,password:a1}]}, 964 SYS:{ users:[ {user:s1,password:s1}]}, 965 } 966 system_account: SYS 967 jetstream: { domain: "%s", store_dir: '%s', max_mem: 100Mb, max_file: 100Mb } 968 server_name: LEAF 969 leafnodes: { 970 remotes:[{url:nats://127.0.0.1:%d, account: A, credentials: '%s'}, 971 {url:nats://127.0.0.1:%d, account: SYS, credentials: '%s'}] 972 } 973 %s 974 ` 975 976 test := func(domain string) { 977 noDomainFix := "" 978 if domain == _EMPTY_ { 979 noDomainFix = `default_js_domain:{A:""}` 980 } 981 982 sysKp, syspub := createKey(t) 983 sysJwt := encodeClaim(t, jwt.NewAccountClaims(syspub), syspub) 984 sysCreds := newUser(t, sysKp) 985 986 aKp, aPub := createKey(t) 987 aClaim := jwt.NewAccountClaims(aPub) 988 aJwt := encodeClaim(t, aClaim, aPub) 989 aCreds := newUser(t, aKp) 990 991 confHub := createConfFile(t, []byte(fmt.Sprintf(tmplHub, -1, ojwt, syspub, syspub, sysJwt, aPub, aJwt, -1, ""))) 992 sHub, _ := RunServerWithConfig(confHub) 993 defer sHub.Shutdown() 994 995 sdLeaf := t.TempDir() 996 confL := createConfFile(t, []byte(fmt.Sprintf(tmplL, 997 domain, 998 sdLeaf, 999 sHub.opts.LeafNode.Port, 1000 aCreds, 1001 sHub.opts.LeafNode.Port, 1002 sysCreds, 1003 noDomainFix))) 1004 sLeaf, _ := RunServerWithConfig(confL) 1005 defer sLeaf.Shutdown() 1006 1007 checkLeafNodeConnectedCount(t, sHub, 2) 1008 checkLeafNodeConnectedCount(t, sLeaf, 2) 1009 1010 ncA := natsConnect(t, fmt.Sprintf("nats://127.0.0.1:%d", sHub.opts.Port), createUserCreds(t, nil, aKp)) 1011 defer ncA.Close() 1012 jsA, err := ncA.JetStream() 1013 require_NoError(t, err) 1014 1015 _, err = jsA.AddStream(&nats.StreamConfig{Name: "foo", Replicas: 1, Subjects: []string{"foo"}}) 1016 require_True(t, err == nats.ErrNoResponders) 1017 1018 // Add in default domain and restart server 1019 require_NoError(t, os.WriteFile(confHub, []byte(fmt.Sprintf(tmplHub, 1020 sHub.opts.Port, ojwt, syspub, syspub, sysJwt, aPub, aJwt, sHub.opts.LeafNode.Port, 1021 fmt.Sprintf(`default_js_domain: {%s:"%s"}`, aPub, domain))), 0664)) 1022 1023 sHub.Shutdown() 1024 sHub.WaitForShutdown() 1025 checkLeafNodeConnectedCount(t, sLeaf, 0) 1026 sHubUpd1, _ := RunServerWithConfig(confHub) 1027 defer sHubUpd1.Shutdown() 1028 1029 checkLeafNodeConnectedCount(t, sHubUpd1, 2) 1030 checkLeafNodeConnectedCount(t, sLeaf, 2) 1031 1032 _, err = jsA.AddStream(&nats.StreamConfig{Name: "bar", Replicas: 1, Subjects: []string{"bar"}}) 1033 require_NoError(t, err) 1034 } 1035 t.Run("with-domain", func(t *testing.T) { 1036 test("domain") 1037 }) 1038 t.Run("no-domain", func(t *testing.T) { 1039 test("") 1040 }) 1041 } 1042 1043 func TestJetStreamLeafNodeDefaultDomainClusterBothEnds(t *testing.T) { 1044 // test to ensure that default domain functions when both ends of the leaf node connection are clusters 1045 tmplHub1 := ` 1046 listen: 127.0.0.1:-1 1047 accounts :{ 1048 A:{ jetstream: enabled, users:[ {user:a1,password:a1}]}, 1049 B:{ jetstream: enabled, users:[ {user:b1,password:b1}]} 1050 } 1051 jetstream : { domain: "DHUB", store_dir: '%s', max_mem: 100Mb, max_file: 100Mb } 1052 server_name: HUB1 1053 cluster: { 1054 name: HUB 1055 listen: 127.0.0.1:20134 1056 routes=[nats-route://127.0.0.1:20135] 1057 } 1058 leafnodes: { 1059 listen:127.0.0.1:-1 1060 } 1061 ` 1062 1063 tmplHub2 := ` 1064 listen: 127.0.0.1:-1 1065 accounts :{ 1066 A:{ jetstream: enabled, users:[ {user:a1,password:a1}]}, 1067 B:{ jetstream: enabled, users:[ {user:b1,password:b1}]} 1068 } 1069 jetstream : { domain: "DHUB", store_dir: '%s', max_mem: 100Mb, max_file: 100Mb } 1070 server_name: HUB2 1071 cluster: { 1072 name: HUB 1073 listen: 127.0.0.1:20135 1074 routes=[nats-route://127.0.0.1:20134] 1075 } 1076 leafnodes: { 1077 listen:127.0.0.1:-1 1078 } 1079 ` 1080 1081 tmplL1 := ` 1082 listen: 127.0.0.1:-1 1083 accounts :{ 1084 A:{ jetstream: enabled, users:[ {user:a1,password:a1}]}, 1085 B:{ jetstream: disabled, users:[ {user:b1,password:b1}]} 1086 } 1087 jetstream: { domain: "DLEAF", store_dir: '%s', max_mem: 100Mb, max_file: 100Mb } 1088 server_name: LEAF1 1089 cluster: { 1090 name: LEAF 1091 listen: 127.0.0.1:20136 1092 routes=[nats-route://127.0.0.1:20137] 1093 } 1094 leafnodes: { 1095 remotes:[{url:nats://a1:a1@127.0.0.1:%d, account: A},{url:nats://b1:b1@127.0.0.1:%d, account: B}] 1096 } 1097 default_js_domain: {B:"DHUB"} 1098 ` 1099 1100 tmplL2 := ` 1101 listen: 127.0.0.1:-1 1102 accounts :{ 1103 A:{ jetstream: enabled, users:[ {user:a1,password:a1}]}, 1104 B:{ jetstream: disabled, users:[ {user:b1,password:b1}]} 1105 } 1106 jetstream: { domain: "DLEAF", store_dir: '%s', max_mem: 100Mb, max_file: 100Mb } 1107 server_name: LEAF2 1108 cluster: { 1109 name: LEAF 1110 listen: 127.0.0.1:20137 1111 routes=[nats-route://127.0.0.1:20136] 1112 } 1113 leafnodes: { 1114 remotes:[{url:nats://a1:a1@127.0.0.1:%d, account: A},{url:nats://b1:b1@127.0.0.1:%d, account: B}] 1115 } 1116 default_js_domain: {B:"DHUB"} 1117 ` 1118 1119 sd1 := t.TempDir() 1120 confHub1 := createConfFile(t, []byte(fmt.Sprintf(tmplHub1, sd1))) 1121 sHub1, _ := RunServerWithConfig(confHub1) 1122 defer sHub1.Shutdown() 1123 1124 sd2 := t.TempDir() 1125 confHub2 := createConfFile(t, []byte(fmt.Sprintf(tmplHub2, sd2))) 1126 sHub2, _ := RunServerWithConfig(confHub2) 1127 defer sHub2.Shutdown() 1128 1129 checkClusterFormed(t, sHub1, sHub2) 1130 c1 := cluster{t: t, servers: []*Server{sHub1, sHub2}} 1131 c1.waitOnPeerCount(2) 1132 1133 sd3 := t.TempDir() 1134 confLeaf1 := createConfFile(t, []byte(fmt.Sprintf(tmplL1, sd3, sHub1.getOpts().LeafNode.Port, sHub1.getOpts().LeafNode.Port))) 1135 sLeaf1, _ := RunServerWithConfig(confLeaf1) 1136 defer sLeaf1.Shutdown() 1137 1138 confLeaf2 := createConfFile(t, []byte(fmt.Sprintf(tmplL2, sd3, sHub1.getOpts().LeafNode.Port, sHub1.getOpts().LeafNode.Port))) 1139 sLeaf2, _ := RunServerWithConfig(confLeaf2) 1140 defer sLeaf2.Shutdown() 1141 1142 checkClusterFormed(t, sLeaf1, sLeaf2) 1143 c2 := cluster{t: t, servers: []*Server{sLeaf1, sLeaf2}} 1144 c2.waitOnPeerCount(2) 1145 1146 checkLeafNodeConnectedCount(t, sHub1, 4) 1147 checkLeafNodeConnectedCount(t, sLeaf1, 2) 1148 checkLeafNodeConnectedCount(t, sLeaf2, 2) 1149 1150 ncB := natsConnect(t, fmt.Sprintf("nats://b1:b1@127.0.0.1:%d", sLeaf1.getOpts().Port)) 1151 defer ncB.Close() 1152 jsB1, err := ncB.JetStream() 1153 require_NoError(t, err) 1154 si, err := jsB1.AddStream(&nats.StreamConfig{Name: "foo", Replicas: 1, Subjects: []string{"foo"}}) 1155 require_NoError(t, err) 1156 require_Equal(t, si.Cluster.Name, "HUB") 1157 1158 jsB2, err := ncB.JetStream(nats.Domain("DHUB")) 1159 require_NoError(t, err) 1160 si, err = jsB2.AddStream(&nats.StreamConfig{Name: "bar", Replicas: 1, Subjects: []string{"bar"}}) 1161 require_NoError(t, err) 1162 require_Equal(t, si.Cluster.Name, "HUB") 1163 } 1164 1165 func TestJetStreamLeafNodeSvcImportExportCycle(t *testing.T) { 1166 accounts := ` 1167 accounts { 1168 SYS: { 1169 users: [{user: admin, password: admin}] 1170 } 1171 LEAF_USER: { 1172 users: [{user: leaf_user, password: leaf_user}] 1173 imports: [ 1174 {service: {account: LEAF_INGRESS, subject: "foo"}} 1175 {service: {account: LEAF_INGRESS, subject: "_INBOX.>"}} 1176 {service: {account: LEAF_INGRESS, subject: "$JS.leaf.API.>"}, to: "JS.leaf_ingress@leaf.API.>" } 1177 ] 1178 jetstream: enabled 1179 } 1180 LEAF_INGRESS: { 1181 users: [{user: leaf_ingress, password: leaf_ingress}] 1182 exports: [ 1183 {service: "foo", accounts: [LEAF_USER]} 1184 {service: "_INBOX.>", accounts: [LEAF_USER]} 1185 {service: "$JS.leaf.API.>", response_type: "stream", accounts: [LEAF_USER]} 1186 ] 1187 imports: [ 1188 ] 1189 jetstream: enabled 1190 } 1191 } 1192 system_account: SYS 1193 ` 1194 1195 hconf := createConfFile(t, []byte(fmt.Sprintf(` 1196 %s 1197 listen: "127.0.0.1:-1" 1198 leafnodes { 1199 listen: "127.0.0.1:-1" 1200 } 1201 `, accounts))) 1202 defer os.Remove(hconf) 1203 s, o := RunServerWithConfig(hconf) 1204 defer s.Shutdown() 1205 1206 lconf := createConfFile(t, []byte(fmt.Sprintf(` 1207 %s 1208 server_name: leaf-server 1209 jetstream { 1210 store_dir: '%s' 1211 domain=leaf 1212 } 1213 1214 listen: "127.0.0.1:-1" 1215 leafnodes { 1216 remotes = [ 1217 { 1218 urls: ["nats-leaf://leaf_ingress:leaf_ingress@127.0.0.1:%v"] 1219 account: "LEAF_INGRESS" 1220 } 1221 ] 1222 } 1223 `, accounts, t.TempDir(), o.LeafNode.Port))) 1224 defer os.Remove(lconf) 1225 sl, so := RunServerWithConfig(lconf) 1226 defer sl.Shutdown() 1227 1228 checkLeafNodeConnected(t, sl) 1229 1230 nc := natsConnect(t, fmt.Sprintf("nats://leaf_user:leaf_user@127.0.0.1:%v", so.Port)) 1231 defer nc.Close() 1232 1233 js, _ := nc.JetStream(nats.APIPrefix("JS.leaf_ingress@leaf.API.")) 1234 1235 _, err := js.AddStream(&nats.StreamConfig{ 1236 Name: "TEST", 1237 Subjects: []string{"foo"}, 1238 Storage: nats.FileStorage, 1239 }) 1240 require_NoError(t, err) 1241 1242 _, err = js.Publish("foo", []byte("msg")) 1243 require_NoError(t, err) 1244 }