初版功能完成
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EventLog struct {
|
||||
ID int64 `json:"id"`
|
||||
Time time.Time `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Module string `json:"module"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
SessionID uint64 `json:"session_id,omitempty,string"`
|
||||
Message string `json:"message"`
|
||||
FieldsJSON json.RawMessage `json:"fields"`
|
||||
}
|
||||
|
||||
type EventLogFilter struct {
|
||||
Level string
|
||||
Module string
|
||||
NodeID string
|
||||
SessionID uint64
|
||||
From time.Time
|
||||
To time.Time
|
||||
Limit int
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNodeTypesAreCompleteAndValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got, want := len(NodeTypes), 2; got != want {
|
||||
t.Fatalf("NodeTypes length = %d, want %d", got, want)
|
||||
}
|
||||
for _, nodeType := range NodeTypes {
|
||||
if !nodeType.Valid() {
|
||||
t.Fatalf("listed node type %q is invalid", nodeType)
|
||||
}
|
||||
}
|
||||
if _, err := ParseNodeType("server"); err == nil {
|
||||
t.Fatal("ParseNodeType accepted the Server, which is not a Windows Node role")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeStatusesAreCompleteAndValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, status := range []NodeStatus{NodeOnline, NodeUnstable, NodeOffline} {
|
||||
if !status.Valid() {
|
||||
t.Fatalf("listed Node status %q is invalid", status)
|
||||
}
|
||||
}
|
||||
if NodeStatus("UNKNOWN").Valid() {
|
||||
t.Fatal("unknown Node status accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionStatusesAreCompleteAndValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got, want := len(SessionStatuses), 7; got != want {
|
||||
t.Fatalf("SessionStatuses length = %d, want %d", got, want)
|
||||
}
|
||||
seen := make(map[SessionStatus]struct{}, len(SessionStatuses))
|
||||
for _, status := range SessionStatuses {
|
||||
if !status.Valid() {
|
||||
t.Fatalf("listed Session status %q is invalid", status)
|
||||
}
|
||||
if _, duplicate := seen[status]; duplicate {
|
||||
t.Fatalf("duplicate Session status %q", status)
|
||||
}
|
||||
seen[status] = struct{}{}
|
||||
}
|
||||
if _, err := ParseSessionStatus("RUNNING"); err == nil {
|
||||
t.Fatal("ParseSessionStatus accepted a non-spec state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionIDsUseLosslessJSONDecimalStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
session := Session{ID: math.MaxUint64, Status: SessionActive}
|
||||
raw, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"session_id":"18446744073709551615"`) {
|
||||
t.Fatalf("SessionID was not encoded losslessly for JavaScript clients: %s", raw)
|
||||
}
|
||||
var decoded Session
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil || decoded.ID != session.ID {
|
||||
t.Fatalf("SessionID round trip = %d, %v", decoded.ID, err)
|
||||
}
|
||||
eventRaw, err := json.Marshal(EventLog{SessionID: math.MaxUint64})
|
||||
if err != nil || !strings.Contains(string(eventRaw), `"session_id":"18446744073709551615"`) {
|
||||
t.Fatalf("Event SessionID was not encoded losslessly: %s, %v", eventRaw, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Package model contains transport-independent RemLink domain values.
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NodeType identifies the role of a RemLink Windows node.
|
||||
type NodeType string
|
||||
|
||||
const (
|
||||
NodeTypeEngineer NodeType = "engineer"
|
||||
NodeTypeSite NodeType = "site"
|
||||
)
|
||||
|
||||
// NodeTypes is the complete v1 node-role set.
|
||||
var NodeTypes = [...]NodeType{
|
||||
NodeTypeEngineer,
|
||||
NodeTypeSite,
|
||||
}
|
||||
|
||||
// Valid reports whether the node type is part of the v1 protocol.
|
||||
func (t NodeType) Valid() bool {
|
||||
switch t {
|
||||
case NodeTypeEngineer, NodeTypeSite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ParseNodeType parses and validates a wire/storage node type.
|
||||
func ParseNodeType(value string) (NodeType, error) {
|
||||
t := NodeType(value)
|
||||
if !t.Valid() {
|
||||
return "", fmt.Errorf("invalid node type %q", value)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// NodeStatus is the Server-observed application status of a Windows node.
|
||||
type NodeStatus string
|
||||
|
||||
const (
|
||||
NodeOnline NodeStatus = "ONLINE"
|
||||
NodeUnstable NodeStatus = "UNSTABLE"
|
||||
NodeOffline NodeStatus = "OFFLINE"
|
||||
)
|
||||
|
||||
// Valid reports whether the status is part of the v1 heartbeat model.
|
||||
func (s NodeStatus) Valid() bool {
|
||||
switch s {
|
||||
case NodeOnline, NodeUnstable, NodeOffline:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Node is the persistent Server-side identity and overlay allocation.
|
||||
type Node struct {
|
||||
ID string `json:"node_id"`
|
||||
Type NodeType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
OverlayIP netip.Addr `json:"overlay_ip"`
|
||||
WGPublicKey string `json:"wg_public_key"`
|
||||
NodeTokenHash []byte `json:"-"`
|
||||
Status NodeStatus `json:"status"`
|
||||
Version string `json:"version"`
|
||||
OSVersion string `json:"os_version"`
|
||||
LastSeen *time.Time `json:"last_seen,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SessionStatus is the Server-authoritative Remote Subnet Session state.
|
||||
type SessionStatus string
|
||||
|
||||
const (
|
||||
SessionCreating SessionStatus = "CREATING"
|
||||
SessionPreparingSite SessionStatus = "PREPARING_SITE"
|
||||
SessionReady SessionStatus = "READY"
|
||||
SessionActive SessionStatus = "ACTIVE"
|
||||
SessionStopping SessionStatus = "STOPPING"
|
||||
SessionClosed SessionStatus = "CLOSED"
|
||||
SessionFailed SessionStatus = "FAILED"
|
||||
)
|
||||
|
||||
// SessionStatuses is the complete v1 state set.
|
||||
var SessionStatuses = [...]SessionStatus{
|
||||
SessionCreating,
|
||||
SessionPreparingSite,
|
||||
SessionReady,
|
||||
SessionActive,
|
||||
SessionStopping,
|
||||
SessionClosed,
|
||||
SessionFailed,
|
||||
}
|
||||
|
||||
// Valid reports whether the status belongs to the v1 state machine.
|
||||
func (s SessionStatus) Valid() bool {
|
||||
switch s {
|
||||
case SessionCreating,
|
||||
SessionPreparingSite,
|
||||
SessionReady,
|
||||
SessionActive,
|
||||
SessionStopping,
|
||||
SessionClosed,
|
||||
SessionFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ParseSessionStatus parses and validates a wire/storage Session state.
|
||||
func ParseSessionStatus(value string) (SessionStatus, error) {
|
||||
status := SessionStatus(value)
|
||||
if !status.Valid() {
|
||||
return "", fmt.Errorf("invalid session status %q", value)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// SessionCounters are cumulative and use the Engineer point of view.
|
||||
type SessionCounters struct {
|
||||
UploadBytes uint64 `json:"upload_bytes"`
|
||||
DownloadBytes uint64 `json:"download_bytes"`
|
||||
UploadPackets uint64 `json:"upload_packets"`
|
||||
DownloadPackets uint64 `json:"download_packets"`
|
||||
}
|
||||
|
||||
// Session is the Server-authoritative persisted Remote Subnet lifecycle.
|
||||
type Session struct {
|
||||
ID uint64 `json:"session_id,string"`
|
||||
EngineerNodeID string `json:"engineer_node_id"`
|
||||
SiteNodeID string `json:"site_node_id"`
|
||||
Status SessionStatus `json:"status"`
|
||||
CIDRs []netip.Prefix `json:"cidrs"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ActiveAt *time.Time `json:"active_at,omitempty"`
|
||||
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
||||
ErrorCode string `json:"error_code,omitempty"`
|
||||
Counters SessionCounters `json:"counters"`
|
||||
}
|
||||
Reference in New Issue
Block a user