Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NATS publisher support to reminder #4829

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Vyom-Yadav
Copy link
Collaborator

Summary

Provide a brief overview of the changes and the issue being addressed.
Explain the rationale and any background necessary for understanding the changes.
List dependencies required by this change, if any.

Add NATS as an option for reminder to publish events.

Change Type

Mark the type of change your PR introduces:

  • Bug fix (resolves an issue without affecting existing features)
  • Feature (adds new functionality without breaking changes)
  • Breaking change (may impact existing functionalities or require documentation updates)
  • Documentation (updates or additions to documentation)
  • Refactoring or test improvements (no bug fixes or new functionality)

Testing

Outline how the changes were tested, including steps to reproduce and any relevant configurations.
Attach screenshots if helpful.

Tested locally, working fine (nats stream view output):

...
[269] Subject: minder.internal.repo.reconciler.event Received: 1970-01-01T05:30:02+05:30

  ce-source: minder
  ce-specversion: 1.0
  ce-subject: TODO
  ce-type: minder.internal.repo.reconciler.event
  ce-datacontenttype: application/json
  ce-id: 36f8fc06-6b36-48e8-ad92-3cde66759a13
  ce-minderpublished0at: 2024-10-26T18:39:07Z

{"entity_id":"ca6f63c7-672b-4906-ae2d-ce050cd0b6bc","project":"f6bf23d3-ace6-4652-b858-8f9addf7bea5","provider":"a3c63545-59d7-4d46-9a4c-5028876ad236"}

...

Review Checklist:

  • Reviewed my own code for quality and clarity.
  • Added comments to complex or tricky code sections.
  • Updated any affected documentation.
  • Included tests that validate the fix or feature.
  • Checked that related changes are merged.

@Vyom-Yadav Vyom-Yadav requested a review from a team as a code owner October 26, 2024 18:46
Comment on lines +129 to +136
_, err = js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
Name: c.cfg.Prefix,
Subjects: []string{c.cfg.Prefix + ".>"},
})
return err
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newer jetstream API, cleaner code, context support. https://github.com/nats-io/nats.go/blob/main/jetstream/README.md

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 if this works; I think when I started looking, this wasn't cleaned up / operable with CloudEvents yet.

@Vyom-Yadav Vyom-Yadav force-pushed the add-nats-publisher-to-reminder branch from 3d399bf to 748cb2c Compare October 27, 2024 11:34
Copy link
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a separate fix going in for the one test failure.

In general, I think you could have separated the changes to internal/events/nats from the changes to reminder, which makes it easier to roll back or hold one change or the other if there needs to be discussion. But I'm willing to approve this and then iterate more going forward, modulo the comments, particularly about re-using eventer.New, rather than duplicating the setup code at this point.

@@ -153,7 +150,7 @@ func instantiateDriver(
return eventersql.BuildPostgreSQLDriver(ctx, cfg)
case constants.NATSDriver:
zerolog.Ctx(ctx).Info().Msg("Using NATS driver")
return nats.BuildNatsChannelDriver(cfg)
return nats.BuildNatsChannelDriver(&cfg.Nats)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the other constructors take the entire cfg, so that's the model I followed here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has become a tedious problem to refactor. I have to change the signature of BuildNatsChannelDriver as cloudEventsNatsAdapter is not externally exposed.

Either I change the signature of every driver function to take in not serverconfig, but only the relevant config (in order to follow a pattern), which again isn't pretty as it does not truly achieve separation from serverconfig as functions like BuildPostgreSQLDriver would still rely on components purely in serverconfig (like cfg.SQLPubSub.AckDeadline) which isn't there in reminderconfig. Either it would result in breaking the pattern or some form of duplication of the instantiation code.

I see how this can slowly become just duplication of all instantiation code in order to keep minder and reminder separate, but I'm torn between keeping them separate (as minder is the main engine and reminder is a completely different service) v/s keeping them in the same code for ease of maintainability (and for de duplication - which again strikes the thought that this duplication would not have been there if reminder was in another language).

What do you think about it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to re-use the implementation of the construction code in reminder, even as it runs as a separate service. (For example, most Kubernetes controllers use the same informers library and construction client to create their connection to the K8s apiserver.)

So, my suggestion would be to embed a serverconfig.EventConfig into the ReminderConfig, rather than attempting to mirror only selected options into the reminder config. This will mean that it's possible to have something meaningless like the Go channel in reminder; you can avoid that by adding a check in the top-level ReminderConfig.Validate that the event configuration isn't using the go module.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This pattern will also give you access to the flag-driven eventer, and future improvements in flagging that I'm planning -- using a central flag server, rather than repeating the flag file for each binary.)

Comment on lines +20 to +29
func (r *reminder) getMessagePublisher(ctx context.Context) (message.Publisher, common.DriverCloser, error) {
switch r.cfg.EventConfig.Driver {
case constants.NATSDriver:
return r.setupNATSPublisher(ctx)
case constants.SQLDriver:
return r.setupSQLPublisher(ctx)
default:
return nil, nil, fmt.Errorf("unknown publisher type: %s", r.cfg.EventConfig.Driver)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm nervous about having this diverge from internal/events/events.go. Can we use an eventer.Interface from eventer.New() here?

(I need to do some refactoring in that package to move eventer/interface into eventer, but I'll wait until this code is landed to reduce the amount of conflicts.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm nervous about having this diverge from internal/events/events.go

internal/events/events.go is all about serverconfig, it would diverge from it as this is reminderconfig. I don't think we should use the interfaces.Interface:

// Interface is a combination of the Publisher, Registrar, and Service interfaces.
// This is handy when spawning the eventer in a single function for easy setup.
type Interface interface {
	Publisher
	Registrar
	Service
}

as reminder only needs publishing capability, I'd be happy to implement interfaces.Publisher for reminder.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to use the interfaces.Publisher in the reminder code, but it feels like it might be easiest if the reminder code expected an interfaces.Publisher (which is satisfied by the current eventing implementations), but the construction code used NewEventer to construct the interfaces.Publisher.

Comment on lines +152 to +162

// NatsConfig is the configuration when using NATS as the event driver
type NatsConfig struct {
// URL is the URL for the NATS server
URL string `mapstructure:"url" default:"nats://localhost:4222"`
// Prefix is the prefix for the NATS subjects to subscribe to
Prefix string `mapstructure:"prefix" default:"minder"`
// Queue is the name of the queue group to join when consuming messages
// queue groups allow multiple process to round-robin process messages.
Queue string `mapstructure:"queue" default:"minder"`
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to move this up to common (rather than just having a split between client and server configuration), let's put it in its own file. It feels kind of peculiar to have this in common while having the rest of the event configuration still in server, so I'd argue for moving the code back, particularly in the context of having smaller, more focused PRs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to move this up to common (rather than just having a split between client and server configuration), let's put it in its own file

That still breaks the pattern as all the structs in common don't have their own file.

so I'd argue for moving the code back,

To duplicate vs 'have common code'. Do you want me to duplicate the NatsConfig code in that case for reminderconfig

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are a couple options here:

  1. The way that structs are all piled into common.go today is probably a historical mistake. I'm not asking you fix that mistake in this PR (because we should be making small, targeted changes), but we shouldn't make that mistake worse. By putting this in nats or eventing files, it will make the future expansion more clear.
  2. I think it's fine to have pkg/config/reminder depend on pkg/config/server, as long as we keep the graph acyclic. In particular, if we have common server-side configuration for e.g. flags and messaging, it feels like it would make sense to have those be shared between the two. Given that you can reasonably run a minder server without reminder, but the opposite is not very useful, I'd argue for having pkg/config/reminder import parts of the config from pkg/config/server. In the future, if we split the Minder server into e.g. rpc_server and event_processor, I'd suggest that both would want to use parts of the current ServerConfig, but would probably each have their own top-level struct for configuration (possibly in a separate module, or possibly in the pkg/config/server module).

I'm more concerned about leaking the dependencies on e.g. NATS into the minder client, which is statically linked and downloaded by many more users than the server. Right now, this take that dependency, but I could see add a helper to create a NATS connection to this library in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of moving the reminder configuration into the same package as server, and calling them all "server-side components`? It feels like it would allow for more sharing between the two implementations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial thoughts with reminder were that it'll be a separate micro service (remember when we had the talk of maybe let's write it in python). I agree with it all being server side components, but I'm not sure about moving that to server config (I wanted to keep the dependency tree separate, as they'd be packaged as separate binaries):

$ go list -f '{{range .Imports}}{{.}} # These are just imports and not deps
{{end}}' ./pkg/config/server
context
crypto/rsa
errors
fmt
github.com/go-playground/validator/v10
github.com/golang-jwt/jwt/v4
github.com/mindersec/minder/internal/auth/jwt
github.com/mindersec/minder/internal/util
github.com/mindersec/minder/pkg/config
github.com/rs/zerolog
github.com/rs/zerolog/log
github.com/spf13/pflag
github.com/spf13/viper
golang.org/x/oauth2
golang.org/x/oauth2/clientcredentials
io
net/http
net/url
os
path/filepath
strings
time
$ go list -f '{{range .Imports}}{{.}}
{{end}}' ./pkg/config/reminder
fmt
github.com/mindersec/minder/internal/util
github.com/mindersec/minder/pkg/config
github.com/rs/zerolog
github.com/spf13/pflag
github.com/spf13/viper
os
strings
time

It feels like it would allow for more sharing between the two implementations.

I agree with that. Currently, there is some duplication regarding how we manage components, which I try to tackle by moving things to a common place (which doesn't feel ideal in some situations). But, I'm of the opinion to try to keep things separate.

Comment on lines +129 to +136
_, err = js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
Name: c.cfg.Prefix,
Subjects: []string{c.cfg.Prefix + ".>"},
})
return err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 if this works; I think when I started looking, this wasn't cleaned up / operable with CloudEvents yet.

Copy link
Contributor

This PR needs additional information before we can continue. It is now marked as stale because it has been open for 30 days with no activity. Please provide the necessary details to continue or it will be closed in 30 days.

@github-actions github-actions bot added the Stale label Nov 28, 2024
@Vyom-Yadav Vyom-Yadav force-pushed the add-nats-publisher-to-reminder branch from 748cb2c to 8b9ebfb Compare December 27, 2024 16:18
@Vyom-Yadav
Copy link
Collaborator Author

Vyom-Yadav commented Dec 27, 2024

Sorry for the delay on this @evankanderson. Please ignore the failing CI, that's not the main point. I'm confused between how to refactor this properly, because there isn't any easy and good way imo. In all approaches, we either have some duplication or break some pattern (unless we refactor a lot of code in serverconfig and internal/events/events.go).

Three problems right now:

  1. Where to move NatsConfig?
  2. Changing signature of BuildNatsChannelDriver?
  3. Should we move reminderconfig to the serverconfig section?

@Vyom-Yadav Vyom-Yadav removed the Stale label Dec 27, 2024
Copy link
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope using parts of the server-side configuration in reminder makes sense -- if necessary, we could have a server/common that both components use, but that feels like a lot of extra refactoring without a lot of extra value.

Comment on lines +152 to +162

// NatsConfig is the configuration when using NATS as the event driver
type NatsConfig struct {
// URL is the URL for the NATS server
URL string `mapstructure:"url" default:"nats://localhost:4222"`
// Prefix is the prefix for the NATS subjects to subscribe to
Prefix string `mapstructure:"prefix" default:"minder"`
// Queue is the name of the queue group to join when consuming messages
// queue groups allow multiple process to round-robin process messages.
Queue string `mapstructure:"queue" default:"minder"`
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are a couple options here:

  1. The way that structs are all piled into common.go today is probably a historical mistake. I'm not asking you fix that mistake in this PR (because we should be making small, targeted changes), but we shouldn't make that mistake worse. By putting this in nats or eventing files, it will make the future expansion more clear.
  2. I think it's fine to have pkg/config/reminder depend on pkg/config/server, as long as we keep the graph acyclic. In particular, if we have common server-side configuration for e.g. flags and messaging, it feels like it would make sense to have those be shared between the two. Given that you can reasonably run a minder server without reminder, but the opposite is not very useful, I'd argue for having pkg/config/reminder import parts of the config from pkg/config/server. In the future, if we split the Minder server into e.g. rpc_server and event_processor, I'd suggest that both would want to use parts of the current ServerConfig, but would probably each have their own top-level struct for configuration (possibly in a separate module, or possibly in the pkg/config/server module).

I'm more concerned about leaking the dependencies on e.g. NATS into the minder client, which is statically linked and downloaded by many more users than the server. Right now, this take that dependency, but I could see add a helper to create a NATS connection to this library in the future.

Comment on lines +20 to +29
func (r *reminder) getMessagePublisher(ctx context.Context) (message.Publisher, common.DriverCloser, error) {
switch r.cfg.EventConfig.Driver {
case constants.NATSDriver:
return r.setupNATSPublisher(ctx)
case constants.SQLDriver:
return r.setupSQLPublisher(ctx)
default:
return nil, nil, fmt.Errorf("unknown publisher type: %s", r.cfg.EventConfig.Driver)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to use the interfaces.Publisher in the reminder code, but it feels like it might be easiest if the reminder code expected an interfaces.Publisher (which is satisfied by the current eventing implementations), but the construction code used NewEventer to construct the interfaces.Publisher.

@@ -153,7 +150,7 @@ func instantiateDriver(
return eventersql.BuildPostgreSQLDriver(ctx, cfg)
case constants.NATSDriver:
zerolog.Ctx(ctx).Info().Msg("Using NATS driver")
return nats.BuildNatsChannelDriver(cfg)
return nats.BuildNatsChannelDriver(&cfg.Nats)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to re-use the implementation of the construction code in reminder, even as it runs as a separate service. (For example, most Kubernetes controllers use the same informers library and construction client to create their connection to the K8s apiserver.)

So, my suggestion would be to embed a serverconfig.EventConfig into the ReminderConfig, rather than attempting to mirror only selected options into the reminder config. This will mean that it's possible to have something meaningless like the Go channel in reminder; you can avoid that by adding a check in the top-level ReminderConfig.Validate that the event configuration isn't using the go module.

@@ -153,7 +150,7 @@ func instantiateDriver(
return eventersql.BuildPostgreSQLDriver(ctx, cfg)
case constants.NATSDriver:
zerolog.Ctx(ctx).Info().Msg("Using NATS driver")
return nats.BuildNatsChannelDriver(cfg)
return nats.BuildNatsChannelDriver(&cfg.Nats)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This pattern will also give you access to the flag-driven eventer, and future improvements in flagging that I'm planning -- using a central flag server, rather than repeating the flag file for each binary.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants