9580f347c6
Implements Phase 2 of v0.1 Foundation:
- internal/model/node.go: Node struct with state machine (pending/ready/left)
- internal/store/store.go: SQLite open with WAL + foreign_keys pragmas
- internal/store/migrate.go: embedded SQL migration runner
- internal/store/migrations/0001_nodes.sql: nodes table schema
- internal/store/node_repo.go: CRUD operations for nodes
- internal/store/node_repo_test.go: 4 tests covering insert/get/list/update/delete
- internal/engine/registry.go: in-memory wrapper with slog audit logging
- internal/cli/node.go: orca node {join,leave,list} wired to registry
Verified: node join/list/leave work end-to-end, JSON output, slog audit logs,
state persists in SQLite, all tests pass with -race.
---ci---
project: orca
phase: 2
milestone: v0.1
status: execute
req_covered:
- REQ-002
- REQ-005
- REQ-008
- REQ-012
- REQ-017
- REQ-018
---/ci---
176 lines
4.2 KiB
Go
176 lines
4.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/engine"
|
|
"git.cloudinit.dev/coreci/orca/internal/model"
|
|
"git.cloudinit.dev/coreci/orca/internal/store"
|
|
)
|
|
|
|
func dbPath() string {
|
|
if p := os.Getenv("ORCA_DB"); p != "" {
|
|
return p
|
|
}
|
|
home, _ := os.UserHomeDir()
|
|
return filepath.Join(home, ".orca", "orca.db")
|
|
}
|
|
|
|
func openDB() (*sql.DB, func() error, error) {
|
|
db, err := store.Open(dbPath())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return db, db.Close, nil
|
|
}
|
|
|
|
func newLogger() *slog.Logger {
|
|
return slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
|
}
|
|
|
|
func nodeRegistry() (*engine.NodeRegistry, func() error, error) {
|
|
db, closer, err := openDB()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
repo := store.NewNodeRepo(db)
|
|
return engine.NewNodeRegistry(repo, newLogger()), closer, nil
|
|
}
|
|
|
|
var (
|
|
joinName string
|
|
joinAddr string
|
|
leaveID string
|
|
)
|
|
|
|
var nodeCmd = &cobra.Command{
|
|
Use: "node",
|
|
Short: "Manage orca nodes",
|
|
Long: "Join, leave, or list orca nodes in the registry.",
|
|
}
|
|
|
|
var nodeJoinCmd = &cobra.Command{
|
|
Use: "join",
|
|
Short: "Join a node to the orca registry",
|
|
Long: "Register a node in the local orca registry. Persisted to SQLite.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if joinName == "" {
|
|
return fmt.Errorf("--name is required")
|
|
}
|
|
if joinAddr == "" {
|
|
joinAddr = "localhost:8443"
|
|
}
|
|
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
registry, closer, err := nodeRegistry()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer()
|
|
|
|
node := &model.Node{
|
|
ID: uuid.NewString(),
|
|
Name: joinName,
|
|
Address: joinAddr,
|
|
State: model.NodeStateReady,
|
|
JoinedAt: time.Now().UTC(),
|
|
LastSeen: time.Now().UTC(),
|
|
}
|
|
if err := registry.Join(ctx, node); err != nil {
|
|
return err
|
|
}
|
|
if jsonOutput {
|
|
return printJSON(node)
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "✓ Node joined: %s (%s) at %s\n", node.ID, node.Name, node.Address)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var nodeLeaveCmd = &cobra.Command{
|
|
Use: "leave [node-id]",
|
|
Short: "Remove a node from the orca registry",
|
|
Long: "Mark a node as left. Use --id to specify, or pass as argument.",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
id := leaveID
|
|
if id == "" && len(args) > 0 {
|
|
id = args[0]
|
|
}
|
|
if id == "" {
|
|
return fmt.Errorf("node id required (use --id or pass as argument)")
|
|
}
|
|
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
registry, closer, err := nodeRegistry()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer()
|
|
|
|
if err := registry.Leave(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
if jsonOutput {
|
|
return printJSON(map[string]string{"id": id, "state": "left"})
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "✓ Node left: %s\n", id)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var nodeListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List all nodes in the orca registry",
|
|
Long: "Display all registered nodes and their state.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
registry, closer, err := nodeRegistry()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer()
|
|
|
|
nodes, err := registry.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if jsonOutput {
|
|
return printJSON(nodes)
|
|
}
|
|
if len(nodes) == 0 {
|
|
fmt.Fprintln(cmd.OutOrStdout(), "No nodes registered. Use 'orca node join' to add one.")
|
|
return nil
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%-36s %-20s %-22s %-10s\n", "ID", "NAME", "ADDRESS", "STATE")
|
|
for _, n := range nodes {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%-36s %-20s %-22s %-10s\n", n.ID, n.Name, n.Address, n.State)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
nodeJoinCmd.Flags().StringVar(&joinName, "name", "", "node name (required)")
|
|
nodeJoinCmd.Flags().StringVar(&joinAddr, "addr", "", "node address (default localhost:8443)")
|
|
nodeLeaveCmd.Flags().StringVar(&leaveID, "id", "", "node id")
|
|
|
|
nodeCmd.AddCommand(nodeJoinCmd)
|
|
nodeCmd.AddCommand(nodeLeaveCmd)
|
|
nodeCmd.AddCommand(nodeListCmd)
|
|
rootCmd.AddCommand(nodeCmd)
|
|
}
|