// Package messages defines the message types used for NATS communication // between deployment clients and listeners. package messages import ( "encoding/json" "fmt" "regexp" ) // Action represents a nixos-rebuild action. type Action string const ( ActionSwitch Action = "switch" ActionBoot Action = "boot" ActionTest Action = "test" ActionDryActivate Action = "dry-activate" ) // Valid returns true if the action is a recognized nixos-rebuild action. func (a Action) Valid() bool { switch a { case ActionSwitch, ActionBoot, ActionTest, ActionDryActivate: return true default: return false } } // Status represents the status of a deployment response. type Status string const ( StatusAccepted Status = "accepted" StatusRejected Status = "rejected" StatusStarted Status = "started" StatusCompleted Status = "completed" StatusFailed Status = "failed" ) // IsFinal returns true if this status indicates a terminal state. func (s Status) IsFinal() bool { switch s { case StatusCompleted, StatusFailed, StatusRejected: return true default: return false } } // ErrorCode represents an error condition. type ErrorCode string const ( ErrorInvalidRevision ErrorCode = "invalid_revision" ErrorInvalidAction ErrorCode = "invalid_action" ErrorAlreadyRunning ErrorCode = "already_running" ErrorBuildFailed ErrorCode = "build_failed" ErrorTimeout ErrorCode = "timeout" ) // DeployRequest is the message sent to request a deployment. type DeployRequest struct { Action Action `json:"action"` Revision string `json:"revision"` ReplyTo string `json:"reply_to"` } // revisionRegex validates git branch names and commit hashes. // Allows: alphanumeric, dashes, underscores, dots, slashes (for branch names), // and hex strings (for commit hashes). var revisionRegex = regexp.MustCompile(`^[a-zA-Z0-9._/-]+$`) // Validate checks that the request is valid. func (r *DeployRequest) Validate() error { if !r.Action.Valid() { return fmt.Errorf("invalid action: %q", r.Action) } if r.Revision == "" { return fmt.Errorf("revision is required") } if !revisionRegex.MatchString(r.Revision) { return fmt.Errorf("invalid revision format: %q", r.Revision) } if r.ReplyTo == "" { return fmt.Errorf("reply_to is required") } return nil } // Marshal serializes the request to JSON. func (r *DeployRequest) Marshal() ([]byte, error) { return json.Marshal(r) } // UnmarshalDeployRequest deserializes a request from JSON. func UnmarshalDeployRequest(data []byte) (*DeployRequest, error) { var r DeployRequest if err := json.Unmarshal(data, &r); err != nil { return nil, fmt.Errorf("failed to unmarshal deploy request: %w", err) } return &r, nil } // DeployResponse is the message sent in response to a deployment request. type DeployResponse struct { Hostname string `json:"hostname"` Status Status `json:"status"` Error *ErrorCode `json:"error"` Message string `json:"message"` } // NewDeployResponse creates a new response with the given hostname and status. func NewDeployResponse(hostname string, status Status, message string) *DeployResponse { return &DeployResponse{ Hostname: hostname, Status: status, Message: message, } } // WithError adds an error code to the response. func (r *DeployResponse) WithError(code ErrorCode) *DeployResponse { r.Error = &code return r } // Marshal serializes the response to JSON. func (r *DeployResponse) Marshal() ([]byte, error) { return json.Marshal(r) } // UnmarshalDeployResponse deserializes a response from JSON. func UnmarshalDeployResponse(data []byte) (*DeployResponse, error) { var r DeployResponse if err := json.Unmarshal(data, &r); err != nil { return nil, fmt.Errorf("failed to unmarshal deploy response: %w", err) } return &r, nil } // DiscoveryRequest is the message sent to discover available hosts. type DiscoveryRequest struct { ReplyTo string `json:"reply_to"` } // Validate checks that the request is valid. func (r *DiscoveryRequest) Validate() error { if r.ReplyTo == "" { return fmt.Errorf("reply_to is required") } return nil } // Marshal serializes the request to JSON. func (r *DiscoveryRequest) Marshal() ([]byte, error) { return json.Marshal(r) } // UnmarshalDiscoveryRequest deserializes a request from JSON. func UnmarshalDiscoveryRequest(data []byte) (*DiscoveryRequest, error) { var r DiscoveryRequest if err := json.Unmarshal(data, &r); err != nil { return nil, fmt.Errorf("failed to unmarshal discovery request: %w", err) } return &r, nil } // DiscoveryResponse is the message sent in response to a discovery request. type DiscoveryResponse struct { Hostname string `json:"hostname"` Tier string `json:"tier"` Role string `json:"role,omitempty"` DeploySubjects []string `json:"deploy_subjects"` } // Marshal serializes the response to JSON. func (r *DiscoveryResponse) Marshal() ([]byte, error) { return json.Marshal(r) } // UnmarshalDiscoveryResponse deserializes a response from JSON. func UnmarshalDiscoveryResponse(data []byte) (*DiscoveryResponse, error) { var r DiscoveryResponse if err := json.Unmarshal(data, &r); err != nil { return nil, fmt.Errorf("failed to unmarshal discovery response: %w", err) } return &r, nil }