-
Notifications
You must be signed in to change notification settings - Fork 1
feat(server): engine.commands defense-in-depth consumer (PR 5) #3
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| package server | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "time" | ||
|
|
||
| "github.com/SiriusScan/go-api/sirius/queue" | ||
| "go.uber.org/zap" | ||
| ) | ||
|
|
||
| const ( | ||
| engineCommandsQueueName = "engine.commands" | ||
| ) | ||
|
|
||
| // EngineCommandMessage is the legacy envelope still used by some | ||
| // producers (notably the pre-PR3 sirius-api delete path and any | ||
| // third-party integrations that publish directly to engine.commands). | ||
| // | ||
| // Two commands matter for template-sync purposes: | ||
| // - "internal:template upload" - new custom template was written. | ||
| // - "internal:template delete" - custom template was removed. | ||
| // | ||
| // In both cases we just need to nudge connected agents to re-pull the | ||
| // template:meta:* namespace; the repository-level sync is unaffected. | ||
| // Anything else is acked and ignored so we don't block other producers | ||
| // that might land on this queue. | ||
| type EngineCommandMessage struct { | ||
| Command string `json:"command"` | ||
| TemplateID string `json:"template_id,omitempty"` | ||
| Timestamp string `json:"timestamp,omitempty"` | ||
| } | ||
|
|
||
| // isTemplateNotifyCommand reports whether the legacy command string | ||
| // should trigger an agent re-pull. Kept as a small pure helper so the | ||
| // classification can be unit-tested without spinning up RabbitMQ. | ||
| func isTemplateNotifyCommand(cmd string) bool { | ||
| switch cmd { | ||
| case "internal:template upload", "internal:template delete": | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| // EngineCommandQueueProcessor is the defense-in-depth consumer that | ||
| // catches any producer still publishing to engine.commands. PR 3 made | ||
| // the canonical path agent.template.sync.jobs / notify_agents, so this | ||
| // consumer is strictly additive: if PR 3's path keeps working this | ||
| // consumer logs but never has anything to do. | ||
| type EngineCommandQueueProcessor struct { | ||
| repositoryMgr *RepositoryManager | ||
| logger *zap.Logger | ||
| ctx context.Context | ||
| cancelFunc context.CancelFunc | ||
| } | ||
|
|
||
| // NewEngineCommandQueueProcessor wires the consumer to the repository | ||
| // manager (notify-agents path). | ||
| func NewEngineCommandQueueProcessor( | ||
| repositoryMgr *RepositoryManager, | ||
| logger *zap.Logger, | ||
| ) *EngineCommandQueueProcessor { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| return &EngineCommandQueueProcessor{ | ||
| repositoryMgr: repositoryMgr, | ||
| logger: logger, | ||
| ctx: ctx, | ||
| cancelFunc: cancel, | ||
| } | ||
| } | ||
|
|
||
| // StartListening spawns the goroutine that pumps messages off | ||
| // engine.commands and routes recognized template commands into the | ||
| // notify-agents helper. Mirrors the shape of TemplateSyncQueueProcessor. | ||
| func (ecp *EngineCommandQueueProcessor) StartListening() error { | ||
| ecp.logger.Info("Starting engine.commands queue processor (defense-in-depth)") | ||
|
|
||
| go func() { | ||
| processor := func(msg string) { | ||
| ecp.logger.Debug("Received engine.commands message", zap.String("message", msg)) | ||
|
|
||
| var cmd EngineCommandMessage | ||
| if err := json.Unmarshal([]byte(msg), &cmd); err != nil { | ||
| ecp.logger.Warn("Failed to parse engine.commands message", | ||
| zap.Error(err), | ||
| zap.String("raw", msg)) | ||
| return | ||
| } | ||
|
|
||
| processCtx, cancel := context.WithTimeout(ecp.ctx, 2*time.Minute) | ||
| defer cancel() | ||
|
|
||
| if isTemplateNotifyCommand(cmd.Command) { | ||
| ecp.logger.Info("Routing engine.commands template event to notify-agents", | ||
| zap.String("command", cmd.Command), | ||
| zap.String("template_id", cmd.TemplateID)) | ||
| ecp.repositoryMgr.NotifyAgents(processCtx) | ||
| } else { | ||
| // Other producers (engine health pings, scan dispatch, | ||
| // etc.) legitimately share this queue. Ack-and-ignore. | ||
| ecp.logger.Debug("Unhandled engine.commands command", | ||
| zap.String("command", cmd.Command)) | ||
| } | ||
| } | ||
|
|
||
| queue.Listen(engineCommandsQueueName, processor) | ||
| ecp.logger.Info("engine.commands queue processor stopped") | ||
| }() | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Stop cancels the consumer's context. The underlying queue.Listen does | ||
| // not currently honour cancellation (matches TemplateSyncQueueProcessor), | ||
| // but we keep the same lifecycle shape so future plumbing is symmetric. | ||
| func (ecp *EngineCommandQueueProcessor) Stop() { | ||
| if ecp.cancelFunc != nil { | ||
| ecp.cancelFunc() | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package server | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestIsTemplateNotifyCommand(t *testing.T) { | ||
| cases := []struct { | ||
| cmd string | ||
| want bool | ||
| }{ | ||
| {"internal:template upload", true}, | ||
| {"internal:template delete", true}, | ||
| {"internal:template-scan --template foo", false}, | ||
| {"scan:start", false}, | ||
| {"", false}, | ||
| {"INTERNAL:template upload", false}, // case-sensitive on purpose; legacy producers always lowercase | ||
| } | ||
| for _, tc := range cases { | ||
| if got := isTemplateNotifyCommand(tc.cmd); got != tc.want { | ||
| t.Errorf("isTemplateNotifyCommand(%q) = %v, want %v", tc.cmd, got, tc.want) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestEngineCommandMessage_Unmarshal(t *testing.T) { | ||
| const payload = `{"command":"internal:template upload","template_id":"smoke-test","timestamp":"2026-04-22T00:00:00Z"}` | ||
| var msg EngineCommandMessage | ||
| if err := json.Unmarshal([]byte(payload), &msg); err != nil { | ||
| t.Fatalf("unmarshal: %v", err) | ||
| } | ||
| if msg.Command != "internal:template upload" { | ||
| t.Errorf("Command = %q", msg.Command) | ||
| } | ||
| if msg.TemplateID != "smoke-test" { | ||
| t.Errorf("TemplateID = %q", msg.TemplateID) | ||
| } | ||
| if !isTemplateNotifyCommand(msg.Command) { | ||
| t.Error("expected isTemplateNotifyCommand true for parsed upload command") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new consumer subscribes to
engine.commandsand explicitly treats unknown commands as "Ack-and-ignore"; on a shared RabbitMQ queue, that means non-template messages can be consumed here and silently dropped instead of reaching the component that actually handles them. The risk is highest in deployments where other command types (e.g. scan/health events noted in the comment) are published to the same queue, because those events may disappear intermittently once this service is running.Useful? React with 👍 / 👎.