diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 49a91917..b2d1c9cc 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -27,7 +27,7 @@ jobs: go get -v -t -d ./... - name: Run tests id: tests - run: richgo test -v -race -cover -bench=. ./... + run: richgo test -v -race -cover ./... env: IN_TRAVIS_CI: yes RICHGO_FORCE_COLOR: 1 diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go new file mode 100644 index 00000000..92055432 --- /dev/null +++ b/auth/jwt/jwt.go @@ -0,0 +1,157 @@ +package jwt + +import ( + "sync" + "time" + + jwtToken "github.com/micro/plugins/v5/auth/jwt/token" + "go-micro.dev/v5/auth" + "go-micro.dev/v5/cmd" +) + +func init() { + cmd.DefaultAuths["jwt"] = NewAuth +} + +// NewAuth returns a new instance of the Auth service. +func NewAuth(opts ...auth.Option) auth.Auth { + j := new(jwt) + j.Init(opts...) + return j +} + +func NewRules() auth.Rules { + return new(jwtRules) +} + +type jwt struct { + sync.Mutex + options auth.Options + jwt jwtToken.Provider +} + +type jwtRules struct { + sync.Mutex + rules []*auth.Rule +} + +func (j *jwt) String() string { + return "jwt" +} + +func (j *jwt) Init(opts ...auth.Option) { + j.Lock() + defer j.Unlock() + + for _, o := range opts { + o(&j.options) + } + + j.jwt = jwtToken.New( + jwtToken.WithPrivateKey(j.options.PrivateKey), + jwtToken.WithPublicKey(j.options.PublicKey), + ) +} + +func (j *jwt) Options() auth.Options { + j.Lock() + defer j.Unlock() + return j.options +} + +func (j *jwt) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) { + options := auth.NewGenerateOptions(opts...) + account := &auth.Account{ + ID: id, + Type: options.Type, + Scopes: options.Scopes, + Metadata: options.Metadata, + Issuer: j.Options().Namespace, + } + + // generate a JWT secret which can be provided to the Token() method + // and exchanged for an access token + secret, err := j.jwt.Generate(account) + if err != nil { + return nil, err + } + account.Secret = secret.Token + + // return the account + return account, nil +} + +func (j *jwtRules) Grant(rule *auth.Rule) error { + j.Lock() + defer j.Unlock() + j.rules = append(j.rules, rule) + return nil +} + +func (j *jwtRules) Revoke(rule *auth.Rule) error { + j.Lock() + defer j.Unlock() + + rules := make([]*auth.Rule, 0, len(j.rules)) + for _, r := range j.rules { + if r.ID != rule.ID { + rules = append(rules, r) + } + } + + j.rules = rules + return nil +} + +func (j *jwtRules) Verify(acc *auth.Account, res *auth.Resource, opts ...auth.VerifyOption) error { + j.Lock() + defer j.Unlock() + + var options auth.VerifyOptions + for _, o := range opts { + o(&options) + } + + return auth.Verify(j.rules, acc, res) +} + +func (j *jwtRules) List(opts ...auth.ListOption) ([]*auth.Rule, error) { + j.Lock() + defer j.Unlock() + return j.rules, nil +} + +func (j *jwt) Inspect(token string) (*auth.Account, error) { + return j.jwt.Inspect(token) +} + +func (j *jwt) Token(opts ...auth.TokenOption) (*auth.Token, error) { + options := auth.NewTokenOptions(opts...) + + secret := options.RefreshToken + if len(options.Secret) > 0 { + secret = options.Secret + } + + account, err := j.jwt.Inspect(secret) + if err != nil { + return nil, err + } + + access, err := j.jwt.Generate(account, jwtToken.WithExpiry(options.Expiry)) + if err != nil { + return nil, err + } + + refresh, err := j.jwt.Generate(account, jwtToken.WithExpiry(options.Expiry+time.Hour)) + if err != nil { + return nil, err + } + + return &auth.Token{ + Created: access.Created, + Expiry: access.Expiry, + AccessToken: access.Token, + RefreshToken: refresh.Token, + }, nil +} diff --git a/auth/jwt/token/jwt.go b/auth/jwt/token/jwt.go new file mode 100644 index 00000000..10e62823 --- /dev/null +++ b/auth/jwt/token/jwt.go @@ -0,0 +1,109 @@ +package token + +import ( + "encoding/base64" + "time" + + "github.com/dgrijalva/jwt-go" + "go-micro.dev/v5/auth" +) + +// authClaims to be encoded in the JWT. +type authClaims struct { + Type string `json:"type"` + Scopes []string `json:"scopes"` + Metadata map[string]string `json:"metadata"` + + jwt.StandardClaims +} + +// JWT implementation of token provider. +type JWT struct { + opts Options +} + +// New returns an initialized basic provider. +func New(opts ...Option) Provider { + return &JWT{ + opts: NewOptions(opts...), + } +} + +// Generate a new JWT. +func (j *JWT) Generate(acc *auth.Account, opts ...GenerateOption) (*Token, error) { + // decode the private key + priv, err := base64.StdEncoding.DecodeString(j.opts.PrivateKey) + if err != nil { + return nil, err + } + + // parse the private key + key, err := jwt.ParseRSAPrivateKeyFromPEM(priv) + if err != nil { + return nil, ErrEncodingToken + } + + // parse the options + options := NewGenerateOptions(opts...) + + // generate the JWT + expiry := time.Now().Add(options.Expiry) + t := jwt.NewWithClaims(jwt.SigningMethodRS256, authClaims{ + acc.Type, acc.Scopes, acc.Metadata, jwt.StandardClaims{ + Subject: acc.ID, + Issuer: acc.Issuer, + ExpiresAt: expiry.Unix(), + }, + }) + tok, err := t.SignedString(key) + if err != nil { + return nil, err + } + + // return the token + return &Token{ + Token: tok, + Expiry: expiry, + Created: time.Now(), + }, nil +} + +// Inspect a JWT. +func (j *JWT) Inspect(t string) (*auth.Account, error) { + // decode the public key + pub, err := base64.StdEncoding.DecodeString(j.opts.PublicKey) + if err != nil { + return nil, err + } + + // parse the public key + res, err := jwt.ParseWithClaims(t, &authClaims{}, func(token *jwt.Token) (interface{}, error) { + return jwt.ParseRSAPublicKeyFromPEM(pub) + }) + if err != nil { + return nil, ErrInvalidToken + } + + // validate the token + if !res.Valid { + return nil, ErrInvalidToken + } + claims, ok := res.Claims.(*authClaims) + if !ok { + return nil, ErrInvalidToken + } + + // return the token + return &auth.Account{ + ID: claims.Subject, + Issuer: claims.Issuer, + Type: claims.Type, + Scopes: claims.Scopes, + Metadata: claims.Metadata, + }, nil +} + +// String returns JWT. +func (j *JWT) String() string { + return "jwt" +} diff --git a/auth/jwt/token/jwt_test.go b/auth/jwt/token/jwt_test.go new file mode 100644 index 00000000..e66e86cd --- /dev/null +++ b/auth/jwt/token/jwt_test.go @@ -0,0 +1,85 @@ +package token + +import ( + "os" + "testing" + "time" + + "go-micro.dev/v5/auth" +) + +func TestGenerate(t *testing.T) { + privKey, err := os.ReadFile("test/sample_key") + if err != nil { + t.Fatalf("Unable to read private key: %v", err) + } + + j := New( + WithPrivateKey(string(privKey)), + ) + + _, err = j.Generate(&auth.Account{ID: "test"}) + if err != nil { + t.Fatalf("Generate returned %v error, expected nil", err) + } +} + +func TestInspect(t *testing.T) { + pubKey, err := os.ReadFile("test/sample_key.pub") + if err != nil { + t.Fatalf("Unable to read public key: %v", err) + } + privKey, err := os.ReadFile("test/sample_key") + if err != nil { + t.Fatalf("Unable to read private key: %v", err) + } + + j := New( + WithPublicKey(string(pubKey)), + WithPrivateKey(string(privKey)), + ) + + t.Run("Valid token", func(t *testing.T) { + md := map[string]string{"foo": "bar"} + scopes := []string{"admin"} + subject := "test" + + acc := &auth.Account{ID: subject, Scopes: scopes, Metadata: md} + tok, err := j.Generate(acc) + if err != nil { + t.Fatalf("Generate returned %v error, expected nil", err) + } + + tok2, err := j.Inspect(tok.Token) + if err != nil { + t.Fatalf("Inspect returned %v error, expected nil", err) + } + if acc.ID != subject { + t.Errorf("Inspect returned %v as the token subject, expected %v", acc.ID, subject) + } + if len(tok2.Scopes) != len(scopes) { + t.Errorf("Inspect returned %v scopes, expected %v", len(tok2.Scopes), len(scopes)) + } + if len(tok2.Metadata) != len(md) { + t.Errorf("Inspect returned %v as the token metadata, expected %v", tok2.Metadata, md) + } + }) + + t.Run("Expired token", func(t *testing.T) { + tok, err := j.Generate(&auth.Account{}, WithExpiry(-10*time.Second)) + if err != nil { + t.Fatalf("Generate returned %v error, expected nil", err) + } + + if _, err = j.Inspect(tok.Token); err != ErrInvalidToken { + t.Fatalf("Inspect returned %v error, expected %v", err, ErrInvalidToken) + } + }) + + t.Run("Invalid token", func(t *testing.T) { + _, err := j.Inspect("Invalid token") + if err != ErrInvalidToken { + t.Fatalf("Inspect returned %v error, expected %v", err, ErrInvalidToken) + } + }) +} diff --git a/auth/jwt/token/options.go b/auth/jwt/token/options.go new file mode 100644 index 00000000..2b015a5c --- /dev/null +++ b/auth/jwt/token/options.go @@ -0,0 +1,78 @@ +package token + +import ( + "time" + + "go-micro.dev/v5/store" +) + +type Options struct { + // Store to persist the tokens + Store store.Store + // PublicKey base64 encoded, used by JWT + PublicKey string + // PrivateKey base64 encoded, used by JWT + PrivateKey string +} + +type Option func(o *Options) + +// WithStore sets the token providers store. +func WithStore(s store.Store) Option { + return func(o *Options) { + o.Store = s + } +} + +// WithPublicKey sets the JWT public key. +func WithPublicKey(key string) Option { + return func(o *Options) { + o.PublicKey = key + } +} + +// WithPrivateKey sets the JWT private key. +func WithPrivateKey(key string) Option { + return func(o *Options) { + o.PrivateKey = key + } +} + +func NewOptions(opts ...Option) Options { + var options Options + for _, o := range opts { + o(&options) + } + // set default store + if options.Store == nil { + options.Store = store.DefaultStore + } + return options +} + +type GenerateOptions struct { + // Expiry for the token + Expiry time.Duration +} + +type GenerateOption func(o *GenerateOptions) + +// WithExpiry for the generated account's token expires. +func WithExpiry(d time.Duration) GenerateOption { + return func(o *GenerateOptions) { + o.Expiry = d + } +} + +// NewGenerateOptions from a slice of options. +func NewGenerateOptions(opts ...GenerateOption) GenerateOptions { + var options GenerateOptions + for _, o := range opts { + o(&options) + } + // set default Expiry of token + if options.Expiry == 0 { + options.Expiry = time.Minute * 15 + } + return options +} diff --git a/auth/jwt/token/test/sample_key b/auth/jwt/token/test/sample_key new file mode 100644 index 00000000..25488667 --- /dev/null +++ b/auth/jwt/token/test/sample_key @@ -0,0 +1 @@ +LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS3dJQkFBS0NBZ0VBOFNiSlA1WGJFaWRSbTViMnNOcExHbzJlV2ZVNU9KZTBpemdySHdEOEg3RjZQa1BkCi9SbDkvMXBNVjdNaU8zTEh3dGhIQzJCUllxcisxd0Zkb1pDR0JZckxhWHVYRnFLMHZ1WmhQcUUzYXpqdUlIUXUKMEJIL2xYUU1xeUVxRjVNSTJ6ZWpDNHpNenIxNU9OK2dFNEpuaXBqcC9DZGpPUEFEbUpHK0JKOXFlRS9RUGVtLwptVWRJVC9MYUY3a1F4eVlLNVZLbitOZ09Xek1sektBQXBDbjdUVEtCVWU4RlpHNldTWDdMVjBlTEdIc29pYnhsCm85akRqbFk1b0JPY3pmcWVOV0hLNUdYQjdRd3BMTmg5NDZQelpucW9hcFdVZStZL1JPaUhpekpUY3I1Wk1TTDUKd2xFcThoTmhtaG01Tk5lL08rR2dqQkROU2ZVaDA2K3E0bmdtYm1OWDVoODM4QmJqUmN5YzM2ZHd6NkpVK2R1bwpSdFFoZ2lZOTEwcFBmOWJhdVhXcXdVQ1VhNHFzSHpqS1IwTC9OMVhYQXlsQ0RqeWVnWnp6Y093MkNIOFNrZkZVCnJnTHJQYkVCOWVnY0drMzgrYnBLczNaNlJyNSt0bkQxQklQSUZHTGVJMFVPQzAreGlCdjBvenhJRE9GbldhOVUKVEdEeFV4OG9qOFZJZVJuV0RxNk1jMWlKcDhVeWNpQklUUnR3NGRabzcweG1mbmVJV3pyM0tTTmFoU29nSmRSMApsYVF6QXVQM2FpV1hJTXAyc2M4U2MrQmwrTGpYbUJveEJyYUJIaDlLa0pKRWNnQUZ3czJib2pDbEpPWXhvRi9YCmdGS1NzSW5IRHJIVk95V1BCZTNmYWRFYzc3YituYi9leE96cjFFcnhoR2c5akZtcmtPK3M0eEdodjZNQ0F3RUEKQVFLQ0FnRUFqUzc1Q2VvUlRRcUtBNzZaaFNiNGEzNVlKRENtcEpSazFsRTNKYnFzNFYxRnhXaDBjZmJYeG9VMgpSdTRRYjUrZWhsdWJGSFQ2a1BxdG9uRWhRVExjMUNmVE9WbHJOb3hocDVZM2ZyUmlQcnNnNXcwK1R3RUtrcFJUCnltanJQTXdQbGxCM2U0NmVaYmVXWGc3R3FFVmptMGcxVFRRK0tocVM4R0w3VGJlTFhRN1ZTem9ydTNCNVRKMVEKeEN6TVB0dnQ2eDYrU3JrcmhvZG1iT3VNRkpDam1TbWxmck9pZzQ4Zkc3NUpERHRObXpLWHBEUVJpYUNodFJhVQpQRHpmUTlTamhYdFFqdkZvWFFFT3BqdkZVRjR2WldNUWNQNUw1VklDM3JRSWp4MFNzQTN6S0FwakVUbjJHNjN2CktZby8zVWttbzhkUCtGRHA3NCs5a3pLNHFFaFJycEl3bEtiN0VOZWtDUXZqUFl1K3pyKzMyUXdQNTJ2L2FveWQKdjJJaUY3M2laTU1vZDhhYjJuQStyVEI2T0cvOVlSYk5kV21tay9VTi9jUHYrN214TmZ6Y1d1ZU1XcThxMXh4eAptNTNpR0NSQ29PQ1lDQk4zcUFkb1JwYW5xd3lCOUxrLzFCQjBHUld3MjgxK3VhNXNYRnZBVDBKeTVURnduMncvClU1MlJKWFlNOXVhMFBvd214b0RDUWRuNFZYVkdNZGdXaHN4aXhHRlYwOUZObWJJQWJaN0xaWGtkS1gzc1ZVbTcKWU1WYWIzVVo2bEhtdXYzT1NzcHNVUlRqN1hiRzZpaVVlaDU1aW91OENWbnRndWtFcnEzQTQwT05FVzhjNDBzOQphVTBGaSs4eWZpQTViaVZHLzF0bWlucUVERkhuQStnWk1xNEhlSkZxcWZxaEZKa1JwRGtDZ2dFQkFQeGR1NGNKCm5Da1duZDdPWFlHMVM3UDdkVWhRUzgwSDlteW9uZFc5bGFCQm84RWRPeTVTZzNOUmsxQ2pNZFZ1a3FMcjhJSnkKeStLWk15SVpvSlJvbllaMEtIUUVMR3ZLbzFOS2NLQ1FJbnYvWHVCdFJpRzBVb1pQNVkwN0RpRFBRQWpYUjlXUwpBc0EzMmQ1eEtFOC91Y3h0MjVQVzJFakNBUmtVeHQ5d0tKazN3bC9JdXVYRlExTDdDWjJsOVlFUjlHeWxUbzhNCmxXUEY3YndtUFV4UVNKaTNVS0FjTzZweTVUU1lkdWQ2aGpQeXJwSXByNU42VGpmTlRFWkVBeU9LbXVpOHVkUkoKMUg3T3RQVEhGZElKQjNrNEJnRDZtRE1HbjB2SXBLaDhZN3NtRUZBbFkvaXlCZjMvOHk5VHVMb1BycEdqR3RHbgp4Y2RpMHFud2p0SGFNbFVDZ2dFQkFQU2Z0dVFCQ2dTU2JLUSswUEFSR2VVeEQyTmlvZk1teENNTmdHUzJ5Ull3CjRGaGV4ZWkwMVJoaFk1NjE3UjduR1dzb0czd1RQa3dvRTJtbE1aQkoxeWEvUU9RRnQ3WG02OVl0RGh0T2FWbDgKL0o4dlVuSTBtWmxtT2pjTlRoYnVPZDlNSDlRdGxIRUMxMlhYdHJNb3Fsb0U2a05TT0pJalNxYm9wcDRXc1BqcApvZTZ0Nkdyd1RhOHBHeUJWWS90Mi85Ym5ORHVPVlpjODBaODdtY2gzcDNQclBqU3h5di9saGxYMFMwYUdHTkhTCk1XVjdUa25OaGo1TWlIRXFnZ1pZemtBWTkyd1JoVENnU1A2M0VNcitUWXFudXVuMXJHbndPYm95TDR2aFRpV0UKcU42UDNCTFlCZ1FpMllDTDludEJrOEl6RHZyd096dW5GVnhhZ0g5SVVoY0NnZ0VCQUwzQXlLa1BlOENWUmR6cQpzL284VkJDZmFSOFhhUGRnSGxTek1BSXZpNXEwNENqckRyMlV3MHZwTVdnM1hOZ0xUT3g5bFJpd3NrYk9SRmxHCmhhd3hRUWlBdkk0SE9WTlBTU0R1WHVNTG5USTQ0S0RFNlMrY2cxU0VMS2pWbDVqcDNFOEpkL1RJMVpLc0xBQUsKZTNHakM5UC9ZbE8xL21ndW4xNjVkWk01cFAwWHBPb2FaeFV2RHFFTktyekR0V1g0RngyOTZlUzdaSFJodFpCNwovQ2t1VUhlcmxrN2RDNnZzdWhTaTh2eTM3c0tPbmQ0K3c4cVM4czhZYVZxSDl3ZzVScUxxakp0bmJBUnc3alVDCm9KQ053M1hNdnc3clhaYzRTbnhVQUNMRGJNV2lLQy9xL1ZGWW9oTEs2WkpUVkJscWd5cjBSYzBRWmpDMlNJb0kKMjRwRWt3VUNnZ0VCQUpqb0FJVVNsVFY0WlVwaExXN3g4WkxPa01UWjBVdFFyd2NPR0hSYndPUUxGeUNGMVFWNQppejNiR2s4SmZyZHpVdk1sTmREZm9uQXVHTHhQa3VTVEUxWlg4L0xVRkJveXhyV3dvZ0cxaUtwME11QTV6em90CjROai9DbUtCQVkvWnh2anA5M2RFS21aZGxWQkdmeUFMeWpmTW5MWUovZXh5L09YSnhPUktZTUttSHg4M08zRWsKMWhvb0FwbTZabTIzMjRGME1iVU1ham5Idld2ZjhHZGJTNk5zcHd4L0dkbk1tYVMrdUJMVUhVMkNLbmc1bEIwVAp4OWJITmY0dXlPbTR0dXRmNzhCd1R5V3UreEdrVW0zZ2VZMnkvR1hqdDZyY2l1ajFGNzFDenZzcXFmZThTcDdJCnd6SHdxcTNzVHR5S2lCYTZuYUdEYWpNR1pKYSt4MVZJV204Q2dnRUJBT001ajFZR25Ba0pxR0czQWJSVDIvNUMKaVVxN0loYkswOGZsSGs5a2YwUlVjZWc0ZVlKY3dIRXJVaE4rdWQyLzE3MC81dDYra0JUdTVZOUg3bkpLREtESQpoeEg5SStyamNlVkR0RVNTRkluSXdDQ1lrOHhOUzZ0cHZMV1U5b0pibGFKMlZsalV2NGRFWGVQb0hkREh1Zk9ZClVLa0lsV2E3Uit1QzNEOHF5U1JrQnFLa3ZXZ1RxcFNmTVNkc1ZTeFIzU2Q4SVhFSHFjTDNUNEtMWGtYNEdEamYKMmZOSTFpZkx6ekhJMTN3Tk5IUTVRNU9SUC9pell2QzVzZkx4U2ZIUXJiMXJZVkpKWkI5ZjVBUjRmWFpHSVFsbApjMG8xd0JmZFlqMnZxVDlpR09IQnNSSTlSL2M2RzJQcUt3aFRpSzJVR2lmVFNEUVFuUkF6b2tpQVkrbE8vUjQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== \ No newline at end of file diff --git a/auth/jwt/token/test/sample_key 2 b/auth/jwt/token/test/sample_key 2 new file mode 100644 index 00000000..25488667 --- /dev/null +++ b/auth/jwt/token/test/sample_key 2 @@ -0,0 +1 @@ +LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS3dJQkFBS0NBZ0VBOFNiSlA1WGJFaWRSbTViMnNOcExHbzJlV2ZVNU9KZTBpemdySHdEOEg3RjZQa1BkCi9SbDkvMXBNVjdNaU8zTEh3dGhIQzJCUllxcisxd0Zkb1pDR0JZckxhWHVYRnFLMHZ1WmhQcUUzYXpqdUlIUXUKMEJIL2xYUU1xeUVxRjVNSTJ6ZWpDNHpNenIxNU9OK2dFNEpuaXBqcC9DZGpPUEFEbUpHK0JKOXFlRS9RUGVtLwptVWRJVC9MYUY3a1F4eVlLNVZLbitOZ09Xek1sektBQXBDbjdUVEtCVWU4RlpHNldTWDdMVjBlTEdIc29pYnhsCm85akRqbFk1b0JPY3pmcWVOV0hLNUdYQjdRd3BMTmg5NDZQelpucW9hcFdVZStZL1JPaUhpekpUY3I1Wk1TTDUKd2xFcThoTmhtaG01Tk5lL08rR2dqQkROU2ZVaDA2K3E0bmdtYm1OWDVoODM4QmJqUmN5YzM2ZHd6NkpVK2R1bwpSdFFoZ2lZOTEwcFBmOWJhdVhXcXdVQ1VhNHFzSHpqS1IwTC9OMVhYQXlsQ0RqeWVnWnp6Y093MkNIOFNrZkZVCnJnTHJQYkVCOWVnY0drMzgrYnBLczNaNlJyNSt0bkQxQklQSUZHTGVJMFVPQzAreGlCdjBvenhJRE9GbldhOVUKVEdEeFV4OG9qOFZJZVJuV0RxNk1jMWlKcDhVeWNpQklUUnR3NGRabzcweG1mbmVJV3pyM0tTTmFoU29nSmRSMApsYVF6QXVQM2FpV1hJTXAyc2M4U2MrQmwrTGpYbUJveEJyYUJIaDlLa0pKRWNnQUZ3czJib2pDbEpPWXhvRi9YCmdGS1NzSW5IRHJIVk95V1BCZTNmYWRFYzc3YituYi9leE96cjFFcnhoR2c5akZtcmtPK3M0eEdodjZNQ0F3RUEKQVFLQ0FnRUFqUzc1Q2VvUlRRcUtBNzZaaFNiNGEzNVlKRENtcEpSazFsRTNKYnFzNFYxRnhXaDBjZmJYeG9VMgpSdTRRYjUrZWhsdWJGSFQ2a1BxdG9uRWhRVExjMUNmVE9WbHJOb3hocDVZM2ZyUmlQcnNnNXcwK1R3RUtrcFJUCnltanJQTXdQbGxCM2U0NmVaYmVXWGc3R3FFVmptMGcxVFRRK0tocVM4R0w3VGJlTFhRN1ZTem9ydTNCNVRKMVEKeEN6TVB0dnQ2eDYrU3JrcmhvZG1iT3VNRkpDam1TbWxmck9pZzQ4Zkc3NUpERHRObXpLWHBEUVJpYUNodFJhVQpQRHpmUTlTamhYdFFqdkZvWFFFT3BqdkZVRjR2WldNUWNQNUw1VklDM3JRSWp4MFNzQTN6S0FwakVUbjJHNjN2CktZby8zVWttbzhkUCtGRHA3NCs5a3pLNHFFaFJycEl3bEtiN0VOZWtDUXZqUFl1K3pyKzMyUXdQNTJ2L2FveWQKdjJJaUY3M2laTU1vZDhhYjJuQStyVEI2T0cvOVlSYk5kV21tay9VTi9jUHYrN214TmZ6Y1d1ZU1XcThxMXh4eAptNTNpR0NSQ29PQ1lDQk4zcUFkb1JwYW5xd3lCOUxrLzFCQjBHUld3MjgxK3VhNXNYRnZBVDBKeTVURnduMncvClU1MlJKWFlNOXVhMFBvd214b0RDUWRuNFZYVkdNZGdXaHN4aXhHRlYwOUZObWJJQWJaN0xaWGtkS1gzc1ZVbTcKWU1WYWIzVVo2bEhtdXYzT1NzcHNVUlRqN1hiRzZpaVVlaDU1aW91OENWbnRndWtFcnEzQTQwT05FVzhjNDBzOQphVTBGaSs4eWZpQTViaVZHLzF0bWlucUVERkhuQStnWk1xNEhlSkZxcWZxaEZKa1JwRGtDZ2dFQkFQeGR1NGNKCm5Da1duZDdPWFlHMVM3UDdkVWhRUzgwSDlteW9uZFc5bGFCQm84RWRPeTVTZzNOUmsxQ2pNZFZ1a3FMcjhJSnkKeStLWk15SVpvSlJvbllaMEtIUUVMR3ZLbzFOS2NLQ1FJbnYvWHVCdFJpRzBVb1pQNVkwN0RpRFBRQWpYUjlXUwpBc0EzMmQ1eEtFOC91Y3h0MjVQVzJFakNBUmtVeHQ5d0tKazN3bC9JdXVYRlExTDdDWjJsOVlFUjlHeWxUbzhNCmxXUEY3YndtUFV4UVNKaTNVS0FjTzZweTVUU1lkdWQ2aGpQeXJwSXByNU42VGpmTlRFWkVBeU9LbXVpOHVkUkoKMUg3T3RQVEhGZElKQjNrNEJnRDZtRE1HbjB2SXBLaDhZN3NtRUZBbFkvaXlCZjMvOHk5VHVMb1BycEdqR3RHbgp4Y2RpMHFud2p0SGFNbFVDZ2dFQkFQU2Z0dVFCQ2dTU2JLUSswUEFSR2VVeEQyTmlvZk1teENNTmdHUzJ5Ull3CjRGaGV4ZWkwMVJoaFk1NjE3UjduR1dzb0czd1RQa3dvRTJtbE1aQkoxeWEvUU9RRnQ3WG02OVl0RGh0T2FWbDgKL0o4dlVuSTBtWmxtT2pjTlRoYnVPZDlNSDlRdGxIRUMxMlhYdHJNb3Fsb0U2a05TT0pJalNxYm9wcDRXc1BqcApvZTZ0Nkdyd1RhOHBHeUJWWS90Mi85Ym5ORHVPVlpjODBaODdtY2gzcDNQclBqU3h5di9saGxYMFMwYUdHTkhTCk1XVjdUa25OaGo1TWlIRXFnZ1pZemtBWTkyd1JoVENnU1A2M0VNcitUWXFudXVuMXJHbndPYm95TDR2aFRpV0UKcU42UDNCTFlCZ1FpMllDTDludEJrOEl6RHZyd096dW5GVnhhZ0g5SVVoY0NnZ0VCQUwzQXlLa1BlOENWUmR6cQpzL284VkJDZmFSOFhhUGRnSGxTek1BSXZpNXEwNENqckRyMlV3MHZwTVdnM1hOZ0xUT3g5bFJpd3NrYk9SRmxHCmhhd3hRUWlBdkk0SE9WTlBTU0R1WHVNTG5USTQ0S0RFNlMrY2cxU0VMS2pWbDVqcDNFOEpkL1RJMVpLc0xBQUsKZTNHakM5UC9ZbE8xL21ndW4xNjVkWk01cFAwWHBPb2FaeFV2RHFFTktyekR0V1g0RngyOTZlUzdaSFJodFpCNwovQ2t1VUhlcmxrN2RDNnZzdWhTaTh2eTM3c0tPbmQ0K3c4cVM4czhZYVZxSDl3ZzVScUxxakp0bmJBUnc3alVDCm9KQ053M1hNdnc3clhaYzRTbnhVQUNMRGJNV2lLQy9xL1ZGWW9oTEs2WkpUVkJscWd5cjBSYzBRWmpDMlNJb0kKMjRwRWt3VUNnZ0VCQUpqb0FJVVNsVFY0WlVwaExXN3g4WkxPa01UWjBVdFFyd2NPR0hSYndPUUxGeUNGMVFWNQppejNiR2s4SmZyZHpVdk1sTmREZm9uQXVHTHhQa3VTVEUxWlg4L0xVRkJveXhyV3dvZ0cxaUtwME11QTV6em90CjROai9DbUtCQVkvWnh2anA5M2RFS21aZGxWQkdmeUFMeWpmTW5MWUovZXh5L09YSnhPUktZTUttSHg4M08zRWsKMWhvb0FwbTZabTIzMjRGME1iVU1ham5Idld2ZjhHZGJTNk5zcHd4L0dkbk1tYVMrdUJMVUhVMkNLbmc1bEIwVAp4OWJITmY0dXlPbTR0dXRmNzhCd1R5V3UreEdrVW0zZ2VZMnkvR1hqdDZyY2l1ajFGNzFDenZzcXFmZThTcDdJCnd6SHdxcTNzVHR5S2lCYTZuYUdEYWpNR1pKYSt4MVZJV204Q2dnRUJBT001ajFZR25Ba0pxR0czQWJSVDIvNUMKaVVxN0loYkswOGZsSGs5a2YwUlVjZWc0ZVlKY3dIRXJVaE4rdWQyLzE3MC81dDYra0JUdTVZOUg3bkpLREtESQpoeEg5SStyamNlVkR0RVNTRkluSXdDQ1lrOHhOUzZ0cHZMV1U5b0pibGFKMlZsalV2NGRFWGVQb0hkREh1Zk9ZClVLa0lsV2E3Uit1QzNEOHF5U1JrQnFLa3ZXZ1RxcFNmTVNkc1ZTeFIzU2Q4SVhFSHFjTDNUNEtMWGtYNEdEamYKMmZOSTFpZkx6ekhJMTN3Tk5IUTVRNU9SUC9pell2QzVzZkx4U2ZIUXJiMXJZVkpKWkI5ZjVBUjRmWFpHSVFsbApjMG8xd0JmZFlqMnZxVDlpR09IQnNSSTlSL2M2RzJQcUt3aFRpSzJVR2lmVFNEUVFuUkF6b2tpQVkrbE8vUjQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== \ No newline at end of file diff --git a/auth/jwt/token/test/sample_key.pub b/auth/jwt/token/test/sample_key.pub new file mode 100644 index 00000000..77bd153d --- /dev/null +++ b/auth/jwt/token/test/sample_key.pub @@ -0,0 +1 @@ +LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUE4U2JKUDVYYkVpZFJtNWIyc05wTApHbzJlV2ZVNU9KZTBpemdySHdEOEg3RjZQa1BkL1JsOS8xcE1WN01pTzNMSHd0aEhDMkJSWXFyKzF3RmRvWkNHCkJZckxhWHVYRnFLMHZ1WmhQcUUzYXpqdUlIUXUwQkgvbFhRTXF5RXFGNU1JMnplakM0ek16cjE1T04rZ0U0Sm4KaXBqcC9DZGpPUEFEbUpHK0JKOXFlRS9RUGVtL21VZElUL0xhRjdrUXh5WUs1VktuK05nT1d6TWx6S0FBcENuNwpUVEtCVWU4RlpHNldTWDdMVjBlTEdIc29pYnhsbzlqRGpsWTVvQk9jemZxZU5XSEs1R1hCN1F3cExOaDk0NlB6ClpucW9hcFdVZStZL1JPaUhpekpUY3I1Wk1TTDV3bEVxOGhOaG1obTVOTmUvTytHZ2pCRE5TZlVoMDYrcTRuZ20KYm1OWDVoODM4QmJqUmN5YzM2ZHd6NkpVK2R1b1J0UWhnaVk5MTBwUGY5YmF1WFdxd1VDVWE0cXNIempLUjBMLwpOMVhYQXlsQ0RqeWVnWnp6Y093MkNIOFNrZkZVcmdMclBiRUI5ZWdjR2szOCticEtzM1o2UnI1K3RuRDFCSVBJCkZHTGVJMFVPQzAreGlCdjBvenhJRE9GbldhOVVUR0R4VXg4b2o4VkllUm5XRHE2TWMxaUpwOFV5Y2lCSVRSdHcKNGRabzcweG1mbmVJV3pyM0tTTmFoU29nSmRSMGxhUXpBdVAzYWlXWElNcDJzYzhTYytCbCtMalhtQm94QnJhQgpIaDlLa0pKRWNnQUZ3czJib2pDbEpPWXhvRi9YZ0ZLU3NJbkhEckhWT3lXUEJlM2ZhZEVjNzdiK25iL2V4T3pyCjFFcnhoR2c5akZtcmtPK3M0eEdodjZNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo= \ No newline at end of file diff --git a/auth/jwt/token/token.go b/auth/jwt/token/token.go new file mode 100644 index 00000000..ab290a17 --- /dev/null +++ b/auth/jwt/token/token.go @@ -0,0 +1,33 @@ +package token + +import ( + "errors" + "time" + + "go-micro.dev/v5/auth" +) + +var ( + // ErrNotFound is returned when a token cannot be found. + ErrNotFound = errors.New("token not found") + // ErrEncodingToken is returned when the service encounters an error during encoding. + ErrEncodingToken = errors.New("error encoding the token") + // ErrInvalidToken is returned when the token provided is not valid. + ErrInvalidToken = errors.New("invalid token provided") +) + +// Provider generates and inspects tokens. +type Provider interface { + Generate(account *auth.Account, opts ...GenerateOption) (*Token, error) + Inspect(token string) (*auth.Account, error) + String() string +} + +type Token struct { + // The actual token + Token string `json:"token"` + // Time of token creation + Created time.Time `json:"created"` + // Time of token expiry + Expiry time.Time `json:"expiry"` +} diff --git a/broker/broker.go b/broker/broker.go index 690da409..5c8c304a 100644 --- a/broker/broker.go +++ b/broker/broker.go @@ -41,7 +41,7 @@ type Subscriber interface { var ( // DefaultBroker is the default Broker. - DefaultBroker = NewMemoryBroker() + DefaultBroker = NewHttpBroker() ) func Init(opts ...Option) error { diff --git a/broker/http/http.go b/broker/http.go similarity index 93% rename from broker/http/http.go rename to broker/http.go index ac541402..0bdf07ae 100644 --- a/broker/http/http.go +++ b/broker/http.go @@ -1,5 +1,4 @@ -// Package http provides a http based message broker -package http +package broker import ( "bytes" @@ -16,7 +15,6 @@ import ( "time" "github.com/google/uuid" - "go-micro.dev/v5/broker" "go-micro.dev/v5/codec/json" merr "go-micro.dev/v5/errors" "go-micro.dev/v5/registry" @@ -30,7 +28,7 @@ import ( // HTTP Broker is a point to point async broker. type httpBroker struct { - opts broker.Options + opts Options r registry.Registry @@ -52,8 +50,8 @@ type httpBroker struct { } type httpSubscriber struct { - opts broker.SubscribeOptions - fn broker.Handler + opts SubscribeOptions + fn Handler svc *registry.Service hb *httpBroker id string @@ -62,7 +60,7 @@ type httpSubscriber struct { type httpEvent struct { err error - m *broker.Message + m *Message t string } @@ -109,8 +107,8 @@ func newTransport(config *tls.Config) *http.Transport { return t } -func newHttpBroker(opts ...broker.Option) broker.Broker { - options := *broker.NewOptions(opts...) +func newHttpBroker(opts ...Option) Broker { + options := *NewOptions(opts...) options.Registry = registry.DefaultRegistry options.Codec = json.Marshaler{} @@ -162,7 +160,7 @@ func (h *httpEvent) Error() error { return h.err } -func (h *httpEvent) Message() *broker.Message { +func (h *httpEvent) Message() *Message { return h.m } @@ -170,7 +168,7 @@ func (h *httpEvent) Topic() string { return h.t } -func (h *httpSubscriber) Options() broker.SubscribeOptions { +func (h *httpSubscriber) Options() SubscribeOptions { return h.opts } @@ -309,7 +307,7 @@ func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - var m *broker.Message + var m *Message if err = h.opts.Codec.Unmarshal(b, &m); err != nil { errr := merr.InternalServerError("go.micro.broker", "Error parsing request body: %v", err) w.WriteHeader(500) @@ -331,7 +329,7 @@ func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) { id := req.Form.Get("id") //nolint:prealloc - var subs []broker.Handler + var subs []Handler h.RLock() for _, subscriber := range h.subscribers[topic] { @@ -459,7 +457,7 @@ func (h *httpBroker) Disconnect() error { return err } -func (h *httpBroker) Init(opts ...broker.Option) error { +func (h *httpBroker) Init(opts ...Option) error { h.RLock() if h.running { h.RUnlock() @@ -506,13 +504,13 @@ func (h *httpBroker) Init(opts ...broker.Option) error { return nil } -func (h *httpBroker) Options() broker.Options { +func (h *httpBroker) Options() Options { return h.opts } -func (h *httpBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error { +func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) error { // create the message first - m := &broker.Message{ + m := &Message{ Header: make(map[string]string), Body: msg.Body, } @@ -638,10 +636,10 @@ func (h *httpBroker) Publish(topic string, msg *broker.Message, opts ...broker.P return nil } -func (h *httpBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { +func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) { var err error var host, port string - options := broker.NewSubscribeOptions(opts...) + options := NewSubscribeOptions(opts...) // parse address for host, port host, port, err = net.SplitHostPort(h.Address()) @@ -707,6 +705,6 @@ func (h *httpBroker) String() string { } // NewHttpBroker returns a new http broker. -func NewHttpBroker(opts ...broker.Option) broker.Broker { +func NewHttpBroker(opts ...Option) Broker { return newHttpBroker(opts...) } diff --git a/broker/http/http_test.go b/broker/http_test.go similarity index 93% rename from broker/http/http_test.go rename to broker/http_test.go index bff3f577..e5e701b2 100644 --- a/broker/http/http_test.go +++ b/broker/http_test.go @@ -1,4 +1,4 @@ -package http_test +package broker_test import ( "sync" @@ -7,7 +7,6 @@ import ( "github.com/google/uuid" "go-micro.dev/v5/broker" - "go-micro.dev/v5/broker/http" "go-micro.dev/v5/registry" ) @@ -61,7 +60,7 @@ func sub(b *testing.B, c int) { b.StopTimer() m := newTestRegistry() - brker := http.NewHttpBroker(broker.Registry(m)) + brker := broker.NewHttpBroker(broker.Registry(m)) topic := uuid.New().String() if err := brker.Init(); err != nil { @@ -122,7 +121,7 @@ func sub(b *testing.B, c int) { func pub(b *testing.B, c int) { b.StopTimer() m := newTestRegistry() - brk := http.NewHttpBroker(broker.Registry(m)) + brk := broker.NewHttpBroker(broker.Registry(m)) topic := uuid.New().String() if err := brk.Init(); err != nil { @@ -191,7 +190,7 @@ func pub(b *testing.B, c int) { func TestBroker(t *testing.T) { m := newTestRegistry() - b := http.NewHttpBroker(broker.Registry(m)) + b := broker.NewHttpBroker(broker.Registry(m)) if err := b.Init(); err != nil { t.Fatalf("Unexpected init error: %v", err) @@ -240,7 +239,7 @@ func TestBroker(t *testing.T) { func TestConcurrentSubBroker(t *testing.T) { m := newTestRegistry() - b := http.NewHttpBroker(broker.Registry(m)) + b := broker.NewHttpBroker(broker.Registry(m)) if err := b.Init(); err != nil { t.Fatalf("Unexpected init error: %v", err) @@ -299,7 +298,7 @@ func TestConcurrentSubBroker(t *testing.T) { func TestConcurrentPubBroker(t *testing.T) { m := newTestRegistry() - b := http.NewHttpBroker(broker.Registry(m)) + b := broker.NewHttpBroker(broker.Registry(m)) if err := b.Init(); err != nil { t.Fatalf("Unexpected init error: %v", err) @@ -363,13 +362,6 @@ func BenchmarkSub32(b *testing.B) { sub(b, 32) } -func BenchmarkSub64(b *testing.B) { - sub(b, 64) -} - -func BenchmarkSub128(b *testing.B) { - sub(b, 128) -} func BenchmarkPub1(b *testing.B) { pub(b, 1) @@ -382,11 +374,3 @@ func BenchmarkPub8(b *testing.B) { func BenchmarkPub32(b *testing.B) { pub(b, 32) } - -func BenchmarkPub64(b *testing.B) { - pub(b, 64) -} - -func BenchmarkPub128(b *testing.B) { - pub(b, 128) -} diff --git a/broker/rabbitmq/auth.go b/broker/rabbitmq/auth.go new file mode 100644 index 00000000..c31f9a54 --- /dev/null +++ b/broker/rabbitmq/auth.go @@ -0,0 +1,12 @@ +package rabbitmq + +type ExternalAuthentication struct { +} + +func (auth *ExternalAuthentication) Mechanism() string { + return "EXTERNAL" +} + +func (auth *ExternalAuthentication) Response() string { + return "" +} diff --git a/broker/rabbitmq/channel.go b/broker/rabbitmq/channel.go new file mode 100644 index 00000000..4484ff57 --- /dev/null +++ b/broker/rabbitmq/channel.go @@ -0,0 +1,178 @@ +package rabbitmq + +// +// All credit to Mondo +// + +import ( + "errors" + "sync" + + "github.com/google/uuid" + "github.com/streadway/amqp" +) + +type rabbitMQChannel struct { + uuid string + connection *amqp.Connection + channel *amqp.Channel + confirmPublish chan amqp.Confirmation + mtx sync.Mutex +} + +func newRabbitChannel(conn *amqp.Connection, prefetchCount int, prefetchGlobal bool, confirmPublish bool) (*rabbitMQChannel, error) { + id, err := uuid.NewRandom() + if err != nil { + return nil, err + } + rabbitCh := &rabbitMQChannel{ + uuid: id.String(), + connection: conn, + } + if err := rabbitCh.Connect(prefetchCount, prefetchGlobal, confirmPublish); err != nil { + return nil, err + } + return rabbitCh, nil +} + +func (r *rabbitMQChannel) Connect(prefetchCount int, prefetchGlobal bool, confirmPublish bool) error { + var err error + r.channel, err = r.connection.Channel() + if err != nil { + return err + } + + err = r.channel.Qos(prefetchCount, 0, prefetchGlobal) + if err != nil { + return err + } + + if confirmPublish { + r.confirmPublish = r.channel.NotifyPublish(make(chan amqp.Confirmation, 1)) + + err = r.channel.Confirm(false) + if err != nil { + return err + } + } + + return nil +} + +func (r *rabbitMQChannel) Close() error { + if r.channel == nil { + return errors.New("Channel is nil") + } + return r.channel.Close() +} + +func (r *rabbitMQChannel) Publish(exchange, key string, message amqp.Publishing) error { + if r.channel == nil { + return errors.New("Channel is nil") + } + + if r.confirmPublish != nil { + r.mtx.Lock() + defer r.mtx.Unlock() + } + + err := r.channel.Publish(exchange, key, false, false, message) + if err != nil { + return err + } + + if r.confirmPublish != nil { + confirmation, ok := <-r.confirmPublish + if !ok { + return errors.New("Channel closed before could receive confirmation of publish") + } + + if !confirmation.Ack { + return errors.New("Could not publish message, received nack from broker on confirmation") + } + } + + return nil +} + +func (r *rabbitMQChannel) DeclareExchange(ex Exchange) error { + return r.channel.ExchangeDeclare( + ex.Name, // name + string(ex.Type), // kind + ex.Durable, // durable + false, // autoDelete + false, // internal + false, // noWait + nil, // args + ) +} + +func (r *rabbitMQChannel) DeclareDurableExchange(ex Exchange) error { + return r.channel.ExchangeDeclare( + ex.Name, // name + string(ex.Type), // kind + true, // durable + false, // autoDelete + false, // internal + false, // noWait + nil, // args + ) +} + +func (r *rabbitMQChannel) DeclareQueue(queue string, args amqp.Table) error { + _, err := r.channel.QueueDeclare( + queue, // name + false, // durable + true, // autoDelete + false, // exclusive + false, // noWait + args, // args + ) + return err +} + +func (r *rabbitMQChannel) DeclareDurableQueue(queue string, args amqp.Table) error { + _, err := r.channel.QueueDeclare( + queue, // name + true, // durable + false, // autoDelete + false, // exclusive + false, // noWait + args, // args + ) + return err +} + +func (r *rabbitMQChannel) DeclareReplyQueue(queue string) error { + _, err := r.channel.QueueDeclare( + queue, // name + false, // durable + true, // autoDelete + true, // exclusive + false, // noWait + nil, // args + ) + return err +} + +func (r *rabbitMQChannel) ConsumeQueue(queue string, autoAck bool) (<-chan amqp.Delivery, error) { + return r.channel.Consume( + queue, // queue + r.uuid, // consumer + autoAck, // autoAck + false, // exclusive + false, // nolocal + false, // nowait + nil, // args + ) +} + +func (r *rabbitMQChannel) BindQueue(queue, key, exchange string, args amqp.Table) error { + return r.channel.QueueBind( + queue, // name + key, // key + exchange, // exchange + false, // noWait + args, // args + ) +} diff --git a/broker/rabbitmq/connection.go b/broker/rabbitmq/connection.go new file mode 100644 index 00000000..add465ac --- /dev/null +++ b/broker/rabbitmq/connection.go @@ -0,0 +1,300 @@ +package rabbitmq + +// +// All credit to Mondo +// + +import ( + "crypto/tls" + "regexp" + "strings" + "sync" + "time" + + "github.com/streadway/amqp" + "go-micro.dev/v5/logger" +) + +type MQExchangeType string + +const ( + ExchangeTypeFanout MQExchangeType = "fanout" + ExchangeTypeTopic = "topic" + ExchangeTypeDirect = "direct" +) + +var ( + DefaultExchange = Exchange{ + Name: "micro", + Type: ExchangeTypeTopic, + } + DefaultRabbitURL = "amqp://guest:guest@127.0.0.1:5672" + DefaultPrefetchCount = 0 + DefaultPrefetchGlobal = false + DefaultRequeueOnError = false + DefaultConfirmPublish = false + DefaultWithoutExchange = false + + // The amqp library does not seem to set these when using amqp.DialConfig + // (even though it says so in the comments) so we set them manually to make + // sure to not brake any existing functionality. + defaultHeartbeat = 10 * time.Second + defaultLocale = "en_US" + + defaultAmqpConfig = amqp.Config{ + Heartbeat: defaultHeartbeat, + Locale: defaultLocale, + } + + dial = amqp.Dial + dialTLS = amqp.DialTLS + dialConfig = amqp.DialConfig +) + +type rabbitMQConn struct { + Connection *amqp.Connection + Channel *rabbitMQChannel + ExchangeChannel *rabbitMQChannel + exchange Exchange + withoutExchange bool + url string + prefetchCount int + prefetchGlobal bool + confirmPublish bool + + sync.Mutex + connected bool + close chan bool + + waitConnection chan struct{} + + logger logger.Logger +} + +// Exchange is the rabbitmq exchange. +type Exchange struct { + // Name of the exchange + Name string + // Type of the exchange + Type MQExchangeType + // Whether its persistent + Durable bool +} + +func newRabbitMQConn(ex Exchange, urls []string, prefetchCount int, prefetchGlobal bool, confirmPublish bool, withoutExchange bool, logger logger.Logger) *rabbitMQConn { + var url string + + if len(urls) > 0 && regexp.MustCompile("^amqp(s)?://.*").MatchString(urls[0]) { + url = urls[0] + } else { + url = DefaultRabbitURL + } + + ret := &rabbitMQConn{ + exchange: ex, + url: url, + withoutExchange: withoutExchange, + prefetchCount: prefetchCount, + prefetchGlobal: prefetchGlobal, + confirmPublish: confirmPublish, + close: make(chan bool), + waitConnection: make(chan struct{}), + logger: logger, + } + // its bad case of nil == waitConnection, so close it at start + close(ret.waitConnection) + return ret +} + +func (r *rabbitMQConn) connect(secure bool, config *amqp.Config) error { + // try connect + if err := r.tryConnect(secure, config); err != nil { + return err + } + + // connected + r.Lock() + r.connected = true + r.Unlock() + + // create reconnect loop + go r.reconnect(secure, config) + return nil +} + +func (r *rabbitMQConn) reconnect(secure bool, config *amqp.Config) { + // skip first connect + var connect bool + + for { + if connect { + // try reconnect + if err := r.tryConnect(secure, config); err != nil { + time.Sleep(1 * time.Second) + continue + } + + // connected + r.Lock() + r.connected = true + r.Unlock() + // unblock resubscribe cycle - close channel + //at this point channel is created and unclosed - close it without any additional checks + close(r.waitConnection) + } + + connect = true + notifyClose := make(chan *amqp.Error) + r.Connection.NotifyClose(notifyClose) + chanNotifyClose := make(chan *amqp.Error) + var channel *amqp.Channel + if !r.withoutExchange { + channel = r.ExchangeChannel.channel + } else { + channel = r.Channel.channel + } + channel.NotifyClose(chanNotifyClose) + // To avoid deadlocks it is necessary to consume the messages from all channels. + for notifyClose != nil || chanNotifyClose != nil { + // block until closed + select { + case err := <-chanNotifyClose: + r.logger.Log(logger.ErrorLevel, err) + // block all resubscribe attempt - they are useless because there is no connection to rabbitmq + // create channel 'waitConnection' (at this point channel is nil or closed, create it without unnecessary checks) + r.Lock() + r.connected = false + r.waitConnection = make(chan struct{}) + r.Unlock() + chanNotifyClose = nil + case err := <-notifyClose: + r.logger.Log(logger.ErrorLevel, err) + // block all resubscribe attempt - they are useless because there is no connection to rabbitmq + // create channel 'waitConnection' (at this point channel is nil or closed, create it without unnecessary checks) + r.Lock() + r.connected = false + r.waitConnection = make(chan struct{}) + r.Unlock() + notifyClose = nil + case <-r.close: + return + } + } + } +} + +func (r *rabbitMQConn) Connect(secure bool, config *amqp.Config) error { + r.Lock() + + // already connected + if r.connected { + r.Unlock() + return nil + } + + // check it was closed + select { + case <-r.close: + r.close = make(chan bool) + default: + // no op + // new conn + } + + r.Unlock() + + return r.connect(secure, config) +} + +func (r *rabbitMQConn) Close() error { + r.Lock() + defer r.Unlock() + + select { + case <-r.close: + return nil + default: + close(r.close) + r.connected = false + } + + return r.Connection.Close() +} + +func (r *rabbitMQConn) tryConnect(secure bool, config *amqp.Config) error { + var err error + + if config == nil { + config = &defaultAmqpConfig + } + + url := r.url + + if secure || config.TLSClientConfig != nil || strings.HasPrefix(r.url, "amqps://") { + if config.TLSClientConfig == nil { + config.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + + url = strings.Replace(r.url, "amqp://", "amqps://", 1) + } + + r.Connection, err = dialConfig(url, *config) + + if err != nil { + return err + } + + if r.Channel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish); err != nil { + return err + } + + if !r.withoutExchange { + if r.exchange.Durable { + r.Channel.DeclareDurableExchange(r.exchange) + } else { + r.Channel.DeclareExchange(r.exchange) + } + r.ExchangeChannel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish) + } + return err +} + +func (r *rabbitMQConn) Consume(queue, key string, headers amqp.Table, qArgs amqp.Table, autoAck, durableQueue bool) (*rabbitMQChannel, <-chan amqp.Delivery, error) { + consumerChannel, err := newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish) + if err != nil { + return nil, nil, err + } + + if durableQueue { + err = consumerChannel.DeclareDurableQueue(queue, qArgs) + } else { + err = consumerChannel.DeclareQueue(queue, qArgs) + } + + if err != nil { + return nil, nil, err + } + + deliveries, err := consumerChannel.ConsumeQueue(queue, autoAck) + if err != nil { + return nil, nil, err + } + + if !r.withoutExchange { + err = consumerChannel.BindQueue(queue, key, r.exchange.Name, headers) + if err != nil { + return nil, nil, err + } + } + + return consumerChannel, deliveries, nil +} + +func (r *rabbitMQConn) Publish(exchange, key string, msg amqp.Publishing) error { + if r.withoutExchange { + return r.Channel.Publish("", key, msg) + } + return r.ExchangeChannel.Publish(exchange, key, msg) +} diff --git a/broker/rabbitmq/connection_test.go b/broker/rabbitmq/connection_test.go new file mode 100644 index 00000000..93bf3969 --- /dev/null +++ b/broker/rabbitmq/connection_test.go @@ -0,0 +1,111 @@ +package rabbitmq + +import ( + "crypto/tls" + "errors" + "testing" + + "github.com/streadway/amqp" + "go-micro.dev/v5/logger" +) + +func TestNewRabbitMQConnURL(t *testing.T) { + testcases := []struct { + title string + urls []string + want string + }{ + {"Multiple URLs", []string{"amqp://example.com/one", "amqp://example.com/two"}, "amqp://example.com/one"}, + {"Insecure URL", []string{"amqp://example.com"}, "amqp://example.com"}, + {"Secure URL", []string{"amqps://example.com"}, "amqps://example.com"}, + {"Invalid URL", []string{"http://example.com"}, DefaultRabbitURL}, + {"No URLs", []string{}, DefaultRabbitURL}, + } + + for _, test := range testcases { + conn := newRabbitMQConn(Exchange{Name: "exchange"}, test.urls, 0, false, false, false, logger.DefaultLogger) + + if have, want := conn.url, test.want; have != want { + t.Errorf("%s: invalid url, want %q, have %q", test.title, want, have) + } + } +} + +func TestTryToConnectTLS(t *testing.T) { + var ( + dialCount, dialTLSCount int + + err = errors.New("stop connect here") + ) + + dialConfig = func(_ string, c amqp.Config) (*amqp.Connection, error) { + if c.TLSClientConfig != nil { + dialTLSCount++ + return nil, err + } + + dialCount++ + return nil, err + } + + testcases := []struct { + title string + url string + secure bool + amqpConfig *amqp.Config + wantTLS bool + }{ + {"unsecure url, secure false, no tls config", "amqp://example.com", false, nil, false}, + {"secure url, secure false, no tls config", "amqps://example.com", false, nil, true}, + {"unsecure url, secure true, no tls config", "amqp://example.com", true, nil, true}, + {"unsecure url, secure false, tls config", "amqp://example.com", false, &amqp.Config{TLSClientConfig: &tls.Config{}}, true}, + } + + for _, test := range testcases { + dialCount, dialTLSCount = 0, 0 + + conn := newRabbitMQConn(Exchange{Name: "exchange"}, []string{test.url}, 0, false, false, false, logger.DefaultLogger) + conn.tryConnect(test.secure, test.amqpConfig) + + have := dialCount + if test.wantTLS { + have = dialTLSCount + } + + if have != 1 { + t.Errorf("%s: used wrong dialer, Dial called %d times, DialTLS called %d times", test.title, dialCount, dialTLSCount) + } + } +} + +func TestNewRabbitMQPrefetchConfirmPublish(t *testing.T) { + testcases := []struct { + title string + urls []string + prefetchCount int + prefetchGlobal bool + confirmPublish bool + }{ + {"Multiple URLs", []string{"amqp://example.com/one", "amqp://example.com/two"}, 1, true, true}, + {"Insecure URL", []string{"amqp://example.com"}, 1, true, true}, + {"Secure URL", []string{"amqps://example.com"}, 1, true, true}, + {"Invalid URL", []string{"http://example.com"}, 1, true, true}, + {"No URLs", []string{}, 1, true, true}, + } + + for _, test := range testcases { + conn := newRabbitMQConn(Exchange{Name: "exchange"}, test.urls, test.prefetchCount, test.prefetchGlobal, test.confirmPublish, false, logger.DefaultLogger) + + if have, want := conn.prefetchCount, test.prefetchCount; have != want { + t.Errorf("%s: invalid prefetch count, want %d, have %d", test.title, want, have) + } + + if have, want := conn.prefetchGlobal, test.prefetchGlobal; have != want { + t.Errorf("%s: invalid prefetch global setting, want %t, have %t", test.title, want, have) + } + + if have, want := conn.confirmPublish, test.confirmPublish; have != want { + t.Errorf("%s: invalid confirm setting, want %t, have %t", test.title, want, have) + } + } +} diff --git a/broker/rabbitmq/context.go b/broker/rabbitmq/context.go new file mode 100644 index 00000000..664205c4 --- /dev/null +++ b/broker/rabbitmq/context.go @@ -0,0 +1,48 @@ +package rabbitmq + +import ( + "context" + + "go-micro.dev/v5/broker" + "go-micro.dev/v5/server" +) + +// setSubscribeOption returns a function to setup a context with given value. +func setSubscribeOption(k, v interface{}) broker.SubscribeOption { + return func(o *broker.SubscribeOptions) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, k, v) + } +} + +// setBrokerOption returns a function to setup a context with given value. +func setBrokerOption(k, v interface{}) broker.Option { + return func(o *broker.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, k, v) + } +} + +// setBrokerOption returns a function to setup a context with given value. +func setServerSubscriberOption(k, v interface{}) server.SubscriberOption { + return func(o *server.SubscriberOptions) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, k, v) + } +} + +// setPublishOption returns a function to setup a context with given value. +func setPublishOption(k, v interface{}) broker.PublishOption { + return func(o *broker.PublishOptions) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, k, v) + } +} diff --git a/broker/rabbitmq/options.go b/broker/rabbitmq/options.go new file mode 100644 index 00000000..df2e4b8b --- /dev/null +++ b/broker/rabbitmq/options.go @@ -0,0 +1,189 @@ +package rabbitmq + +import ( + "context" + "time" + + "go-micro.dev/v5/broker" + "go-micro.dev/v5/client" + "go-micro.dev/v5/server" +) + +type durableQueueKey struct{} +type headersKey struct{} +type queueArgumentsKey struct{} +type prefetchCountKey struct{} +type prefetchGlobalKey struct{} +type confirmPublishKey struct{} +type exchangeKey struct{} +type exchangeTypeKey struct{} +type withoutExchangeKey struct{} +type requeueOnErrorKey struct{} +type deliveryMode struct{} +type priorityKey struct{} +type contentType struct{} +type contentEncoding struct{} +type correlationID struct{} +type replyTo struct{} +type expiration struct{} +type messageID struct{} +type timestamp struct{} +type typeMsg struct{} +type userID struct{} +type appID struct{} +type externalAuth struct{} +type durableExchange struct{} + +// ServerDurableQueue provide durable queue option for micro.RegisterSubscriber +func ServerDurableQueue() server.SubscriberOption { + return setServerSubscriberOption(durableQueueKey{}, true) +} + +// ServerAckOnSuccess export AckOnSuccess server.SubscriberOption +func ServerAckOnSuccess() server.SubscriberOption { + return setServerSubscriberOption(ackSuccessKey{}, true) +} + +// DurableQueue creates a durable queue when subscribing. +func DurableQueue() broker.SubscribeOption { + return setSubscribeOption(durableQueueKey{}, true) +} + +// DurableExchange is an option to set the Exchange to be durable. +func DurableExchange() broker.Option { + return setBrokerOption(durableExchange{}, true) +} + +// Headers adds headers used by the headers exchange. +func Headers(h map[string]interface{}) broker.SubscribeOption { + return setSubscribeOption(headersKey{}, h) +} + +// QueueArguments sets arguments for queue creation. +func QueueArguments(h map[string]interface{}) broker.SubscribeOption { + return setSubscribeOption(queueArgumentsKey{}, h) +} + +func RequeueOnError() broker.SubscribeOption { + return setSubscribeOption(requeueOnErrorKey{}, true) +} + +// ExchangeName is an option to set the ExchangeName. +func ExchangeName(e string) broker.Option { + return setBrokerOption(exchangeKey{}, e) +} + +// WithoutExchange is an option to use the rabbitmq default exchange. +// means it would not create any custom exchange. +func WithoutExchange() broker.Option { + return setBrokerOption(withoutExchangeKey{}, true) +} + +// ExchangeType is an option to set the rabbitmq exchange type. +func ExchangeType(t MQExchangeType) broker.Option { + return setBrokerOption(exchangeTypeKey{}, t) +} + +// PrefetchCount ... +func PrefetchCount(c int) broker.Option { + return setBrokerOption(prefetchCountKey{}, c) +} + +// PrefetchGlobal creates a durable queue when subscribing. +func PrefetchGlobal() broker.Option { + return setBrokerOption(prefetchGlobalKey{}, true) +} + +// ConfirmPublish ensures all published messages are confirmed by waiting for an ack/nack from the broker. +func ConfirmPublish() broker.Option { + return setBrokerOption(confirmPublishKey{}, true) +} + +// DeliveryMode sets a delivery mode for publishing. +func DeliveryMode(value uint8) broker.PublishOption { + return setPublishOption(deliveryMode{}, value) +} + +// Priority sets a priority level for publishing. +func Priority(value uint8) broker.PublishOption { + return setPublishOption(priorityKey{}, value) +} + +// ContentType sets a property MIME content type for publishing. +func ContentType(value string) broker.PublishOption { + return setPublishOption(contentType{}, value) +} + +// ContentEncoding sets a property MIME content encoding for publishing. +func ContentEncoding(value string) broker.PublishOption { + return setPublishOption(contentEncoding{}, value) +} + +// CorrelationID sets a property correlation ID for publishing. +func CorrelationID(value string) broker.PublishOption { + return setPublishOption(correlationID{}, value) +} + +// ReplyTo sets a property address to to reply to (ex: RPC) for publishing. +func ReplyTo(value string) broker.PublishOption { + return setPublishOption(replyTo{}, value) +} + +// Expiration sets a property message expiration spec for publishing. +func Expiration(value string) broker.PublishOption { + return setPublishOption(expiration{}, value) +} + +// MessageId sets a property message identifier for publishing. +func MessageId(value string) broker.PublishOption { + return setPublishOption(messageID{}, value) +} + +// Timestamp sets a property message timestamp for publishing. +func Timestamp(value time.Time) broker.PublishOption { + return setPublishOption(timestamp{}, value) +} + +// TypeMsg sets a property message type name for publishing. +func TypeMsg(value string) broker.PublishOption { + return setPublishOption(typeMsg{}, value) +} + +// UserID sets a property user id for publishing. +func UserID(value string) broker.PublishOption { + return setPublishOption(userID{}, value) +} + +// AppID sets a property application id for publishing. +func AppID(value string) broker.PublishOption { + return setPublishOption(appID{}, value) +} + +func ExternalAuth() broker.Option { + return setBrokerOption(externalAuth{}, ExternalAuthentication{}) +} + +type subscribeContextKey struct{} + +// SubscribeContext set the context for broker.SubscribeOption. +func SubscribeContext(ctx context.Context) broker.SubscribeOption { + return setSubscribeOption(subscribeContextKey{}, ctx) +} + +type ackSuccessKey struct{} + +// AckOnSuccess will automatically acknowledge messages when no error is returned. +func AckOnSuccess() broker.SubscribeOption { + return setSubscribeOption(ackSuccessKey{}, true) +} + +// PublishDeliveryMode client.PublishOption for setting message "delivery mode" +// mode , Transient (0 or 1) or Persistent (2) +func PublishDeliveryMode(mode uint8) client.PublishOption { + return func(o *client.PublishOptions) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, deliveryMode{}, mode) + } +} diff --git a/broker/rabbitmq/rabbitmq.go b/broker/rabbitmq/rabbitmq.go new file mode 100644 index 00000000..e35d1110 --- /dev/null +++ b/broker/rabbitmq/rabbitmq.go @@ -0,0 +1,445 @@ +// Package rabbitmq provides a RabbitMQ broker +package rabbitmq + +import ( + "context" + "errors" + "fmt" + "net/url" + "sync" + "time" + + "github.com/streadway/amqp" + "go-micro.dev/v5/broker" + "go-micro.dev/v5/logger" +) + +type rbroker struct { + conn *rabbitMQConn + addrs []string + opts broker.Options + prefetchCount int + prefetchGlobal bool + mtx sync.Mutex + wg sync.WaitGroup +} + +type subscriber struct { + mtx sync.Mutex + unsub chan bool + opts broker.SubscribeOptions + topic string + ch *rabbitMQChannel + durableQueue bool + queueArgs map[string]interface{} + r *rbroker + fn func(msg amqp.Delivery) + headers map[string]interface{} + wg sync.WaitGroup +} + +type publication struct { + d amqp.Delivery + m *broker.Message + t string + err error +} + + + +func (p *publication) Ack() error { + return p.d.Ack(false) +} + +func (p *publication) Error() error { + return p.err +} + +func (p *publication) Topic() string { + return p.t +} + +func (p *publication) Message() *broker.Message { + return p.m +} + +func (s *subscriber) Options() broker.SubscribeOptions { + return s.opts +} + +func (s *subscriber) Topic() string { + return s.topic +} + +func (s *subscriber) Unsubscribe() error { + s.unsub <- true + + // Need to wait on subscriber to exit if autoack is disabled + // since closing the channel will prevent the ack/nack from + // being sent upon handler completion. + if !s.opts.AutoAck { + s.wg.Wait() + } + + s.mtx.Lock() + defer s.mtx.Unlock() + if s.ch != nil { + return s.ch.Close() + } + return nil +} + +func (s *subscriber) resubscribe() { + s.wg.Add(1) + defer s.wg.Done() + + minResubscribeDelay := 100 * time.Millisecond + maxResubscribeDelay := 30 * time.Second + expFactor := time.Duration(2) + reSubscribeDelay := minResubscribeDelay + // loop until unsubscribe + for { + select { + // unsubscribe case + case <-s.unsub: + return + // check shutdown case + case <-s.r.conn.close: + // yep, its shutdown case + return + // wait until we reconect to rabbit + case <-s.r.conn.waitConnection: + // When the connection is disconnected, the waitConnection will be re-assigned, so '<-s.r.conn.waitConnection' maybe blocked. + // Here, it returns once a second, and then the latest waitconnection will be used + case <-time.After(time.Second): + continue + } + + // it may crash (panic) in case of Consume without connection, so recheck it + s.r.mtx.Lock() + if !s.r.conn.connected { + s.r.mtx.Unlock() + continue + } + + ch, sub, err := s.r.conn.Consume( + s.opts.Queue, + s.topic, + s.headers, + s.queueArgs, + s.opts.AutoAck, + s.durableQueue, + ) + + s.r.mtx.Unlock() + switch err { + case nil: + reSubscribeDelay = minResubscribeDelay + s.mtx.Lock() + s.ch = ch + s.mtx.Unlock() + default: + if reSubscribeDelay > maxResubscribeDelay { + reSubscribeDelay = maxResubscribeDelay + } + time.Sleep(reSubscribeDelay) + reSubscribeDelay *= expFactor + continue + } + + SubLoop: + for { + select { + case <-s.unsub: + return + case d, ok := <-sub: + if !ok { + break SubLoop + } + s.r.wg.Add(1) + s.fn(d) + s.r.wg.Done() + } + } + } +} + +func (r *rbroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error { + m := amqp.Publishing{ + Body: msg.Body, + Headers: amqp.Table{}, + } + + options := broker.PublishOptions{} + for _, o := range opts { + o(&options) + } + + if options.Context != nil { + if value, ok := options.Context.Value(deliveryMode{}).(uint8); ok { + m.DeliveryMode = value + } + + if value, ok := options.Context.Value(priorityKey{}).(uint8); ok { + m.Priority = value + } + + if value, ok := options.Context.Value(contentType{}).(string); ok { + m.Headers["Content-Type"] = value + m.ContentType = value + } + + if value, ok := options.Context.Value(contentEncoding{}).(string); ok { + m.ContentEncoding = value + } + + if value, ok := options.Context.Value(correlationID{}).(string); ok { + m.CorrelationId = value + } + + if value, ok := options.Context.Value(replyTo{}).(string); ok { + m.ReplyTo = value + } + + if value, ok := options.Context.Value(expiration{}).(string); ok { + m.Expiration = value + } + + if value, ok := options.Context.Value(messageID{}).(string); ok { + m.MessageId = value + } + + if value, ok := options.Context.Value(timestamp{}).(time.Time); ok { + m.Timestamp = value + } + + if value, ok := options.Context.Value(typeMsg{}).(string); ok { + m.Type = value + } + + if value, ok := options.Context.Value(userID{}).(string); ok { + m.UserId = value + } + + if value, ok := options.Context.Value(appID{}).(string); ok { + m.AppId = value + } + } + + for k, v := range msg.Header { + m.Headers[k] = v + } + + if r.getWithoutExchange() { + m.Headers["Micro-Topic"] = topic + } + + if r.conn == nil { + return errors.New("connection is nil") + } + + return r.conn.Publish(r.conn.exchange.Name, topic, m) +} + +func (r *rbroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { + var ackSuccess bool + + if r.conn == nil { + return nil, errors.New("not connected") + } + + opt := broker.SubscribeOptions{ + AutoAck: true, + } + + for _, o := range opts { + o(&opt) + } + + // Make sure context is setup + if opt.Context == nil { + opt.Context = context.Background() + } + + ctx := opt.Context + if subscribeContext, ok := ctx.Value(subscribeContextKey{}).(context.Context); ok && subscribeContext != nil { + ctx = subscribeContext + } + + var requeueOnError bool + requeueOnError, _ = ctx.Value(requeueOnErrorKey{}).(bool) + + var durableQueue bool + durableQueue, _ = ctx.Value(durableQueueKey{}).(bool) + + var qArgs map[string]interface{} + if qa, ok := ctx.Value(queueArgumentsKey{}).(map[string]interface{}); ok { + qArgs = qa + } + + var headers map[string]interface{} + if h, ok := ctx.Value(headersKey{}).(map[string]interface{}); ok { + headers = h + } + + if bval, ok := ctx.Value(ackSuccessKey{}).(bool); ok && bval { + opt.AutoAck = false + ackSuccess = true + } + + fn := func(msg amqp.Delivery) { + header := make(map[string]string) + for k, v := range msg.Headers { + header[k] = fmt.Sprintf("%v", v) + } + + // Get rid of dependence on 'Micro-Topic' + msgTopic := header["Micro-Topic"] + if msgTopic == "" { + header["Micro-Topic"] = msg.RoutingKey + } + + m := &broker.Message{ + Header: header, + Body: msg.Body, + } + p := &publication{d: msg, m: m, t: msg.RoutingKey} + p.err = handler(p) + if p.err == nil && ackSuccess && !opt.AutoAck { + msg.Ack(false) + } else if p.err != nil && !opt.AutoAck { + msg.Nack(false, requeueOnError) + } + } + + sret := &subscriber{topic: topic, opts: opt, unsub: make(chan bool), r: r, + durableQueue: durableQueue, fn: fn, headers: headers, queueArgs: qArgs, + wg: sync.WaitGroup{}} + + go sret.resubscribe() + + return sret, nil +} + +func (r *rbroker) Options() broker.Options { + return r.opts +} + +func (r *rbroker) String() string { + return "rabbitmq" +} + +func (r *rbroker) Address() string { + if len(r.addrs) > 0 { + u, err := url.Parse(r.addrs[0]) + if err != nil { + return "" + } + + return u.Redacted() + } + return "" +} + +func (r *rbroker) Init(opts ...broker.Option) error { + for _, o := range opts { + o(&r.opts) + } + r.addrs = r.opts.Addrs + return nil +} + +func (r *rbroker) Connect() error { + if r.conn == nil { + r.conn = newRabbitMQConn( + r.getExchange(), + r.opts.Addrs, + r.getPrefetchCount(), + r.getPrefetchGlobal(), + r.getConfirmPublish(), + r.getWithoutExchange(), + r.opts.Logger, + ) + } + + conf := defaultAmqpConfig + + if auth, ok := r.opts.Context.Value(externalAuth{}).(ExternalAuthentication); ok { + conf.SASL = []amqp.Authentication{&auth} + } + + conf.TLSClientConfig = r.opts.TLSConfig + + return r.conn.Connect(r.opts.Secure, &conf) +} + +func (r *rbroker) Disconnect() error { + if r.conn == nil { + return errors.New("connection is nil") + } + ret := r.conn.Close() + r.wg.Wait() // wait all goroutines + return ret +} + +func NewBroker(opts ...broker.Option) broker.Broker { + options := broker.Options{ + Context: context.Background(), + Logger: logger.DefaultLogger, + } + + for _, o := range opts { + o(&options) + } + + return &rbroker{ + addrs: options.Addrs, + opts: options, + } +} + +func (r *rbroker) getExchange() Exchange { + ex := DefaultExchange + + if e, ok := r.opts.Context.Value(exchangeKey{}).(string); ok { + ex.Name = e + } + + if t, ok := r.opts.Context.Value(exchangeTypeKey{}).(MQExchangeType); ok { + ex.Type = t + } + + if d, ok := r.opts.Context.Value(durableExchange{}).(bool); ok { + ex.Durable = d + } + + return ex +} + +func (r *rbroker) getPrefetchCount() int { + if e, ok := r.opts.Context.Value(prefetchCountKey{}).(int); ok { + return e + } + return DefaultPrefetchCount +} + +func (r *rbroker) getPrefetchGlobal() bool { + if e, ok := r.opts.Context.Value(prefetchGlobalKey{}).(bool); ok { + return e + } + return DefaultPrefetchGlobal +} + +func (r *rbroker) getConfirmPublish() bool { + if e, ok := r.opts.Context.Value(confirmPublishKey{}).(bool); ok { + return e + } + return DefaultConfirmPublish +} + +func (r *rbroker) getWithoutExchange() bool { + if e, ok := r.opts.Context.Value(withoutExchangeKey{}).(bool); ok { + return e + } + return DefaultWithoutExchange +} diff --git a/broker/rabbitmq/rabbitmq_test.go b/broker/rabbitmq/rabbitmq_test.go new file mode 100644 index 00000000..b4bcb88a --- /dev/null +++ b/broker/rabbitmq/rabbitmq_test.go @@ -0,0 +1,305 @@ +package rabbitmq_test + +import ( + "context" + "encoding/json" + "os" + "testing" + "time" + + "go-micro.dev/v5/logger" + + micro "go-micro.dev/v5" + broker "go-micro.dev/v5/broker" + rabbitmq "go-micro.dev/v5/broker/rabbitmq" + server "go-micro.dev/v5/server" +) + +type Example struct{} + +func init() { + rabbitmq.DefaultRabbitURL = "amqp://rabbitmq:rabbitmq@127.0.0.1:5672" +} + +type TestEvent struct { + Name string `json:"name"` + Age int `json:"age"` + Time time.Time `json:"time"` +} + +func (e *Example) Handler(ctx context.Context, r interface{}) error { + return nil +} + +func TestDurable(t *testing.T) { + if tr := os.Getenv("TRAVIS"); len(tr) > 0 { + t.Skip() + } + brkrSub := broker.NewSubscribeOptions( + broker.Queue("queue.default"), + broker.DisableAutoAck(), + rabbitmq.DurableQueue(), + ) + + b := rabbitmq.NewBroker() + b.Init() + if err := b.Connect(); err != nil { + t.Logf("cant conect to broker, skip: %v", err) + t.Skip() + } + + s := server.NewServer(server.Broker(b)) + + service := micro.NewService( + micro.Server(s), + micro.Broker(b), + ) + h := &Example{} + // Register a subscriber + micro.RegisterSubscriber( + "topic", + service.Server(), + h.Handler, + server.SubscriberContext(brkrSub.Context), + server.SubscriberQueue("queue.default"), + ) + + // service.Init() + + if err := service.Run(); err != nil { + t.Fatal(err) + } +} + +func TestWithoutExchange(t *testing.T) { + + b := rabbitmq.NewBroker(rabbitmq.WithoutExchange()) + b.Init() + if err := b.Connect(); err != nil { + t.Logf("cant conect to broker, skip: %v", err) + t.Skip() + } + + s := server.NewServer(server.Broker(b)) + + service := micro.NewService( + micro.Server(s), + micro.Broker(b), + ) + brkrSub := broker.NewSubscribeOptions( + broker.Queue("direct.queue"), + broker.DisableAutoAck(), + rabbitmq.DurableQueue(), + ) + // Register a subscriber + err := micro.RegisterSubscriber( + "direct.queue", + service.Server(), + func(ctx context.Context, evt *TestEvent) error { + logger.Logf(logger.InfoLevel, "receive event: %+v", evt) + return nil + }, + server.SubscriberContext(brkrSub.Context), + server.SubscriberQueue("direct.queue"), + ) + if err != nil { + t.Fatal(err) + } + + go func() { + time.Sleep(5 * time.Second) + logger.Logf(logger.InfoLevel, "pub event") + jsonData, _ := json.Marshal(&TestEvent{ + Name: "test", + Age: 16, + }) + err := b.Publish("direct.queue", &broker.Message{ + Body: jsonData, + }, + rabbitmq.DeliveryMode(2), + rabbitmq.ContentType("application/json")) + if err != nil { + t.Fatal(err) + } + }() + + // service.Init() + + if err := service.Run(); err != nil { + t.Fatal(err) + } +} + +func TestFanoutExchange(t *testing.T) { + b := rabbitmq.NewBroker(rabbitmq.ExchangeType(rabbitmq.ExchangeTypeFanout), rabbitmq.ExchangeName("fanout.test")) + b.Init() + if err := b.Connect(); err != nil { + t.Logf("cant conect to broker, skip: %v", err) + t.Skip() + } + + s := server.NewServer(server.Broker(b)) + + service := micro.NewService( + micro.Server(s), + micro.Broker(b), + ) + brkrSub := broker.NewSubscribeOptions( + broker.Queue("fanout.queue"), + broker.DisableAutoAck(), + rabbitmq.DurableQueue(), + ) + // Register a subscriber + err := micro.RegisterSubscriber( + "fanout.queue", + service.Server(), + func(ctx context.Context, evt *TestEvent) error { + logger.Logf(logger.InfoLevel, "receive event: %+v", evt) + return nil + }, + server.SubscriberContext(brkrSub.Context), + server.SubscriberQueue("fanout.queue"), + ) + if err != nil { + t.Fatal(err) + } + + go func() { + time.Sleep(5 * time.Second) + logger.Logf(logger.InfoLevel, "pub event") + jsonData, _ := json.Marshal(&TestEvent{ + Name: "test", + Age: 16, + }) + err := b.Publish("fanout.queue", &broker.Message{ + Body: jsonData, + }, + rabbitmq.DeliveryMode(2), + rabbitmq.ContentType("application/json")) + if err != nil { + t.Fatal(err) + } + }() + + // service.Init() + + if err := service.Run(); err != nil { + t.Fatal(err) + } +} + +func TestDirectExchange(t *testing.T) { + b := rabbitmq.NewBroker(rabbitmq.ExchangeType(rabbitmq.ExchangeTypeDirect), rabbitmq.ExchangeName("direct.test")) + b.Init() + if err := b.Connect(); err != nil { + t.Logf("cant conect to broker, skip: %v", err) + t.Skip() + } + + s := server.NewServer(server.Broker(b)) + + service := micro.NewService( + micro.Server(s), + micro.Broker(b), + ) + brkrSub := broker.NewSubscribeOptions( + broker.Queue("direct.exchange.queue"), + broker.DisableAutoAck(), + rabbitmq.DurableQueue(), + ) + // Register a subscriber + err := micro.RegisterSubscriber( + "direct.exchange.queue", + service.Server(), + func(ctx context.Context, evt *TestEvent) error { + logger.Logf(logger.InfoLevel, "receive event: %+v", evt) + return nil + }, + server.SubscriberContext(brkrSub.Context), + server.SubscriberQueue("direct.exchange.queue"), + ) + if err != nil { + t.Fatal(err) + } + + go func() { + time.Sleep(5 * time.Second) + logger.Logf(logger.InfoLevel, "pub event") + jsonData, _ := json.Marshal(&TestEvent{ + Name: "test", + Age: 16, + }) + err := b.Publish("direct.exchange.queue", &broker.Message{ + Body: jsonData, + }, + rabbitmq.DeliveryMode(2), + rabbitmq.ContentType("application/json")) + if err != nil { + t.Fatal(err) + } + }() + + // service.Init() + + if err := service.Run(); err != nil { + t.Fatal(err) + } +} + +func TestTopicExchange(t *testing.T) { + b := rabbitmq.NewBroker() + b.Init() + if err := b.Connect(); err != nil { + t.Logf("cant conect to broker, skip: %v", err) + t.Skip() + } + + s := server.NewServer(server.Broker(b)) + + service := micro.NewService( + micro.Server(s), + micro.Broker(b), + ) + brkrSub := broker.NewSubscribeOptions( + broker.Queue("topic.exchange.queue"), + broker.DisableAutoAck(), + rabbitmq.DurableQueue(), + ) + // Register a subscriber + err := micro.RegisterSubscriber( + "my-test-topic", + service.Server(), + func(ctx context.Context, evt *TestEvent) error { + logger.Logf(logger.InfoLevel, "receive event: %+v", evt) + return nil + }, + server.SubscriberContext(brkrSub.Context), + server.SubscriberQueue("topic.exchange.queue"), + ) + if err != nil { + t.Fatal(err) + } + + go func() { + time.Sleep(5 * time.Second) + logger.Logf(logger.InfoLevel, "pub event") + jsonData, _ := json.Marshal(&TestEvent{ + Name: "test", + Age: 16, + }) + err := b.Publish("my-test-topic", &broker.Message{ + Body: jsonData, + }, + rabbitmq.DeliveryMode(2), + rabbitmq.ContentType("application/json")) + if err != nil { + t.Fatal(err) + } + }() + + // service.Init() + + if err := service.Run(); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/cmd.go b/cmd/cmd.go index b7e73e0c..185d509b 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -11,8 +11,8 @@ import ( "github.com/urfave/cli/v2" "go-micro.dev/v5/auth" - hbroker "go-micro.dev/v5/broker/http" nbroker "go-micro.dev/v5/broker/nats" + rabbit "go-micro.dev/v5/broker/rabbitmq" "go-micro.dev/v5/broker" "go-micro.dev/v5/cache" @@ -33,7 +33,10 @@ import ( "go-micro.dev/v5/server" "go-micro.dev/v5/store" "go-micro.dev/v5/store/mysql" + natsjskv "go-micro.dev/v5/store/nats-js-kv" + postgres "go-micro.dev/v5/store/postgres" "go-micro.dev/v5/transport" + ntransport "go-micro.dev/v5/transport/nats" ) type Cmd interface { @@ -146,6 +149,11 @@ var ( Usage: "Plugin profile to use. (local, nats, etc)", EnvVars: []string{"MICRO_PROFILE"}, }, + &cli.StringFlag{ + Name: "debug-profile", + Usage: "Debug Plugin profile to use.", + EnvVars: []string{"MICRO_DEBUG_PROFILE"}, + }, &cli.StringFlag{ Name: "registry", EnvVars: []string{"MICRO_REGISTRY"}, @@ -240,9 +248,10 @@ var ( } DefaultBrokers = map[string]func(...broker.Option) broker.Broker{ - "memory": broker.NewMemoryBroker, - "http": hbroker.NewHttpBroker, - "nats": nbroker.NewNatsBroker, + "memory": broker.NewMemoryBroker, + "http": broker.NewHttpBroker, + "nats": nbroker.NewNatsBroker, + "rabbitmq": rabbit.NewBroker, } DefaultClients = map[string]func(...client.Option) client.Client{} @@ -259,18 +268,22 @@ var ( DefaultServers = map[string]func(...server.Option) server.Server{} - DefaultTransports = map[string]func(...transport.Option) transport.Transport{} + DefaultTransports = map[string]func(...transport.Option) transport.Transport{ + "nats": ntransport.NewTransport, + } DefaultStores = map[string]func(...store.Option) store.Store{ - "memory": store.NewMemoryStore, - "mysql": mysql.NewMysqlStore, + "memory": store.NewMemoryStore, + "mysql": mysql.NewMysqlStore, + "natsjskv": natsjskv.NewStore, + "postgres": postgres.NewStore, } DefaultTracers = map[string]func(...trace.Option) trace.Tracer{} DefaultAuths = map[string]func(...auth.Option) auth.Auth{} - DefaultProfiles = map[string]func(...profile.Option) profile.Profile{ + DefaultDebugProfiles = map[string]func(...profile.Option) profile.Profile{ "http": http.NewProfile, "pprof": pprof.NewProfile, } @@ -288,31 +301,31 @@ func init() { func newCmd(opts ...Option) Cmd { options := Options{ - Auth: &auth.DefaultAuth, - Broker: &broker.DefaultBroker, - Client: &client.DefaultClient, - Registry: ®istry.DefaultRegistry, - Server: &server.DefaultServer, - Selector: &selector.DefaultSelector, - Transport: &transport.DefaultTransport, - Store: &store.DefaultStore, - Tracer: &trace.DefaultTracer, - Profile: &profile.DefaultProfile, - Config: &config.DefaultConfig, - Cache: &cache.DefaultCache, + Auth: &auth.DefaultAuth, + Broker: &broker.DefaultBroker, + Client: &client.DefaultClient, + Registry: ®istry.DefaultRegistry, + Server: &server.DefaultServer, + Selector: &selector.DefaultSelector, + Transport: &transport.DefaultTransport, + Store: &store.DefaultStore, + Tracer: &trace.DefaultTracer, + DebugProfile: &profile.DefaultProfile, + Config: &config.DefaultConfig, + Cache: &cache.DefaultCache, - Brokers: DefaultBrokers, - Clients: DefaultClients, - Registries: DefaultRegistries, - Selectors: DefaultSelectors, - Servers: DefaultServers, - Transports: DefaultTransports, - Stores: DefaultStores, - Tracers: DefaultTracers, - Auths: DefaultAuths, - Profiles: DefaultProfiles, - Configs: DefaultConfigs, - Caches: DefaultCaches, + Brokers: DefaultBrokers, + Clients: DefaultClients, + Registries: DefaultRegistries, + Selectors: DefaultSelectors, + Servers: DefaultServers, + Transports: DefaultTransports, + Stores: DefaultStores, + Tracers: DefaultTracers, + Auths: DefaultAuths, + DebugProfiles: DefaultDebugProfiles, + Configs: DefaultConfigs, + Caches: DefaultCaches, } for _, o := range opts { @@ -354,12 +367,57 @@ func (c *cmd) Before(ctx *cli.Context) error { // If flags are set then use them otherwise do nothing var serverOpts []server.Option var clientOpts []client.Option + // --- Profile Grouping Extension --- + profileName := ctx.String("profile") + if profileName == "" { + profileName = os.Getenv("MICRO_PROFILE") + } + if profileName != "" { + switch profileName { + case "local": + imported := mprofile.LocalProfile() + *c.opts.Registry = imported.Registry + registry.DefaultRegistry = imported.Registry + *c.opts.Broker = imported.Broker + broker.DefaultBroker = imported.Broker + *c.opts.Store = imported.Store + store.DefaultStore = imported.Store + *c.opts.Transport = imported.Transport + transport.DefaultTransport = imported.Transport + case "nats": + imported := mprofile.NatsProfile() + // Set the registry + sopts, clopts := c.setRegistry(imported.Registry) + serverOpts = append(serverOpts, sopts...) + clientOpts = append(clientOpts, clopts...) + + // set the store + sopts, clopts = c.setStore(imported.Store) + serverOpts = append(serverOpts, sopts...) + clientOpts = append(clientOpts, clopts...) + + // set the transport + sopts, clopts = c.setTransport(imported.Transport) + serverOpts = append(serverOpts, sopts...) + clientOpts = append(clientOpts, clopts...) + + // Set the broker + sopts, clopts = c.setBroker(imported.Broker) + serverOpts = append(serverOpts, sopts...) + clientOpts = append(clientOpts, clopts...) + + // Add more profiles as needed + default: + return fmt.Errorf("unsupported profile: %s", profileName) + } + } // Set the client if name := ctx.String("client"); len(name) > 0 { // only change if we have the client and type differs if cl, ok := c.opts.Clients[name]; ok && (*c.opts.Client).String() != name { *c.opts.Client = cl() + client.DefaultClient = *c.opts.Client } } @@ -368,6 +426,7 @@ func (c *cmd) Before(ctx *cli.Context) error { // only change if we have the server and type differs if s, ok := c.opts.Servers[name]; ok && (*c.opts.Server).String() != name { *c.opts.Server = s() + server.DefaultServer = *c.opts.Server } } @@ -379,6 +438,7 @@ func (c *cmd) Before(ctx *cli.Context) error { } *c.opts.Store = s(store.WithClient(*c.opts.Client)) + store.DefaultStore = *c.opts.Store } // Set the tracer @@ -389,6 +449,7 @@ func (c *cmd) Before(ctx *cli.Context) error { } *c.opts.Tracer = r() + trace.DefaultTracer = *c.opts.Tracer } // Setup auth @@ -415,6 +476,7 @@ func (c *cmd) Before(ctx *cli.Context) error { } *c.opts.Auth = r(authOpts...) + auth.DefaultAuth = *c.opts.Auth } // Set the registry @@ -424,63 +486,19 @@ func (c *cmd) Before(ctx *cli.Context) error { return fmt.Errorf("Registry %s not found", name) } - *c.opts.Registry = r() - serverOpts = append(serverOpts, server.Registry(*c.opts.Registry)) - clientOpts = append(clientOpts, client.Registry(*c.opts.Registry)) - - if err := (*c.opts.Selector).Init(selector.Registry(*c.opts.Registry)); err != nil { - logger.Fatalf("Error configuring registry: %v", err) - } - - clientOpts = append(clientOpts, client.Selector(*c.opts.Selector)) - - if err := (*c.opts.Broker).Init(broker.Registry(*c.opts.Registry)); err != nil { - logger.Fatalf("Error configuring broker: %v", err) - } + sopts, clopts := c.setRegistry(r()) + serverOpts = append(serverOpts, sopts...) + clientOpts = append(clientOpts, clopts...) } - // --- Profile Grouping Extension --- - // Check for new profile flag/env (not just debug profiler) - profileName := ctx.String("profile") - if profileName == "" { - profileName = os.Getenv("MICRO_PROFILE") - } - if profileName != "" { - switch profileName { - case "local": - imported := mprofile.LocalProfile() - *c.opts.Registry = imported.Registry - registry.DefaultRegistry = imported.Registry - *c.opts.Broker = imported.Broker - broker.DefaultBroker = imported.Broker - *c.opts.Store = imported.Store - store.DefaultStore = imported.Store - *c.opts.Transport = imported.Transport - transport.DefaultTransport = imported.Transport - case "nats": - imported := mprofile.NatsProfile() - *c.opts.Registry = imported.Registry - registry.DefaultRegistry = imported.Registry - *c.opts.Broker = imported.Broker - broker.DefaultBroker = imported.Broker - *c.opts.Store = imported.Store - store.DefaultStore = imported.Store - *c.opts.Transport = imported.Transport - transport.DefaultTransport = imported.Transport - // Add more profiles as needed - default: - return fmt.Errorf("unsupported profile: %s", profileName) - } - } - - // Set the profile - if name := ctx.String("profile"); len(name) > 0 { - p, ok := c.opts.Profiles[name] + // Set the debug profile + if name := ctx.String("debug-profile"); len(name) > 0 { + p, ok := c.opts.DebugProfiles[name] if !ok { return fmt.Errorf("unsupported profile: %s", name) } - - *c.opts.Profile = p() + *c.opts.DebugProfile = p() + profile.DefaultProfile = *c.opts.DebugProfile } // Set the broker @@ -489,10 +507,9 @@ func (c *cmd) Before(ctx *cli.Context) error { if !ok { return fmt.Errorf("Broker %s not found", name) } - - *c.opts.Broker = b() - serverOpts = append(serverOpts, server.Broker(*c.opts.Broker)) - clientOpts = append(clientOpts, client.Broker(*c.opts.Broker)) + sopts, clopts := c.setBroker(b()) + serverOpts = append(serverOpts, sopts...) + clientOpts = append(clientOpts, clopts...) } // Set the selector @@ -506,6 +523,7 @@ func (c *cmd) Before(ctx *cli.Context) error { // No server option here. Should there be? clientOpts = append(clientOpts, client.Selector(*c.opts.Selector)) + selector.DefaultSelector = *c.opts.Selector } // Set the transport @@ -515,9 +533,10 @@ func (c *cmd) Before(ctx *cli.Context) error { return fmt.Errorf("Transport %s not found", name) } - *c.opts.Transport = t() - serverOpts = append(serverOpts, server.Transport(*c.opts.Transport)) - clientOpts = append(clientOpts, client.Transport(*c.opts.Transport)) + sopts, clopts := c.setTransport(t()) + serverOpts = append(serverOpts, sopts...) + clientOpts = append(clientOpts, clopts...) + } // Parse the server options @@ -657,12 +676,60 @@ func (c *cmd) Before(ctx *cli.Context) error { logger.Fatalf("Error configuring config: %v", err) } *c.opts.Config = rc + config.DefaultConfig = *c.opts.Config } } - return nil } +func (c *cmd) setRegistry(r registry.Registry) ([]server.Option, []client.Option) { + var serverOpts []server.Option + var clientOpts []client.Option + *c.opts.Registry = r + serverOpts = append(serverOpts, server.Registry(*c.opts.Registry)) + clientOpts = append(clientOpts, client.Registry(*c.opts.Registry)) + + if err := (*c.opts.Selector).Init(selector.Registry(*c.opts.Registry)); err != nil { + logger.Fatalf("Error configuring registry: %v", err) + } + + clientOpts = append(clientOpts, client.Selector(*c.opts.Selector)) + + if err := (*c.opts.Broker).Init(broker.Registry(*c.opts.Registry)); err != nil { + logger.Fatalf("Error configuring broker: %v", err) + } + registry.DefaultRegistry = *c.opts.Registry + return serverOpts, clientOpts +} + +func (c *cmd) setBroker(b broker.Broker) ([]server.Option, []client.Option) { + var serverOpts []server.Option + var clientOpts []client.Option + *c.opts.Broker = b + serverOpts = append(serverOpts, server.Broker(*c.opts.Broker)) + clientOpts = append(clientOpts, client.Broker(*c.opts.Broker)) + broker.DefaultBroker = *c.opts.Broker + return serverOpts, clientOpts +} + +func (c *cmd) setStore(s store.Store) ([]server.Option, []client.Option) { + var serverOpts []server.Option + var clientOpts []client.Option + *c.opts.Store = s + store.DefaultStore = *c.opts.Store + return serverOpts, clientOpts +} + +func (c *cmd) setTransport(t transport.Transport) ([]server.Option, []client.Option) { + var serverOpts []server.Option + var clientOpts []client.Option + *c.opts.Transport = t + serverOpts = append(serverOpts, server.Transport(*c.opts.Transport)) + clientOpts = append(clientOpts, client.Transport(*c.opts.Transport)) + transport.DefaultTransport = *c.opts.Transport + return serverOpts, clientOpts +} + func (c *cmd) Init(opts ...Option) error { for _, o := range opts { o(&c.opts) diff --git a/cmd/options.go b/cmd/options.go index dc79fad7..221ed434 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -21,22 +21,22 @@ type Options struct { // Other options for implementations of the interface // can be stored in a context - Context context.Context - Auth *auth.Auth - Selector *selector.Selector - Profile *profile.Profile + Context context.Context + Auth *auth.Auth + Selector *selector.Selector + DebugProfile *profile.Profile Registry *registry.Registry - Brokers map[string]func(...broker.Option) broker.Broker - Transport *transport.Transport - Cache *cache.Cache - Config *config.Config - Client *client.Client - Server *server.Server - Caches map[string]func(...cache.Option) cache.Cache - Tracer *trace.Tracer - Profiles map[string]func(...profile.Option) profile.Profile + Brokers map[string]func(...broker.Option) broker.Broker + Transport *transport.Transport + Cache *cache.Cache + Config *config.Config + Client *client.Client + Server *server.Server + Caches map[string]func(...cache.Option) cache.Cache + Tracer *trace.Tracer + DebugProfiles map[string]func(...profile.Option) profile.Profile // We need pointers to things so we can swap them out if needed. Broker *broker.Broker @@ -81,72 +81,84 @@ func Version(v string) Option { func Broker(b *broker.Broker) Option { return func(o *Options) { o.Broker = b + broker.DefaultBroker = *b } } func Cache(c *cache.Cache) Option { return func(o *Options) { o.Cache = c + cache.DefaultCache = *c } } func Config(c *config.Config) Option { return func(o *Options) { o.Config = c + config.DefaultConfig = *c } } func Selector(s *selector.Selector) Option { return func(o *Options) { o.Selector = s + selector.DefaultSelector = *s } } func Registry(r *registry.Registry) Option { return func(o *Options) { o.Registry = r + registry.DefaultRegistry = *r } } func Transport(t *transport.Transport) Option { return func(o *Options) { o.Transport = t + transport.DefaultTransport = *t } } func Client(c *client.Client) Option { return func(o *Options) { o.Client = c + client.DefaultClient = *c } } func Server(s *server.Server) Option { return func(o *Options) { o.Server = s + server.DefaultServer = *s } } func Store(s *store.Store) Option { return func(o *Options) { o.Store = s + store.DefaultStore = *s } } func Tracer(t *trace.Tracer) Option { return func(o *Options) { o.Tracer = t + trace.DefaultTracer = *t } } func Auth(a *auth.Auth) Option { return func(o *Options) { o.Auth = a + auth.DefaultAuth = *a } } func Profile(p *profile.Profile) Option { return func(o *Options) { - o.Profile = p + o.DebugProfile = p + profile.DefaultProfile = *p } } @@ -223,6 +235,6 @@ func NewConfig(name string, t func(...config.Option) (config.Config, error)) Opt // New profile func. func NewProfile(name string, t func(...profile.Option) profile.Profile) Option { return func(o *Options) { - o.Profiles[name] = t + o.DebugProfiles[name] = t } } diff --git a/config/source/nats/README.md b/config/source/nats/README.md new file mode 100644 index 00000000..b3d1c636 --- /dev/null +++ b/config/source/nats/README.md @@ -0,0 +1,56 @@ +# Nats Source + +The nats source reads config from nats key/values + +## Nats Format + +The nats source expects keys under the default bucket `default` default key `micro_config` + +Values are expected to be json + +``` +nats kv put default micro_config '{"nats": {"address": "10.0.0.1", "port": 8488}}' +``` + +``` +conf.Get("nats") +``` + +## New Source + +Specify source with data + +```go +natsSource := nats.NewSource( + nats.WithUrl("127.0.0.1:4222"), + nats.WithBucket("my_bucket"), + nats.WithKey("my_key"), +) +``` + +## Load Source + +Load the source into config + +```go +// Create new config +conf := config.NewConfig() + +// Load nats source +conf.Load(natsSource) +``` + +## Watch + +```go +wh, _ := natsSource.Watch() + +for { + v, err := watcher.Next() + if err != nil { + log.Fatalf("err %v", err) + } + + log.Infof("data %v", string(v.Data)) +} +``` diff --git a/config/source/nats/nats.go b/config/source/nats/nats.go new file mode 100644 index 00000000..b4467857 --- /dev/null +++ b/config/source/nats/nats.go @@ -0,0 +1,134 @@ +package nats + +import ( + "fmt" + "net" + "strings" + "time" + + natsgo "github.com/nats-io/nats.go" + "go-micro.dev/v5/config/source" + log "go-micro.dev/v5/logger" +) + +type nats struct { + url string + bucket string + key string + kv natsgo.KeyValue + opts source.Options +} + +// DefaultBucket is the bucket that nats keys will be assumed to have if you +// haven't specified one. +var ( + DefaultBucket = "default" + DefaultKey = "micro_config" +) + +func (n *nats) Read() (*source.ChangeSet, error) { + e, err := n.kv.Get(n.key) + if err != nil { + if err == natsgo.ErrKeyNotFound { + return nil, nil + } + return nil, err + } + + if e.Value() == nil || len(e.Value()) == 0 { + return nil, fmt.Errorf("source not found: %s", n.key) + } + + cs := &source.ChangeSet{ + Data: e.Value(), + Format: n.opts.Encoder.String(), + Source: n.String(), + Timestamp: time.Now(), + } + cs.Checksum = cs.Sum() + + return cs, nil +} + +func (n *nats) Write(cs *source.ChangeSet) error { + _, err := n.kv.Put(n.key, cs.Data) + if err != nil { + return err + } + + return nil +} + +func (n *nats) String() string { + return "nats" +} + +func (n *nats) Watch() (source.Watcher, error) { + return newWatcher(n.kv, n.bucket, n.key, n.String(), n.opts.Encoder) +} + +func NewSource(opts ...source.Option) source.Source { + options := source.NewOptions(opts...) + + config := natsgo.GetDefaultOptions() + + urls, ok := options.Context.Value(urlKey{}).([]string) + endpoints := []string{} + if ok { + for _, u := range urls { + addr, port, err := net.SplitHostPort(u) + if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { + port = "4222" + addr = u + endpoints = append(endpoints, fmt.Sprintf("%s:%s", addr, port)) + } else if err == nil { + endpoints = append(endpoints, fmt.Sprintf("%s:%s", addr, port)) + } + } + } + if len(endpoints) == 0 { + endpoints = append(endpoints, "127.0.0.1:4222") + } + + bucket, ok := options.Context.Value(bucketKey{}).(string) + if !ok { + bucket = DefaultBucket + } + + key, ok := options.Context.Value(keyKey{}).(string) + if !ok { + key = DefaultKey + } + + config.Url = strings.Join(endpoints, ",") + + nc, err := natsgo.Connect(config.Url) + if err != nil { + log.Error(err) + } + + js, err := nc.JetStream(natsgo.MaxWait(10 * time.Second)) + if err != nil { + log.Error(err) + } + + kv, err := js.KeyValue(bucket) + if err == natsgo.ErrBucketNotFound || err == natsgo.ErrKeyNotFound { + kv, err = js.CreateKeyValue(&natsgo.KeyValueConfig{Bucket: bucket}) + if err != nil { + log.Error(err) + } + } + + if err != nil { + log.Error(err) + } + + return &nats{ + url: config.Url, + bucket: bucket, + key: key, + kv: kv, + opts: options, + } +} diff --git a/config/source/nats/options.go b/config/source/nats/options.go new file mode 100644 index 00000000..959348f0 --- /dev/null +++ b/config/source/nats/options.go @@ -0,0 +1,54 @@ +package nats + +import ( + "context" + "time" + + natsgo "github.com/nats-io/nats.go" + "go-micro.dev/v5/config/source" +) + +type ( + urlKey struct{} + bucketKey struct{} + keyKey struct{} +) + +// WithUrl sets the nats url. +func WithUrl(a ...string) source.Option { + return func(o *source.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, urlKey{}, a) + } +} + +// WithBucket sets the nats key. +func WithBucket(a string) source.Option { + return func(o *source.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, bucketKey{}, a) + } +} + +// WithKey sets the nats key. +func WithKey(a string) source.Option { + return func(o *source.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, keyKey{}, a) + } +} + +func Client(url string) (natsgo.JetStreamContext, error) { + nc, err := natsgo.Connect(url) + if err != nil { + return nil, err + } + + return nc.JetStream(natsgo.MaxWait(10 * time.Second)) +} diff --git a/config/source/nats/watcher.go b/config/source/nats/watcher.go new file mode 100644 index 00000000..bd1f0ee8 --- /dev/null +++ b/config/source/nats/watcher.go @@ -0,0 +1,79 @@ +package nats + +import ( + "time" + + natsgo "github.com/nats-io/nats.go" + "go-micro.dev/v5/config/encoder" + "go-micro.dev/v5/config/source" +) + +type watcher struct { + e encoder.Encoder + name string + bucket string + key string + + ch chan *source.ChangeSet + exit chan bool +} + +func newWatcher(kv natsgo.KeyValue, bucket, key, name string, e encoder.Encoder) (source.Watcher, error) { + w := &watcher{ + e: e, + name: name, + bucket: bucket, + key: key, + ch: make(chan *source.ChangeSet), + exit: make(chan bool), + } + + wh, _ := kv.Watch(key) + + go func() { + for { + select { + case v := <-wh.Updates(): + if v != nil { + w.handle(v.Value()) + } + case <-w.exit: + _ = wh.Stop() + return + } + } + }() + return w, nil +} + +func (w *watcher) handle(data []byte) { + cs := &source.ChangeSet{ + Timestamp: time.Now(), + Format: w.e.String(), + Source: w.name, + Data: data, + } + cs.Checksum = cs.Sum() + + w.ch <- cs +} + +func (w *watcher) Next() (*source.ChangeSet, error) { + select { + case cs := <-w.ch: + return cs, nil + case <-w.exit: + return nil, source.ErrWatcherStopped + } +} + +func (w *watcher) Stop() error { + select { + case <-w.exit: + return nil + default: + close(w.exit) + } + + return nil +} diff --git a/go.mod b/go.mod index 028473d1..89e32c48 100644 --- a/go.mod +++ b/go.mod @@ -6,26 +6,36 @@ toolchain go1.24.1 require ( github.com/bitly/go-simplejson v0.5.0 + github.com/cornelk/hashmap v1.0.8 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/fsnotify/fsnotify v1.6.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.9.2 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 github.com/hashicorp/consul/api v1.32.1 - github.com/imdario/mergo v0.3.12 - github.com/kr/pretty v0.3.0 + github.com/imdario/mergo v0.3.13 + github.com/jackc/pgx/v4 v4.18.3 + github.com/kr/pretty v0.3.1 + github.com/lib/pq v1.10.9 + github.com/micro/plugins/v5/auth/jwt v0.0.0-20250502062951-be3f35ce6464 github.com/miekg/dns v1.1.50 github.com/mitchellh/hashstructure v1.1.0 + github.com/nats-io/nats-server/v2 v2.11.3 github.com/nats-io/nats.go v1.42.0 github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 + github.com/streadway/amqp v1.1.0 github.com/stretchr/testify v1.10.0 + github.com/test-go/testify v1.1.4 github.com/urfave/cli/v2 v2.25.7 go.etcd.io/bbolt v1.4.0 go.etcd.io/etcd/api/v3 v3.5.21 go.etcd.io/etcd/client/v3 v3.5.21 + go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.37.0 golang.org/x/net v0.38.0 @@ -45,7 +55,10 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/color v1.16.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-tpm v0.9.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect @@ -54,27 +67,39 @@ require ( github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/serf v0.10.1 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.14.3 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgtype v1.14.0 // indirect + github.com/jackc/puddle v1.3.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/highwayhash v1.0.3 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/nats-io/jwt/v2 v2.7.4 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.31.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 82c1c1a6..f0295213 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,14 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op h1:+OSa/t11TFhqfrX0EOSqQBDJ0YlpmK0rDSiB19dg9M0= +github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= @@ -24,17 +28,26 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc= +github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -46,8 +59,11 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -58,6 +74,8 @@ github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRj github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -71,9 +89,12 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= +github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= @@ -120,8 +141,57 @@ github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= +github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -130,16 +200,24 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -147,6 +225,8 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -155,10 +235,14 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/micro/plugins/v5/auth/jwt v0.0.0-20250502062951-be3f35ce6464 h1:einNYloNFQ4h52c0CBvWv67frSq1xS0EUXCf1ncr1UM= +github.com/micro/plugins/v5/auth/jwt v0.0.0-20250502062951-be3f35ce6464/go.mod h1:Mqqsr1LYrIiAuqKUI/C0sJRoIB80SATNBagcXjqK7oQ= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -172,6 +256,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI= +github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= +github.com/nats-io/nats-server/v2 v2.11.3 h1:AbGtXxuwjo0gBroLGGr/dE0vf24kTKdRnBq/3z/Fdoc= +github.com/nats-io/nats-server/v2 v2.11.3/go.mod h1:6Z6Fd+JgckqzKig7DYwhgrE7bJ6fypPHnGPND+DqgMY= github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM= github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= @@ -191,6 +279,7 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -211,26 +300,42 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= +github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= @@ -239,6 +344,7 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= @@ -259,30 +365,52 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -305,8 +433,10 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -319,6 +449,7 @@ golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -326,23 +457,37 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -364,14 +509,17 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/profile/profile.go b/profile/profile.go index 52191652..1d8f62cf 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -3,15 +3,18 @@ package profile import ( "os" + "strings" + + natslib "github.com/nats-io/nats.go" "go-micro.dev/v5/broker" - "go-micro.dev/v5/broker/http" "go-micro.dev/v5/broker/nats" "go-micro.dev/v5/registry" nreg "go-micro.dev/v5/registry/nats" "go-micro.dev/v5/store" + nstore "go-micro.dev/v5/store/nats-js-kv" "go-micro.dev/v5/transport" - + ntx "go-micro.dev/v5/transport/nats" ) type Profile struct { @@ -21,23 +24,52 @@ type Profile struct { Transport transport.Transport } +// LocalProfile returns a profile with local mDNS as the registry, HTTP as the broker, file as the store, and HTTP as the transport +// It is used for local development and testing func LocalProfile() Profile { return Profile{ Registry: registry.NewMDNSRegistry(), - Broker: http.NewHttpBroker(), + Broker: broker.NewHttpBroker(), Store: store.NewFileStore(), Transport: transport.NewHTTPTransport(), } } +// NatsProfile returns a profile with NATS as the registry, broker, store, and transport +// It uses the environment variable MICR_NATS_ADDRESS to set the NATS server address +// If the variable is not set, it defaults to nats://0.0.0.0:4222 which will connect to a local NATS server func NatsProfile() Profile { addr := os.Getenv("MICRO_NATS_ADDRESS") + if addr == "" { + addr = "nats://0.0.0.0:4222" + } + // Split the address by comma, trim whitespace, and convert to a slice of strings + addrs := splitNatsAdressList(addr) + reg := nreg.NewNatsRegistry(registry.Addrs(addrs...)) + brok := nats.NewNatsBroker(broker.Addrs(addrs...)) + st := nstore.NewStore(nstore.NatsOptions(natslib.Options{Servers: addrs})) + tx := ntx.NewTransport(ntx.Options(natslib.Options{Servers: addrs})) + + registry.DefaultRegistry = reg + broker.DefaultBroker = brok + store.DefaultStore = st + transport.DefaultTransport = tx return Profile{ - Registry: nreg.NewNatsRegistry(registry.Addrs(addr)), - Broker: nats.NewNatsBroker(broker.Addrs(addr)), - Store: store.NewFileStore(), // nats-backed store when available - Transport: transport.NewHTTPTransport(), // nats transport when available + Registry: reg, + Broker: brok, + Store: st, + Transport: tx, } } +func splitNatsAdressList(addr string) []string { + // Split the address by comma + addrs := strings.Split(addr, ",") + // Trim any whitespace from each address + for i, a := range addrs { + addrs[i] = strings.TrimSpace(a) + } + return addrs +} + // Add more profiles as needed, e.g. grpc diff --git a/service/options.go b/service/options.go index ed737096..f4b4a406 100644 --- a/service/options.go +++ b/service/options.go @@ -83,12 +83,14 @@ func Broker(b broker.Broker) Option { // Update Client and Server o.Client.Init(client.Broker(b)) o.Server.Init(server.Broker(b)) + broker.DefaultBroker = b } } func Cache(c cache.Cache) Option { return func(o *Options) { o.Cache = c + cache.DefaultCache = c } } @@ -102,6 +104,7 @@ func Cmd(c cmd.Cmd) Option { func Client(c client.Client) Option { return func(o *Options) { o.Client = c + client.DefaultClient = c } } @@ -135,6 +138,7 @@ func HandleSignal(b bool) Option { func Profile(p profile.Profile) Option { return func(o *Options) { o.Profile = p + profile.DefaultProfile = p } } @@ -142,6 +146,7 @@ func Profile(p profile.Profile) Option { func Server(s server.Server) Option { return func(o *Options) { o.Server = s + server.DefaultServer = s } } @@ -149,6 +154,7 @@ func Server(s server.Server) Option { func Store(s store.Store) Option { return func(o *Options) { o.Store = s + store.DefaultStore = s } } @@ -162,6 +168,7 @@ func Registry(r registry.Registry) Option { o.Server.Init(server.Registry(r)) // Update Broker o.Broker.Init(broker.Registry(r)) + broker.DefaultBroker = o.Broker } } @@ -170,12 +177,15 @@ func Tracer(t trace.Tracer) Option { return func(o *Options) { o.Server.Init(server.Tracer(t)) } + } // Auth sets the auth for the service. func Auth(a auth.Auth) Option { return func(o *Options) { o.Auth = a + auth.DefaultAuth = a + } } @@ -183,6 +193,7 @@ func Auth(a auth.Auth) Option { func Config(c config.Config) Option { return func(o *Options) { o.Config = c + config.DefaultConfig = c } } @@ -190,6 +201,7 @@ func Config(c config.Config) Option { func Selector(s selector.Selector) Option { return func(o *Options) { o.Client.Init(client.Selector(s)) + selector.DefaultSelector = s } } @@ -201,6 +213,7 @@ func Transport(t transport.Transport) Option { // Update Client and Server o.Client.Init(client.Transport(t)) o.Server.Init(server.Transport(t)) + transport.DefaultTransport = t } } diff --git a/store/nats-js-kv/README.md b/store/nats-js-kv/README.md new file mode 100644 index 00000000..84db5e6d --- /dev/null +++ b/store/nats-js-kv/README.md @@ -0,0 +1,79 @@ +# NATS JetStream Key Value Store Plugin + +This plugin uses the NATS JetStream [KeyValue Store](https://docs.nats.io/nats-concepts/jetstream/key-value-store) to implement the Go-Micro store interface. + +You can use this plugin like any other store plugin. +To start a local NATS JetStream server run `nats-server -js`. + +To manually create a new storage object call: + +```go +natsjskv.NewStore(opts ...store.Option) +``` + +The Go-Micro store interface uses databases and tables to store keys. These translate +to buckets (key value stores) and key prefixes. If no database (bucket name) is provided, "default" will be used. + +You can call `Write` with any arbitrary database name, and if a bucket with that name does not exist yet, +it will be automatically created. + +If a table name is provided, it will use it to prefix the key as `