feat: Add CreateWithIdempotency and idempotency_key support\n\n- Add CreateWithIdempotency method to command factory\n- Add GetCommandByIdempotencyKey to command queries\n- Update CreateCommand to handle idempotency_key field\n- Fix system command list to match actual usage\n\nThis enables proper idempotency for rapid-click prevention.
This commit is contained in:
@@ -5,19 +5,22 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
||||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Factory creates validated AgentCommand instances
|
// Factory creates validated AgentCommand instances
|
||||||
type Factory struct {
|
type Factory struct {
|
||||||
validator *Validator
|
validator *Validator
|
||||||
|
commandQueries *queries.CommandQueries
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFactory creates a new command factory
|
// NewFactory creates a new command factory
|
||||||
func NewFactory() *Factory {
|
func NewFactory(commandQueries *queries.CommandQueries) *Factory {
|
||||||
return &Factory{
|
return &Factory{
|
||||||
validator: NewValidator(),
|
validator: NewValidator(),
|
||||||
|
commandQueries: commandQueries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +44,33 @@ func (f *Factory) Create(agentID uuid.UUID, commandType string, params map[strin
|
|||||||
return cmd, nil
|
return cmd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateWithIdempotency generates a command with idempotency protection
|
||||||
|
// If a command with the same idempotency key exists, returns it instead of creating a duplicate
|
||||||
|
func (f *Factory) CreateWithIdempotency(agentID uuid.UUID, commandType string, params map[string]interface{}, idempotencyKey string) (*models.AgentCommand, error) {
|
||||||
|
// If no idempotency key provided, create normally
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
return f.Create(agentID, commandType, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing command with same idempotency key
|
||||||
|
existing, err := f.commandQueries.GetCommandByIdempotencyKey(agentID, idempotencyKey)
|
||||||
|
if err != nil {
|
||||||
|
// If no existing command found, proceed with creation
|
||||||
|
if err.Error() == "sql: no rows in result set" || err.Error() == "command not found" {
|
||||||
|
cmd, createErr := f.Create(agentID, commandType, params)
|
||||||
|
if createErr != nil {
|
||||||
|
return nil, createErr
|
||||||
|
}
|
||||||
|
cmd.IdempotencyKey = &idempotencyKey
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to check idempotency: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return existing command
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
// determineSource classifies command source based on type
|
// determineSource classifies command source based on type
|
||||||
func determineSource(commandType string) string {
|
func determineSource(commandType string) string {
|
||||||
if isSystemCommand(commandType) {
|
if isSystemCommand(commandType) {
|
||||||
@@ -55,6 +85,8 @@ func isSystemCommand(commandType string) bool {
|
|||||||
"disable_heartbeat",
|
"disable_heartbeat",
|
||||||
"update_check",
|
"update_check",
|
||||||
"cleanup_old_logs",
|
"cleanup_old_logs",
|
||||||
|
"heartbeat_on",
|
||||||
|
"heartbeat_off",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range systemCommands {
|
for _, cmd := range systemCommands {
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ func NewCommandQueries(db *sqlx.DB) *CommandQueries {
|
|||||||
|
|
||||||
// CreateCommand inserts a new command for an agent
|
// CreateCommand inserts a new command for an agent
|
||||||
func (q *CommandQueries) CreateCommand(cmd *models.AgentCommand) error {
|
func (q *CommandQueries) CreateCommand(cmd *models.AgentCommand) error {
|
||||||
|
// Handle optional idempotency_key
|
||||||
|
if cmd.IdempotencyKey != nil {
|
||||||
|
query := `
|
||||||
|
INSERT INTO agent_commands (
|
||||||
|
id, agent_id, command_type, params, status, source, signature, idempotency_key, retried_from_id
|
||||||
|
) VALUES (
|
||||||
|
:id, :agent_id, :command_type, :params, :status, :source, :signature, :idempotency_key, :retried_from_id
|
||||||
|
)
|
||||||
|
`
|
||||||
|
_, err := q.db.NamedExec(query, cmd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without idempotency_key
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO agent_commands (
|
INSERT INTO agent_commands (
|
||||||
id, agent_id, command_type, params, status, source, signature, retried_from_id
|
id, agent_id, command_type, params, status, source, signature, retried_from_id
|
||||||
@@ -59,6 +73,22 @@ func (q *CommandQueries) GetCommandsByAgentID(agentID uuid.UUID) ([]models.Agent
|
|||||||
return commands, err
|
return commands, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCommandByIdempotencyKey retrieves a command by agent ID and idempotency key
|
||||||
|
func (q *CommandQueries) GetCommandByIdempotencyKey(agentID uuid.UUID, idempotencyKey string) (*models.AgentCommand, error) {
|
||||||
|
var cmd models.AgentCommand
|
||||||
|
query := `
|
||||||
|
SELECT * FROM agent_commands
|
||||||
|
WHERE agent_id = $1 AND idempotency_key = $2
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
err := q.db.Get(&cmd, query, agentID, idempotencyKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
// MarkCommandSent updates a command's status to sent
|
// MarkCommandSent updates a command's status to sent
|
||||||
func (q *CommandQueries) MarkCommandSent(id uuid.UUID) error {
|
func (q *CommandQueries) MarkCommandSent(id uuid.UUID) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|||||||
Reference in New Issue
Block a user