diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c080009..61f358e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,16 +58,22 @@ make test ```bash make ops + +# build development version that detects race conditions +make dev ``` ## Run ```bash ./build/ops + +# run development build +./build/ops-dev ``` - clear database file and log file ```bash -./build/ops clean +./build/ops clear ``` diff --git a/cli/commands/clear.go b/cli/commands/clear.go index a118eb0..755a47b 100644 --- a/cli/commands/clear.go +++ b/cli/commands/clear.go @@ -8,6 +8,9 @@ import ( "github.com/spf13/viper" ) +/** + * Command to remove database and log files + */ func clear() *cobra.Command { cmd := &cobra.Command{ Use: "clear", diff --git a/cli/commands/root.go b/cli/commands/root.go index ce0fff0..1150eac 100644 --- a/cli/commands/root.go +++ b/cli/commands/root.go @@ -6,12 +6,11 @@ import ( "github.com/spf13/cobra" ) +// CommandProps injected props that can be made available to all commands type CommandProps struct { UI *ui.UI } -// flag var to set verbose logging - // Root builds and returns our root command func Root(props *CommandProps) *cobra.Command { var verbose bool diff --git a/cli/main.go b/cli/main.go index e12051e..299b4df 100644 --- a/cli/main.go +++ b/cli/main.go @@ -13,6 +13,11 @@ import ( "github.com/spf13/viper" ) +/** + * Main entry point for all commands + * Here we setup environment config via viper + */ + func setRuntTimeConfig() error { userHomeDir, err := os.UserHomeDir() @@ -57,7 +62,7 @@ func setRuntTimeConfig() error { return nil } -// Entry point for the cli. +// Entry point for the cli func main() { log := logger.New() diff --git a/internal/config/interface.go b/internal/config/interface.go index 7f418d8..823d7db 100644 --- a/internal/config/interface.go +++ b/internal/config/interface.go @@ -48,6 +48,7 @@ type ConfigModel struct { Loaded time.Time `gorm:"index:,sort:desc"` } +// Repo interface representing access to stored configs type Repo interface { Get(id int) (*Config, error) GetAll() ([]*Config, error) @@ -58,6 +59,7 @@ type Repo interface { LastLoaded() (*Config, error) } +// Service interface for manipulating configurations type Service interface { Get(id int) (*Config, error) GetAll() ([]*Config, error) diff --git a/internal/config/repo.go b/internal/config/repo.go index 3d4abf8..229b35e 100644 --- a/internal/config/repo.go +++ b/internal/config/repo.go @@ -122,6 +122,7 @@ func (r *SqliteRepo) Delete(id int) error { return r.db.Delete(&ConfigModel{ID: id}).Error } +// SetLastLoaded updates a configs "loaded" field to the current timestamp func (r *SqliteRepo) SetLastLoaded(id int) error { confModel := ConfigModel{ID: id} diff --git a/internal/config/service.go b/internal/config/service.go index af65c7b..11e5a7d 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -1,37 +1,46 @@ package config +// ConfigService is an implementation of the config.Service interface type ConfigService struct { repo Repo } +// NewConfigService returns a new instance of ConfigService func NewConfigService(repo Repo) *ConfigService { return &ConfigService{repo: repo} } +// Get returns a config by id func (s *ConfigService) Get(id int) (*Config, error) { return s.repo.Get(id) } +// GetAll returns all stored configs func (s *ConfigService) GetAll() ([]*Config, error) { return s.repo.GetAll() } +// Create creates a new config func (s *ConfigService) Create(conf *Config) (*Config, error) { return s.repo.Create(conf) } +// Update updates an existing config func (s *ConfigService) Update(conf *Config) (*Config, error) { return s.repo.Update(conf) } +// Delete deletes a config func (s *ConfigService) Delete(id int) error { return s.repo.Delete(id) } +// SetLasLoaded sets the "loaded" field for a config to current timestamp func (s *ConfigService) SetLastLoaded(id int) error { return s.repo.SetLastLoaded(id) } +// LastLoaded retrieves the most recently loaded config func (s *ConfigService) LastLoaded() (*Config, error) { return s.repo.LastLoaded() } diff --git a/internal/core/core.go b/internal/core/core.go index a20cd64..3f348ea 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -11,17 +11,21 @@ import ( "github.com/robgonnella/ops/internal/server" ) +// EventListener represents a registered listener for database events type EventListener struct { id int channel chan *event.Event } +// ServerPollListener represents a registered listener for +// database server polling type ServerPollListener struct { id int channel chan []*server.Server } -// Core represents our core data structure +// Core represents our core data structure through which the ui can interact +// with the backend type Core struct { ctx context.Context cancel context.CancelFunc @@ -63,6 +67,9 @@ func New( } } +// Stop stops all processes managed by Core +// The core will be useless after calling stop, a new one must be +// instantiated to continue. func (c *Core) Stop() error { c.discovery.Stop() if c.eventSubscription != 0 { @@ -72,10 +79,12 @@ func (c *Core) Stop() error { return c.ctx.Err() } +// Conf return the currently loaded configuration func (c *Core) Conf() config.Config { return *c.conf } +// CreateConfig creates a new config in the database func (c *Core) CreateConfig(conf config.Config) error { _, err := c.configService.Create(&conf) @@ -86,6 +95,7 @@ func (c *Core) CreateConfig(conf config.Config) error { return nil } +// UpdateConfig updates an existing config in the database func (c *Core) UpdateConfig(conf config.Config) error { updated, err := c.configService.Update(&conf) @@ -102,6 +112,7 @@ func (c *Core) UpdateConfig(conf config.Config) error { return nil } +// SetConfig sets the current active configuration func (c *Core) SetConfig(id int) error { conf, err := c.configService.Get(id) @@ -118,18 +129,22 @@ func (c *Core) SetConfig(id int) error { return nil } +// DeleteConfig deletes a configuration func (c *Core) DeleteConfig(id int) error { return c.configService.Delete(id) } +// GetConfigs returns all stored configs func (c *Core) GetConfigs() ([]*config.Config, error) { return c.configService.GetAll() } +// StartDaemon starts the network monitoring processes in a goroutine func (c *Core) StartDaemon() { go c.Monitor() } +// RegisterEventListener registers a channel as a listener for database events func (c *Core) RegisterEventListener(channel chan *event.Event) int { c.mux.Lock() defer c.mux.Unlock() @@ -144,6 +159,8 @@ func (c *Core) RegisterEventListener(channel chan *event.Event) int { return listener.id } +// RemoveEventListener removes and closes a channel previously +// registered as a listener func (c *Core) RemoveEventListener(id int) { c.mux.Lock() defer c.mux.Unlock() @@ -161,6 +178,8 @@ func (c *Core) RemoveEventListener(id int) { c.evtListeners = listeners } +// RegisterServerPollListener registers a channel as a +// listener for server polling results func (c *Core) RegisterServerPollListener(channel chan []*server.Server) int { c.mux.Lock() defer c.mux.Unlock() @@ -176,6 +195,8 @@ func (c *Core) RegisterServerPollListener(channel chan []*server.Server) int { return listener.id } +// RemoveServerPollListener removes and closes a channel previously +// registered to listen for server polling results func (c *Core) RemoveServerPollListener(id int) { c.mux.Lock() defer c.mux.Unlock() diff --git a/internal/core/monitor.go b/internal/core/monitor.go index 0a7f484..b60a375 100644 --- a/internal/core/monitor.go +++ b/internal/core/monitor.go @@ -8,7 +8,8 @@ import ( "github.com/robgonnella/ops/internal/server" ) -// Run runs the sequence driver for the HostInstallStage +// Monitor starts the processes for monitoring and tracking +// devices on the configured network func (c *Core) Monitor() error { evtReceiveChan := make(chan *event.Event) @@ -35,6 +36,7 @@ func (c *Core) Monitor() error { } } +// handles server events from the database func (c *Core) handleServerEvent(evt *event.Event) { payload := evt.Payload.(*server.Server) @@ -57,6 +59,7 @@ func (c *Core) handleServerEvent(evt *event.Event) { } } +// polls database for all servers within configured network targets func (c *Core) pollForDatabaseUpdates() error { pollTime := time.Second * 2 errCount := 0 diff --git a/internal/discovery/ansible.go b/internal/discovery/ansible.go index fac4803..61cd38a 100644 --- a/internal/discovery/ansible.go +++ b/internal/discovery/ansible.go @@ -15,14 +15,17 @@ import ( "github.com/robgonnella/ops/internal/config" ) +// AnsibleIpScanner is an implementation of the DetailScanner interface type AnsibleIpScanner struct { conf config.Config } +// NewAnsibleIpScanner returns a new instance of AnsibleIpScanner func NewAnsibleIpScanner(conf config.Config) *AnsibleIpScanner { return &AnsibleIpScanner{conf: conf} } +// GetServerDetails returns server details using ansible's get-facts module func (s *AnsibleIpScanner) GetServerDetails(ctx context.Context, ip string) (*Details, error) { if err := os.Setenv(options.AnsibleHostKeyCheckingEnv, "False"); err != nil { return nil, err diff --git a/internal/discovery/interface.go b/internal/discovery/interface.go index 40c51af..dcead4d 100644 --- a/internal/discovery/interface.go +++ b/internal/discovery/interface.go @@ -4,15 +4,18 @@ import "context" //go:generate mockgen -destination=../mock/discovery/mock_discovery.go -package=mock_discovery . DetailScanner,Scanner +// DetailScanner interface for gathering more details about a device type DetailScanner interface { GetServerDetails(ctx context.Context, ip string) (*Details, error) } +// Scanner interface for scanning a network for devices type Scanner interface { Scan() ([]*DiscoveryResult, error) Stop() } +// Service interface for monitoring a network type Service interface { MonitorNetwork() Stop() diff --git a/internal/discovery/nmap.go b/internal/discovery/nmap.go index b1dc4a3..5266cbb 100644 --- a/internal/discovery/nmap.go +++ b/internal/discovery/nmap.go @@ -11,7 +11,7 @@ import ( "github.com/robgonnella/ops/internal/server" ) -// NmapScanner implements our discovery service using nmap +// NmapScanner is an implementation of the Scanner interface type NmapScanner struct { ctx context.Context cancel context.CancelFunc @@ -19,7 +19,7 @@ type NmapScanner struct { log logger.Logger } -// NewNmapScanner returns a new intance of nmap network discovery NmapScanner +// NewNmapScanner returns a new instance of NmapScanner func NewNmapScanner(targets []string) (*NmapScanner, error) { log := logger.New() @@ -48,7 +48,8 @@ func NewNmapScanner(targets []string) (*NmapScanner, error) { }, nil } -// Stop stop network discover +// Stop stops network scanning. Once called this scanner will be useless, +// a new one will need to be instantiated to continue scanning. func (s *NmapScanner) Stop() { s.cancel() } diff --git a/internal/discovery/result.go b/internal/discovery/result.go index 5e4cdf3..4b50e96 100644 --- a/internal/discovery/result.go +++ b/internal/discovery/result.go @@ -11,13 +11,17 @@ const ( UnknownDevice ) +// PortStatus represents possible port statuses type PortStatus string const ( - PortOpen PortStatus = "open" + // PortOpen status used when a port is marked open + PortOpen PortStatus = "open" + // PortClosed status used when a port is marked closed PortClosed PortStatus = "closed" ) +// Port data structure representing a server port type Port struct { ID uint16 Status PortStatus @@ -33,6 +37,7 @@ type DiscoveryResult struct { Ports []Port } +// Details represents the details returned by DetailScanner type Details struct { Hostname string OS string diff --git a/internal/discovery/service.go b/internal/discovery/service.go index 443c0a9..6f1fafc 100644 --- a/internal/discovery/service.go +++ b/internal/discovery/service.go @@ -8,7 +8,7 @@ import ( "github.com/robgonnella/ops/internal/server" ) -// ScannerService implements our discovery service using nmap +// ScannerService implements the Service interface for monitoring a network type ScannerService struct { ctx context.Context cancel context.CancelFunc @@ -18,8 +18,12 @@ type ScannerService struct { log logger.Logger } -// NewScannerService returns a new intance of nmap network discovery ScannerService -func NewScannerService(scanner Scanner, detailScanner DetailScanner, serverService server.Service) *ScannerService { +// NewScannerService returns a new instance of ScannerService +func NewScannerService( + scanner Scanner, + detailScanner DetailScanner, + serverService server.Service, +) *ScannerService { log := logger.New() // Use a cancelable context so we can properly cleanup when needed @@ -35,7 +39,7 @@ func NewScannerService(scanner Scanner, detailScanner DetailScanner, serverServi } } -// MonitorNetwork polls the network and calls out to grpc with the results +// MonitorNetwork polls the network to discover and track devices func (s *ScannerService) MonitorNetwork() { s.log.Info().Msg("Starting network discovery") @@ -43,14 +47,15 @@ func (s *ScannerService) MonitorNetwork() { s.pollNetwork() } -// Stop stop network discover +// Stop stop network discover. Once called this service will be useless. +// A new one must be instantiated to continue func (s *ScannerService) Stop() { s.scanner.Stop() s.cancel() } // private -// pollNetwork runs Discover function on an interval to discover devices on the network +// make polling calls to scanner.Scan() func (s *ScannerService) pollNetwork() { pollTime := time.Second * 30 @@ -78,6 +83,7 @@ func (s *ScannerService) pollNetwork() { } } +// handle results found during polling func (s *ScannerService) handleDiscoveryResults(results []*DiscoveryResult) { for _, result := range results { deviceType := s.getDeviceType(result) @@ -113,6 +119,7 @@ func (s *ScannerService) handleDiscoveryResults(results []*DiscoveryResult) { } } +// if port 22 is detected then we can assume its a server func (s *ScannerService) getDeviceType(result *DiscoveryResult) DeviceType { for _, port := range result.Ports { if port.ID == 22 { @@ -123,6 +130,7 @@ func (s *ScannerService) getDeviceType(result *DiscoveryResult) DeviceType { return UnknownDevice } +// checks if port is 22 and whether the port is open or closed func (s *ScannerService) getSSHStatus(result *DiscoveryResult) server.SSHStatus { for _, port := range result.Ports { if port.ID == 22 { @@ -137,11 +145,8 @@ func (s *ScannerService) getSSHStatus(result *DiscoveryResult) server.SSHStatus return server.SSHDisabled } -// makes a call to our grpc server with a request to update the servers status -// to online. We also checke that server's associated services and update each -// service's status accordingly. +// makes a call to our server service to update the servers status to online func (s *ScannerService) setServerToOnline(result *DiscoveryResult) { - // finally update our server to "online" sshStatus := s.getSSHStatus(result) if sshStatus == server.SSHEnabled { @@ -186,6 +191,7 @@ func (s *ScannerService) setServerToOnline(result *DiscoveryResult) { } } +// makes a call to our server service to set a device to "offline" func (s *ScannerService) setServerToOffline(result *DiscoveryResult) { s.log.Info().Str("ip", result.IP).Msg("Marking device offline") if err := s.serverService.MarkServerOffline(result.IP); err != nil { diff --git a/internal/event/event.go b/internal/event/event.go index 4ff1762..aae3c04 100644 --- a/internal/event/event.go +++ b/internal/event/event.go @@ -1,11 +1,14 @@ package event +// EventType represents the possible types of events type EventType string const ( + // SeverUpdate event type representing a server update SeverUpdate EventType = "SERVER_UPDATE" ) +// Event data structure representing any event we may want to react to type Event struct { Type EventType Payload any diff --git a/internal/exception/error.go b/internal/exception/error.go index f8a3c82..9976b77 100644 --- a/internal/exception/error.go +++ b/internal/exception/error.go @@ -2,5 +2,5 @@ package exception import "errors" -// ErrNotFound custom database error for failure to find record +// ErrRecordNotFound custom database error for failure to find record var ErrRecordNotFound = errors.New("record not found") diff --git a/internal/logger/logger.go b/internal/logger/logger.go index bd027ad..03059bb 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -6,12 +6,16 @@ import ( "github.com/rs/zerolog" ) +// Logger our internal "singleton" wrapper around zerolog allowing us +// to set all loggers to log to file or console all at once type Logger struct { zl *zerolog.Logger } +// unexported "singleton" logger var logger Logger +// init sets the internal "singleton" logger func init() { zl := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}). With(). @@ -24,40 +28,39 @@ func init() { } } +// New returns the internal "singleton" logger func New() Logger { return logger } -func GlobalSetLogFile(filePath string) error { - f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - - if err != nil { - return err - } - +// GlobalSetLogFile set all loggers to log to file +func GlobalSetLogFile(f *os.File) { newZl := logger.zl.Output(f) *logger.zl = newZl - - return nil } +// Info wrapper around zerolog Info func (l Logger) Info() *zerolog.Event { return l.zl.Info() } +// Debug wrapper around zerolog Debug func (l Logger) Debug() *zerolog.Event { return l.zl.Debug() } +// Warn wrapper around zerolog Warn func (l Logger) Warn() *zerolog.Event { return l.zl.Warn() } +// Error wrapper around zerolog Error func (l Logger) Error() *zerolog.Event { return l.zl.Error() } +// Fatal wrapper around zerolog Fatal func (l Logger) Fatal() *zerolog.Event { return l.zl.Fatal() } diff --git a/internal/name/name.go b/internal/name/name.go index 5eb68a0..ae82d09 100644 --- a/internal/name/name.go +++ b/internal/name/name.go @@ -1,3 +1,5 @@ package name +// APP_NAME intended to reduce direct references to the app name +// in case we ever decide to change the name const APP_NAME = "ops" diff --git a/internal/server/interface.go b/internal/server/interface.go index c6e1712..187b2eb 100644 --- a/internal/server/interface.go +++ b/internal/server/interface.go @@ -4,17 +4,26 @@ import "github.com/robgonnella/ops/internal/event" //go:generate mockgen -destination=../mock/server/mock_server.go -package=mock_server . Repo,Service +// Status represents possible server statues type Status string + +// SSHStatus represents possible server ssh statuses type SSHStatus string const ( - StatusUnknown Status = "unknown" - StatusOnline Status = "online" - StatusOffline Status = "offline" - SSHEnabled SSHStatus = "enabled" - SSHDisabled SSHStatus = "disabled" + // StatusUnknown unknown status for server + StatusUnknown Status = "unknown" + // StatusOnline status if server is online + StatusOnline Status = "online" + // StatusOffline status if server is offline + StatusOffline Status = "offline" + // SSHEnabled status when server has ssh enabled + SSHEnabled SSHStatus = "enabled" + // SSHDisabled status when server has ssh disabled + SSHDisabled SSHStatus = "disabled" ) +// Server database model for a server type Server struct { ID string `gorm:"primaryKey"` Status Status @@ -24,6 +33,7 @@ type Server struct { SshStatus SSHStatus } +// Repo interface for accessing stored servers type Repo interface { GetAllServers() ([]*Server, error) GetServerByID(serverID string) (*Server, error) @@ -33,6 +43,7 @@ type Repo interface { RemoveServer(id string) error } +// Service interface for server related logic type Service interface { GetAllServers() ([]*Server, error) GetAllServersInNetworkTargets(targets []string) ([]*Server, error) diff --git a/internal/server/repo.go b/internal/server/repo.go index 498286d..cb839ce 100644 --- a/internal/server/repo.go +++ b/internal/server/repo.go @@ -7,7 +7,7 @@ import ( "gorm.io/gorm" ) -// SqliteRepo is our repo implementation for sqlite +// SqliteRepo is our Repo implementation for sqlite type SqliteRepo struct { db *gorm.DB } @@ -58,6 +58,7 @@ func (r *SqliteRepo) GetServerByIP(ip string) (*Server, error) { return &server, nil } +// AddServer add a new server to the database func (r *SqliteRepo) AddServer(server *Server) (*Server, error) { if server.ID == "" { return nil, errors.New("server id cannot be empty") @@ -70,6 +71,7 @@ func (r *SqliteRepo) AddServer(server *Server) (*Server, error) { return server, nil } +// RemoveServer removes a server func (r *SqliteRepo) RemoveServer(id string) error { if id == "" { return errors.New("server id cannot be empty") @@ -78,6 +80,7 @@ func (r *SqliteRepo) RemoveServer(id string) error { return r.db.Delete(&Server{ID: id}).Error } +// UpdateServer updates an existing server func (r *SqliteRepo) UpdateServer(server *Server) (*Server, error) { if server.ID == "" { return nil, errors.New("server id cannot be empty") diff --git a/internal/server/service.go b/internal/server/service.go index 6b9bef2..627e2b1 100644 --- a/internal/server/service.go +++ b/internal/server/service.go @@ -12,18 +12,22 @@ import ( "github.com/robgonnella/ops/internal/logger" ) +// internal var for tracking event listeners var channelID = 0 +// generates sequential ids for registered event listeners func nextChannelID() int { channelID++ return channelID } +// represents a registered event listener type eventChannel struct { id int send chan *event.Event } +// helper for filtering registered event listeners func filterChannels(channels []*eventChannel, fn func(c *eventChannel) bool) []*eventChannel { modifiedChannels := []*eventChannel{} for _, evtChan := range channels { @@ -35,7 +39,7 @@ func filterChannels(channels []*eventChannel, fn func(c *eventChannel) bool) []* return modifiedChannels } -// ServerService represents our server service implementation +// ServerService represents our server.Service implementation type ServerService struct { log logger.Logger repo Repo @@ -43,7 +47,7 @@ type ServerService struct { mux sync.Mutex } -// NewService returns a new instance server service +// NewService returns a new instance ServerService func NewService(conf config.Config, repo Repo) *ServerService { log := logger.New() @@ -60,6 +64,8 @@ func (s *ServerService) GetAllServers() ([]*Server, error) { return s.repo.GetAllServers() } +// GetAllServersInNetworkTargets returns all servers in database that have ips +// within the provided list of network targets func (s *ServerService) GetAllServersInNetworkTargets(targets []string) ([]*Server, error) { allServers, err := s.GetAllServers() @@ -166,7 +172,7 @@ func (s *ServerService) MarkServerOffline(ip string) error { return err } -// StreamEvents streams server updates to client +// StreamEvents registers a listener for database updates func (s *ServerService) StreamEvents(send chan *event.Event) int { evtChan := &eventChannel{ id: nextChannelID(), @@ -180,6 +186,7 @@ func (s *ServerService) StreamEvents(send chan *event.Event) int { return evtChan.id } +// StopStream removes and closes channel for a specific registered listener func (s *ServerService) StopStream(id int) { s.mux.Lock() defer s.mux.Unlock() @@ -198,6 +205,7 @@ func (s *ServerService) GetServer(id string) (*Server, error) { return s.repo.GetServerByID(id) } +// sends out server update events to all registered listeners func (s *ServerService) sendServerUpdateEvent(server *Server) { s.mux.Lock() defer s.mux.Unlock() diff --git a/internal/ui/component/configure.go b/internal/ui/component/configure.go index 978dc86..c87a9cc 100644 --- a/internal/ui/component/configure.go +++ b/internal/ui/component/configure.go @@ -9,6 +9,7 @@ import ( "github.com/robgonnella/ops/internal/ui/style" ) +// ConfigureForm component for updating and creating configurations type ConfigureForm struct { root *tview.Form configName *tview.InputField @@ -23,6 +24,7 @@ type ConfigureForm struct { creatingNewConfig bool } +// adds blank form inputs and sets styling func addBlankFormItems( form *tview.Form, confName string, @@ -58,6 +60,7 @@ func addBlankFormItems( return configName, sshUserInput, sshIdentityInput, cidrInput } +// every time the add ssh override button is clicked we add three new inputs func createOverrideInputs() (*tview.InputField, *tview.InputField, *tview.InputField) { overrideTarget := tview.NewInputField() overrideTarget.SetLabel("Override Target: ") @@ -71,6 +74,7 @@ func createOverrideInputs() (*tview.InputField, *tview.InputField, *tview.InputF return overrideTarget, overrideSSHUser, overrideSSHIdentity } +// NewConfigureForm returns a new instance of ConfigureForm func NewConfigureForm( conf config.Config, onUpdate func(conf config.Config), @@ -99,11 +103,13 @@ func NewConfigureForm( } } +// Primitive return the root primitive for ConfigureForm func (f *ConfigureForm) Primitive() tview.Primitive { f.render() return f.root } +// preloads all info based on current active configuration (context) func (f *ConfigureForm) render() { f.root.Clear(true) f.overrides = []map[string]*tview.InputField{} @@ -137,6 +143,7 @@ func (f *ConfigureForm) render() { f.addFormButtons() } +// adds buttons to form func (f *ConfigureForm) addFormButtons() { f.root.AddButton("Cancel", func() { if f.creatingNewConfig { diff --git a/internal/ui/component/context.go b/internal/ui/component/context.go index 12944f8..6c73faf 100644 --- a/internal/ui/component/context.go +++ b/internal/ui/component/context.go @@ -12,10 +12,12 @@ import ( "github.com/robgonnella/ops/internal/ui/style" ) +// ConfigContext table selecting and deleting contexts (configurations) type ConfigContext struct { root *tview.Table } +// NewConfigContext returns a new instance of NewConfigContext func NewConfigContext( current int, confs []*config.Config, @@ -67,6 +69,7 @@ func NewConfigContext( return c } +// UpdateConfigs updates the table with a new list of contexts func (c *ConfigContext) UpdateConfigs(current int, confs []*config.Config) { c.clearRows() @@ -103,10 +106,12 @@ func (c *ConfigContext) UpdateConfigs(current int, confs []*config.Config) { } } +// Primitive returns the root primitive for ConfigContext func (c *ConfigContext) Primitive() tview.Primitive { return c.root } +// removes all row from table func (c *ConfigContext) clearRows() { count := c.root.GetRowCount() diff --git a/internal/ui/component/event.go b/internal/ui/component/event.go index fcf3b3f..2f2db61 100644 --- a/internal/ui/component/event.go +++ b/internal/ui/component/event.go @@ -9,6 +9,7 @@ import ( "github.com/robgonnella/ops/internal/ui/style" ) +// EvenTable table for viewing all incoming events in realtime type EventTable struct { table *tview.Table columnHeaders []string @@ -16,6 +17,7 @@ type EventTable struct { maxEvents uint } +// NewEventTable returns a new instance of EventTable func NewEventTable() *EventTable { columnHeaders := []string{ "NO", @@ -36,10 +38,13 @@ func NewEventTable() *EventTable { } } +// Primitive returns the root primitive for EventTable func (t *EventTable) Primitive() tview.Primitive { return t.table } +// UpdateTable adds a new event to the table and removes oldest row if we've +// reached configured maximum for events to display func (t *EventTable) UpdateTable(evt *event.Event) { t.count++ evtType := string(evt.Type) @@ -65,6 +70,7 @@ func (t *EventTable) UpdateTable(evt *event.Event) { t.table.SetCell(rowIdx, col, cell) } + // remove oldest row if max reached if t.count > t.maxEvents { t.table.RemoveRow(2) } diff --git a/internal/ui/component/header.go b/internal/ui/component/header.go index c54be18..d6d30c1 100644 --- a/internal/ui/component/header.go +++ b/internal/ui/component/header.go @@ -16,6 +16,7 @@ const appText = ` ╚██████╔╝██║ ███████║ ╚═════╝ ╚═╝ ╚══════╝` +// Header shown above all views. Includes app title and dynamic key legend type Header struct { root *tview.Flex legendContainer *tview.Flex @@ -26,7 +27,12 @@ type Header struct { extraLegendMap map[string]tview.Primitive } -func NewHeader(userIP string, targets []string, onViewSwitch func(text string)) *Header { +// NewHeader returns a new instance of Header +func NewHeader( + userIP string, + targets []string, + onViewSwitch func(text string), +) *Header { h := &Header{} h.root = tview.NewFlex().SetDirection(tview.FlexRow) @@ -83,10 +89,12 @@ func NewHeader(userIP string, targets []string, onViewSwitch func(text string)) return h } +// Primitive returns the root primitive for Header func (h *Header) Primitive() tview.Primitive { return h.root } +// AddLegendKey adds a new key and description to the legend func (h *Header) AddLegendKey(key, description string) { v := tview.NewTextView(). SetText(fmt.Sprintf("\"%s\" - %s", key, description)). @@ -98,6 +106,7 @@ func (h *Header) AddLegendKey(key, description string) { h.legendCol2.AddItem(v, 0, 1, false) } +// RemoveLegendKey removes key and description from legend func (h *Header) RemoveLegendKey(key string) { for k, primitive := range h.extraLegendMap { if k == key { @@ -107,6 +116,8 @@ func (h *Header) RemoveLegendKey(key string) { } } +// RemoveAllExtraLegendKeys removes all non-default keys and descriptions +// from legend func (h *Header) RemoveAllExtraLegendKeys() { for k, primitive := range h.extraLegendMap { h.legendCol2.RemoveItem(primitive) @@ -114,6 +125,7 @@ func (h *Header) RemoveAllExtraLegendKeys() { } } +// SwitchViewInput returns access to the Header's SwitchViewInput component func (h *Header) SwitchViewInput() *SwitchViewInput { return h.switchViewInput } diff --git a/internal/ui/component/modal.go b/internal/ui/component/modal.go index 8b942e2..fccefca 100644 --- a/internal/ui/component/modal.go +++ b/internal/ui/component/modal.go @@ -5,15 +5,18 @@ import ( "github.com/robgonnella/ops/internal/ui/style" ) +// ModalButton represents a button added to a modal type ModalButton struct { Label string OnClick func() } +// Modal generic structure for displaying modals type Modal struct { root *tview.Modal } +// NewModal returns a new instance of Modal func NewModal(message string, buttons []ModalButton) *Modal { modal := tview.NewModal() @@ -50,6 +53,7 @@ func NewModal(message string, buttons []ModalButton) *Modal { } } +// Primitive returns the root primitive for Modal func (m *Modal) Primitive() tview.Primitive { return m.root } diff --git a/internal/ui/component/server.go b/internal/ui/component/server.go index e6bdad0..9fd7b47 100644 --- a/internal/ui/component/server.go +++ b/internal/ui/component/server.go @@ -8,11 +8,13 @@ import ( "github.com/robgonnella/ops/internal/ui/style" ) +// ServerTable table displaying all servers for the active context type ServerTable struct { table *tview.Table columnHeaders []string } +// NewServerTable returns a new instance of ServerTable func NewServerTable(OnSSH func(ip string)) *ServerTable { columnHeaders := []string{"HOSTNAME", "IP", "ID", "OS", "SSH", "STATUS"} @@ -35,10 +37,14 @@ func NewServerTable(OnSSH func(ip string)) *ServerTable { } } +// Primitive returns the root primitive for ServerTable func (t *ServerTable) Primitive() tview.Primitive { return t.table } +// UpdateTable updates the table with the incoming list of servers from +// the database. We expect these servers to always be sorted so the ordering +// should remain relatively consistent. func (t *ServerTable) UpdateTable(servers []*server.Server) { for rowIdx, svr := range servers { status := string(svr.Status) diff --git a/internal/ui/component/switch-view-input.go b/internal/ui/component/switch-view-input.go index 0c805e0..9ea5423 100644 --- a/internal/ui/component/switch-view-input.go +++ b/internal/ui/component/switch-view-input.go @@ -7,12 +7,14 @@ import ( "github.com/robgonnella/ops/internal/ui/style" ) +// SwitchViewInput toggle-able input for switching views type SwitchViewInput struct { root *tview.InputField showing bool onSubmit func(text string) } +// NewSwitchViewInput returns a new instance of SwitchViewInput func NewSwitchViewInput(onSubmit func(text string)) *SwitchViewInput { input := tview.NewInputField() @@ -20,12 +22,14 @@ func NewSwitchViewInput(onSubmit func(text string)) *SwitchViewInput { input.SetBorderPadding(0, 0, 1, 1) input.SetPlaceholderStyle(style.StyleDefault.Dim(true)) + // Show when focused input.SetFocusFunc(func() { input.SetBorder(true) input.SetBorderColor(style.ColorPurple) input.SetPlaceholder("Enter view: servers, events, context, configure") }) + // hide when blurred input.SetBlurFunc(func() { input.SetBorder(false) input.SetPlaceholder("") @@ -37,6 +41,7 @@ func NewSwitchViewInput(onSubmit func(text string)) *SwitchViewInput { onSubmit: onSubmit, } + // submit and then clear text when user presses enter ai.root.SetDoneFunc(func(k tcell.Key) { if k == key.KeyEnter { ai.onSubmit(ai.root.GetText()) @@ -47,6 +52,7 @@ func NewSwitchViewInput(onSubmit func(text string)) *SwitchViewInput { return ai } +// Primitive returns the root primitive for SwitchViewInput func (i *SwitchViewInput) Primitive() tview.Primitive { return i.root } diff --git a/internal/ui/component/table.go b/internal/ui/component/table.go index bd58971..324e59a 100644 --- a/internal/ui/component/table.go +++ b/internal/ui/component/table.go @@ -6,6 +6,7 @@ import ( "github.com/robgonnella/ops/internal/ui/style" ) +// helper for creating consistently styled tables func createTable(title string, columnHeaders []string) *tview.Table { table := tview.NewTable(). SetBorders(false). diff --git a/internal/ui/key/key.go b/internal/ui/key/key.go index 4ea20b8..a3e18f7 100644 --- a/internal/ui/key/key.go +++ b/internal/ui/key/key.go @@ -2,6 +2,10 @@ package key import "github.com/gdamore/tcell/v2" +/** + * Keys and Runes! + */ + const ( RuneColon = ':' ) diff --git a/internal/ui/launch.go b/internal/ui/launch.go index e90ee5f..f7d94c4 100644 --- a/internal/ui/launch.go +++ b/internal/ui/launch.go @@ -10,22 +10,28 @@ import ( "github.com/spf13/viper" ) +// capture original stdout & stderr to be restored when ssh-ing to a server var originalStdout = os.Stdout var originalStderr = os.Stderr +// restores original stdout & stderr func restoreStdout() { os.Stdout = originalStdout os.Stderr = originalStderr } +// UI wrapper around view used for initial launch type UI struct { view *view } +// NewUI returns a new instance of UI func NewUI() *UI { return &UI{} } +// Launch configures logging, creates a new instance of view and launches +// our terminal UI application func (u *UI) Launch() error { log := logger.New() @@ -41,13 +47,20 @@ func (u *UI) Launch() error { log.Info().Msg("disabling logs") zerolog.SetGlobalLevel(zerolog.Disabled) } else { - if err := logger.GlobalSetLogFile(logFile); err != nil { - log.Error().Err(err) + file, err := os.OpenFile( + logFile, + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0644, + ) + + if err != nil { + log.Error().Err(err).Msg("") log.Info().Msg("disabling logs") zerolog.SetGlobalLevel(zerolog.Disabled) + } else { + logger.GlobalSetLogFile(file) } } - } userIP, cidr, err := util.GetNetworkInfo() diff --git a/internal/ui/style/color.go b/internal/ui/style/color.go index c91ecf2..5011c08 100644 --- a/internal/ui/style/color.go +++ b/internal/ui/style/color.go @@ -2,6 +2,10 @@ package style import "github.com/gdamore/tcell/v2" +/** + * Styles and Colors! + */ + const ( ColorDefault = tcell.ColorDefault ColorBlack = tcell.ColorBlack diff --git a/internal/ui/view.go b/internal/ui/view.go index 7835dd9..cc4800d 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -20,14 +20,19 @@ import ( "github.com/robgonnella/ops/internal/util" ) -type ViewOption func(v *view) +// viewOption provides a way to modify our view during initialization +// this is helpful when restarting the view and focusing a specific page +type viewOption func(v *view) -func WithFocusedView(name string) ViewOption { +// withFocusedView sets the option to focus a specific view +// during initialization +func withFocusedView(name string) viewOption { return func(v *view) { v.focusedName = name } } +// data structure for managing our entire terminal ui application type view struct { app *tview.Application root *tview.Flex @@ -50,6 +55,7 @@ type view struct { log logger.Logger } +// returns a new instance of view func newView(userIP string, allConfigs []*config.Config, appCore *core.Core) *view { log := logger.New() @@ -63,10 +69,11 @@ func newView(userIP string, allConfigs []*config.Config, appCore *core.Core) *vi return v } +// initializes the terminal ui application func (v *view) initialize( userIP string, allConfigs []*config.Config, - options ...ViewOption, + options ...viewOption, ) { v.viewNames = []string{"servers", "events", "context", "configure"} v.showingSwitchViewInput = false @@ -117,6 +124,7 @@ func (v *view) initialize( v.focus(v.focusedName) } +// change view based on result from switch view input func (v *view) onActionSubmit(text string) { focusedName := "" @@ -131,10 +139,12 @@ func (v *view) onActionSubmit(text string) { v.showingSwitchViewInput = false } +// dismisses configuration form - focuses previously focused view func (v *view) onDismissConfigureForm() { v.onActionSubmit(v.prevFocusedName) } +// updates current config with result from config form inputs func (v *view) onConfigureFormUpdate(conf config.Config) { if reflect.DeepEqual(conf, v.appCore.Conf()) { v.onDismissConfigureForm() @@ -147,9 +157,10 @@ func (v *view) onConfigureFormUpdate(conf config.Config) { return } - v.restart(WithFocusedView("context")) + v.restart(withFocusedView("context")) } +// creates a new config with results from config form inputs func (v *view) onConfigureFormCreate(conf config.Config) { if err := v.appCore.CreateConfig(conf); err != nil { v.log.Error().Err(err).Msg("failed to create config") @@ -170,6 +181,7 @@ func (v *view) onConfigureFormCreate(conf config.Config) { v.focus("context") } +// selects a new context for network scanning func (v *view) onContextSelect(id int) { if err := v.appCore.SetConfig(id); err != nil { v.log.Error().Err(err).Msg("failed to set new context") @@ -180,11 +192,13 @@ func (v *view) onContextSelect(id int) { v.restart() } +// dismisses confirmation modal when deleting a context func (v *view) dismissContextDelete() { v.contextToDelete = 0 v.app.SetRoot(v.root, true) } +// shows confirmation modal when attempting to delete a context func (v *view) onContextDelete(name string, id int) { v.contextToDelete = id buttons := []component.ModalButton{ @@ -204,6 +218,7 @@ func (v *view) onContextDelete(name string, id int) { v.app.SetRoot(contextDeleteConfirm.Primitive(), false) } +// deletes a network scanning context (configuration) func (v *view) deleteContext() { if v.contextToDelete == 0 { return @@ -223,7 +238,7 @@ func (v *view) deleteContext() { if v.contextToDelete == currentConfig { // deleted current context - restart app - v.restart(WithFocusedView("context")) + v.restart(withFocusedView("context")) } else { confs, err := v.appCore.GetConfigs() @@ -238,6 +253,7 @@ func (v *view) deleteContext() { } } +// displays an error modal func (v *view) showErrorModal(message string) { buttons := []component.ModalButton{ { @@ -252,10 +268,12 @@ func (v *view) showErrorModal(message string) { v.app.SetRoot(errorModal.Primitive(), false) } +// dismisses an error modal func (v *view) dismissErrorModal() { v.app.SetRoot(v.root, true) } +// binds global key handlers func (v *view) bindKeys() { v.app.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { switch evt.Key() { @@ -285,6 +303,8 @@ func (v *view) bindKeys() { }) } +// focuses a given view by name and updates the legend to display the correct +// key mappings for that view func (v *view) focus(name string) { p := v.getFocusNamePrimitive(name) @@ -321,9 +341,14 @@ func (v *view) focus(name string) { v.app.SetFocus(p) } +// Attempts to ssh to the given server using current config's ssh properties. +// This requires stopping the terminal ui application so we can return +// to the normal terminal screen. We ensure our terminal app is restarted +// once the ssh command finishes aka when the user exists the ssh tunnel. func (v *view) onSSH(ip string) { v.stop() + // ensure we restart our tui app defer v.restart() conf := v.appCore.Conf() @@ -354,6 +379,7 @@ func (v *view) onSSH(ip string) { cmd.Run() } +// handle incoming server results from database polling func (v *view) processBackgroundServerUpdates() { go func() { for { @@ -377,6 +403,7 @@ func (v *view) processBackgroundServerUpdates() { }() } +// handle incoming database events func (v *view) processBackgroundEventUpdates() { go func() { for { @@ -391,6 +418,7 @@ func (v *view) processBackgroundEventUpdates() { }() } +// maps names to primitives for focusing func (v *view) getFocusNamePrimitive(name string) tview.Primitive { switch name { case "servers": @@ -406,6 +434,8 @@ func (v *view) getFocusNamePrimitive(name string) tview.Primitive { } } +// completely stops the tui app and all backend processes. +// this requires a full restart including re-instantiation of entire backend func (v *view) stop() { v.appCore.RemoveServerPollListener(v.serverPollListenerID) v.appCore.RemoveEventListener(v.eventListenerID) @@ -415,7 +445,8 @@ func (v *view) stop() { v.app.Stop() } -func (v *view) restart(options ...ViewOption) { +// restarts the entire application including re-instantiation of entire backend +func (v *view) restart(options ...viewOption) { v.stop() userIP, cidr, err := util.GetNetworkInfo() @@ -449,6 +480,8 @@ func (v *view) restart(options ...ViewOption) { } } +// sets up global key handlers, registers event listeners, sets up processing +// for channel updates, starts backend processes, and starts terminal ui func (v *view) run() error { v.bindKeys() v.serverPollListenerID = v.appCore.RegisterServerPollListener( diff --git a/internal/util/slice.go b/internal/util/slice.go index 0a8189b..e2d3d56 100644 --- a/internal/util/slice.go +++ b/internal/util/slice.go @@ -4,7 +4,7 @@ package util * Generic shared utilities */ -// SliceIncludes returns true is string slice includes value +// SliceIncludes helper for detecting if a slice includes a value func SliceIncludes[T comparable](s []T, val T) bool { for _, v := range s { if v == val {