commit 84673d881008684688113e514c8989244fc3a901
parent 6c553e5f99e22bbe1284396a6c268a7036fd6e6e
Author: Vetle Haflan <vetle@haflan.dev>
Date: Sat, 11 Apr 2020 12:37:20 +0200
Restructure for a more correct package structure
Diffstat:
13 files changed, 651 insertions(+), 648 deletions(-)
diff --git a/cmd/sermoni/main.go b/cmd/sermoni/main.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "sermoni/internal/database"
+)
+
+func main() {
+ // TODO: Use getopt package instead of flags?
+ port := flag.Int("p", 8080, "Port")
+ dbFile := flag.String("d", "sermoni.db", "Database file")
+ //password := flag.String("w", "", "Password for the web interface")
+ flag.Parse()
+ if err := database.Init(*dbFile); err != nil {
+ log.Fatal(err)
+ }
+ defer database.Close()
+ fmt.Printf("Server running on port %v\n", *port)
+}
diff --git a/events/events.go b/events/events.go
@@ -1,167 +0,0 @@
-package events
-
-import (
- "errors"
- "fmt"
- "log"
- "sermoni/database"
- "sermoni/services"
- "strconv"
-
- "go.etcd.io/bbolt"
-)
-
-var (
- keyEventID = []byte("id")
- keyEventService = []byte("service")
- keyEventTimestamp = []byte("timestamp")
- keyEventStatus = []byte("status")
- keyEventTitle = []byte("title")
- keyEventDetails = []byte("details")
-)
-
-// Event contains data sent to sermoni from a service
-type Event struct {
- ID uint64 `json:"id"`
- Service uint64 `json:"service"` // ID of a Service (to be mapped to service name client-side)
- Timestamp uint64 `json:"timestamp"`
- Status string `json:"status"`
- Title string `json:"title"`
- Details string `json:"details"`
-}
-
-// GetAll returns all events in the database
-func GetAll() (events []*Event) {
- db := database.GetDB()
- err := db.View(func(tx *bbolt.Tx) error {
- b := tx.Bucket(database.BucketKeyEvents)
- if b == nil {
- log.Panic("The events bucket does not exist")
- }
- // ForEach doesn't return buckets (nil instead), so only the key is useful
- return b.ForEach(func(id, _ []byte) error {
- eb := b.Bucket(id)
- event := new(Event)
- if err := event.fromBucket(eb); err != nil {
- return err
- }
- events = append(events, event)
- return nil
- })
- })
- if err != nil {
- fmt.Println(err)
- }
- return
-}
-
-// Delete event with the given ID.
-// Returns error if no such event can be found
-func Delete(idInt uint64) error {
- db := database.GetDB()
- id := database.Uint64ToBytes(idInt)
- return db.View(func(tx *bbolt.Tx) error {
- b := tx.Bucket(database.BucketKeyEvents)
- if b == nil {
- log.Panic("The events bucket does notexist")
- }
- return b.DeleteBucket(id)
- })
-}
-
-// Add a new service event if the token matches any services in the database
-// The event.Service will be set to the service ID automatically, given a valid token
-func Add(serviceToken string, event *Event) error {
- db := database.GetDB()
- service := services.GetByToken(serviceToken)
- if service == nil {
- return errors.New("no service found for the given token")
- }
- return db.Update(func(tx *bbolt.Tx) error {
- b := tx.Bucket(database.BucketKeyEvents)
- if b == nil {
- log.Panic("The events bucket does not exist")
- }
-
- // Create a new event ID
- idInt, err := b.NextSequence()
- if err != nil {
- return err
- }
- id := []byte(strconv.FormatUint(idInt, 10))
- event.ID = idInt
- event.Service = service.ID
-
- // Create the event bucket and fill it with data from event
- eb, err := b.CreateBucket(id)
- if err != nil {
- return err
- }
- return event.toBucket(eb)
- })
-}
-
-// Writes the event data to the given bucket
-func (event *Event) toBucket(eb *bbolt.Bucket) error {
- var err error
- id := database.Uint64ToBytes(event.ID)
- serviceID := database.Uint64ToBytes(event.Service)
- timestamp := database.Uint64ToBytes(event.Timestamp)
- if err = eb.Put(keyEventID, id); err != nil {
- return err
- }
- if eb.Put(keyEventService, serviceID); err != nil {
- return err
- }
- if eb.Put(keyEventTimestamp, timestamp); err != nil {
- return err
- }
- if eb.Put(keyEventStatus, []byte(event.Status)); err != nil {
- return err
- }
- if eb.Put(keyEventTitle, []byte(event.Title)); err != nil {
- return err
- }
- if eb.Put(keyEventDetails, []byte(event.Details)); err != nil {
- return err
- }
- return nil
-}
-
-// Reads data from the given bucket into the fields of event
-// Returns error if any of the fields cannot be found
-func (event *Event) fromBucket(eb *bbolt.Bucket) error {
- var id, service, timestamp []byte
- var status, title, details []byte
- err := errors.New("missing field from database")
-
- // Get data from database
- if id = eb.Get(keyEventID); id == nil {
- return err
- }
- if service = eb.Get(keyEventService); service == nil {
- return err
- }
- if timestamp = eb.Get(keyEventTimestamp); timestamp == nil {
- return err
- }
- if status = eb.Get(keyEventStatus); status == nil {
- return err
- }
- if title = eb.Get(keyEventTitle); title == nil {
- return err
- }
- if details = eb.Get(keyEventDetails); details == nil {
- return err
- }
-
- // Format data and set fields of event
- event.ID = database.BytesToUint64(id)
- event.Service = database.BytesToUint64(service)
- event.Timestamp = database.BytesToUint64(timestamp)
- event.Status = string(status)
- event.Title = string(title)
- event.Details = string(details)
-
- return nil
-}
diff --git a/events/events_test.go b/events/events_test.go
@@ -1,97 +0,0 @@
-package events
-
-import (
- "fmt"
- "os"
- "sermoni/database"
- "sermoni/services"
- "testing"
-)
-
-const serviceToken = "test-service"
-
-func (e1 *Event) equals(e2 *Event) bool {
- switch {
- case e1.ID != e2.ID:
- return false
- case e1.Service != e2.Service:
- return false
- case e1.Status != e2.Status:
- return false
- case e1.Title != e2.Title:
- return false
- case e1.Details != e2.Details:
- return false
- default:
- return true
- }
-}
-
-var testEvents = []*Event{
- {
- Timestamp: 1586558825515,
- Status: "ok",
- Title: "Backup completed successfully",
- },
- {
- Timestamp: 1586558838488,
- Status: "info",
- Title: "SSH login for user vetle",
- Details: "User vetle logged in from IP 192.168.10.110",
- },
- {
- Timestamp: 1586558848488,
- Status: "ok",
- },
- {
- Timestamp: 1586558949488,
- Status: "error",
- Title: "Backup failed",
- Details: "Backup couldn't complete because the disk is full",
- },
-}
-
-func TestAddEvent(t *testing.T) {
- for _, event := range testEvents {
- if err := Add(serviceToken, event); err != nil {
- fmt.Println(err)
- t.Fatal("error returned when trying to add event")
- }
- }
-
- // Assumes that bbolt starts sequences on 1
- for i, event := range testEvents {
- event.ID = uint64(i) + 1
- event.Service = 1
- }
-}
-
-func TestGetAll(t *testing.T) {
- events := GetAll()
- for i, event := range events {
- if !event.equals(testEvents[i]) {
- t.Fatal("stored event does not match original")
- }
- }
-}
-
-func TestMain(m *testing.M) {
- // (Re)create the test database
- testDB := "test.db"
- os.Remove(testDB)
- var err error
- if err = database.Init(testDB); err != nil {
- print("Couldn't initialize test database")
- os.Exit(1)
- }
- err = services.Add(serviceToken, &services.Service{
- Name: "test @ dev-laptop",
- Description: "Service used for testing only",
- })
- if err != nil {
- print("Couldn't add test service")
- os.Exit(1)
- }
- defer database.Close()
- os.Exit(m.Run())
-}
diff --git a/database/database.go b/internal/database/database.go
diff --git a/internal/events/events.go b/internal/events/events.go
@@ -0,0 +1,167 @@
+package events
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "sermoni/internal/database"
+ "sermoni/internal/services"
+ "strconv"
+
+ "go.etcd.io/bbolt"
+)
+
+var (
+ keyEventID = []byte("id")
+ keyEventService = []byte("service")
+ keyEventTimestamp = []byte("timestamp")
+ keyEventStatus = []byte("status")
+ keyEventTitle = []byte("title")
+ keyEventDetails = []byte("details")
+)
+
+// Event contains data sent to sermoni from a service
+type Event struct {
+ ID uint64 `json:"id"`
+ Service uint64 `json:"service"` // ID of a Service (to be mapped to service name client-side)
+ Timestamp uint64 `json:"timestamp"`
+ Status string `json:"status"`
+ Title string `json:"title"`
+ Details string `json:"details"`
+}
+
+// GetAll returns all events in the database
+func GetAll() (events []*Event) {
+ db := database.GetDB()
+ err := db.View(func(tx *bbolt.Tx) error {
+ b := tx.Bucket(database.BucketKeyEvents)
+ if b == nil {
+ log.Panic("The events bucket does not exist")
+ }
+ // ForEach doesn't return buckets (nil instead), so only the key is useful
+ return b.ForEach(func(id, _ []byte) error {
+ eb := b.Bucket(id)
+ event := new(Event)
+ if err := event.fromBucket(eb); err != nil {
+ return err
+ }
+ events = append(events, event)
+ return nil
+ })
+ })
+ if err != nil {
+ fmt.Println(err)
+ }
+ return
+}
+
+// Delete event with the given ID.
+// Returns error if no such event can be found
+func Delete(idInt uint64) error {
+ db := database.GetDB()
+ id := database.Uint64ToBytes(idInt)
+ return db.View(func(tx *bbolt.Tx) error {
+ b := tx.Bucket(database.BucketKeyEvents)
+ if b == nil {
+ log.Panic("The events bucket does notexist")
+ }
+ return b.DeleteBucket(id)
+ })
+}
+
+// Add a new service event if the token matches any services in the database
+// The event.Service will be set to the service ID automatically, given a valid token
+func Add(serviceToken string, event *Event) error {
+ db := database.GetDB()
+ service := services.GetByToken(serviceToken)
+ if service == nil {
+ return errors.New("no service found for the given token")
+ }
+ return db.Update(func(tx *bbolt.Tx) error {
+ b := tx.Bucket(database.BucketKeyEvents)
+ if b == nil {
+ log.Panic("The events bucket does not exist")
+ }
+
+ // Create a new event ID
+ idInt, err := b.NextSequence()
+ if err != nil {
+ return err
+ }
+ id := []byte(strconv.FormatUint(idInt, 10))
+ event.ID = idInt
+ event.Service = service.ID
+
+ // Create the event bucket and fill it with data from event
+ eb, err := b.CreateBucket(id)
+ if err != nil {
+ return err
+ }
+ return event.toBucket(eb)
+ })
+}
+
+// Writes the event data to the given bucket
+func (event *Event) toBucket(eb *bbolt.Bucket) error {
+ var err error
+ id := database.Uint64ToBytes(event.ID)
+ serviceID := database.Uint64ToBytes(event.Service)
+ timestamp := database.Uint64ToBytes(event.Timestamp)
+ if err = eb.Put(keyEventID, id); err != nil {
+ return err
+ }
+ if eb.Put(keyEventService, serviceID); err != nil {
+ return err
+ }
+ if eb.Put(keyEventTimestamp, timestamp); err != nil {
+ return err
+ }
+ if eb.Put(keyEventStatus, []byte(event.Status)); err != nil {
+ return err
+ }
+ if eb.Put(keyEventTitle, []byte(event.Title)); err != nil {
+ return err
+ }
+ if eb.Put(keyEventDetails, []byte(event.Details)); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Reads data from the given bucket into the fields of event
+// Returns error if any of the fields cannot be found
+func (event *Event) fromBucket(eb *bbolt.Bucket) error {
+ var id, service, timestamp []byte
+ var status, title, details []byte
+ err := errors.New("missing field from database")
+
+ // Get data from database
+ if id = eb.Get(keyEventID); id == nil {
+ return err
+ }
+ if service = eb.Get(keyEventService); service == nil {
+ return err
+ }
+ if timestamp = eb.Get(keyEventTimestamp); timestamp == nil {
+ return err
+ }
+ if status = eb.Get(keyEventStatus); status == nil {
+ return err
+ }
+ if title = eb.Get(keyEventTitle); title == nil {
+ return err
+ }
+ if details = eb.Get(keyEventDetails); details == nil {
+ return err
+ }
+
+ // Format data and set fields of event
+ event.ID = database.BytesToUint64(id)
+ event.Service = database.BytesToUint64(service)
+ event.Timestamp = database.BytesToUint64(timestamp)
+ event.Status = string(status)
+ event.Title = string(title)
+ event.Details = string(details)
+
+ return nil
+}
diff --git a/internal/events/events_test.go b/internal/events/events_test.go
@@ -0,0 +1,97 @@
+package events
+
+import (
+ "fmt"
+ "os"
+ "sermoni/internal/database"
+ "sermoni/internal/services"
+ "testing"
+)
+
+const serviceToken = "test-service"
+
+func (e1 *Event) equals(e2 *Event) bool {
+ switch {
+ case e1.ID != e2.ID:
+ return false
+ case e1.Service != e2.Service:
+ return false
+ case e1.Status != e2.Status:
+ return false
+ case e1.Title != e2.Title:
+ return false
+ case e1.Details != e2.Details:
+ return false
+ default:
+ return true
+ }
+}
+
+var testEvents = []*Event{
+ {
+ Timestamp: 1586558825515,
+ Status: "ok",
+ Title: "Backup completed successfully",
+ },
+ {
+ Timestamp: 1586558838488,
+ Status: "info",
+ Title: "SSH login for user vetle",
+ Details: "User vetle logged in from IP 192.168.10.110",
+ },
+ {
+ Timestamp: 1586558848488,
+ Status: "ok",
+ },
+ {
+ Timestamp: 1586558949488,
+ Status: "error",
+ Title: "Backup failed",
+ Details: "Backup couldn't complete because the disk is full",
+ },
+}
+
+func TestAddEvent(t *testing.T) {
+ for _, event := range testEvents {
+ if err := Add(serviceToken, event); err != nil {
+ fmt.Println(err)
+ t.Fatal("error returned when trying to add event")
+ }
+ }
+
+ // Assumes that bbolt starts sequences on 1
+ for i, event := range testEvents {
+ event.ID = uint64(i) + 1
+ event.Service = 1
+ }
+}
+
+func TestGetAll(t *testing.T) {
+ events := GetAll()
+ for i, event := range events {
+ if !event.equals(testEvents[i]) {
+ t.Fatal("stored event does not match original")
+ }
+ }
+}
+
+func TestMain(m *testing.M) {
+ // (Re)create the test database
+ testDB := "test.db"
+ os.Remove(testDB)
+ var err error
+ if err = database.Init(testDB); err != nil {
+ print("Couldn't initialize test database")
+ os.Exit(1)
+ }
+ err = services.Add(serviceToken, &services.Service{
+ Name: "test @ dev-laptop",
+ Description: "Service used for testing only",
+ })
+ if err != nil {
+ print("Couldn't add test service")
+ os.Exit(1)
+ }
+ defer database.Close()
+ os.Exit(m.Run())
+}
diff --git a/internal/services/services.go b/internal/services/services.go
@@ -0,0 +1,223 @@
+package services
+
+import (
+ "errors"
+ "log"
+ "sermoni/internal/database"
+ "strconv"
+
+ "go.etcd.io/bbolt"
+)
+
+var (
+ keyServiceID = []byte("id")
+ keyServiceName = []byte("name")
+ keyServiceDescription = []byte("description")
+ keyServicePeriod = []byte("period")
+)
+
+// Service describes a service that is expected to report
+type Service struct {
+ ID uint64 `json:"id"` // service id, an integer that represents the service
+ Name string `json:"name"` // service name, usually on the format 'service @ server'
+ Description string `json:"description"` // more detailed description of the service
+ ExpectationPeriod uint64 `json:"period"` // set if the service is expected to report periodically, format is UnixTime (milli?)
+}
+
+// GetByToken returns the service structure associated with the token string, if there
+// are any matching entries in service-tokens bucket. Returns nil if there are no matches
+func GetByToken(token string) *Service {
+ id := getIDFromToken(token)
+ if id == nil {
+ log.Printf("no service found for the token '%v'\n", token)
+ return nil
+ }
+ return get(id)
+}
+
+// GetByID returns the service structure associated with the given uint64-formatted
+// service ID, if that service exists. Otherwise returns nil
+func GetByID(id uint64) *Service {
+ return get([]byte(strconv.FormatUint(id, 10)))
+}
+
+// GetAll returns all services in the database (TODO)
+func GetAll() (services []*Service) {
+ db := database.GetDB()
+ db.View(func(tx *bbolt.Tx) error {
+ b := tx.Bucket(database.BucketKeyServices)
+ stb := tx.Bucket(database.BucketKeyServiceTokens)
+ if b == nil {
+ log.Panic("The services bucket does not exist")
+ }
+ if stb == nil {
+ log.Panic("The service-tokens bucket does not exist")
+ }
+
+ // Go through all k-v pairs in the service *tokens* bucket, in order to get service bucket IDs
+ // Use the ID to get the service bucket and create service fromBucket
+ return stb.ForEach(func(_, id []byte) error {
+ sb := b.Bucket(id)
+ service := new(Service)
+ service.fromBucket(id, sb)
+ services = append(services, service)
+ return nil
+ })
+ })
+ return
+}
+
+// Delete deletes the given service if it exists
+func Delete(intID uint64) error {
+ db := database.GetDB()
+ serviceID := []byte(strconv.FormatUint(intID, 10))
+ return db.Update(func(tx *bbolt.Tx) error {
+ var b, stb *bbolt.Bucket
+ if b = tx.Bucket(database.BucketKeyServices); b == nil {
+ log.Panic("The services bucket does not exist")
+ }
+ if stb = tx.Bucket(database.BucketKeyServiceTokens); b == nil {
+ log.Panic("The service-tokens bucket does not exist")
+ }
+
+ // Delete the entry from root services bucket
+ if b.Bucket(serviceID) == nil {
+ return errors.New("no service for the given id")
+ }
+ if err := b.DeleteBucket(serviceID); err != nil {
+ return err
+ }
+
+ // Find the token entry and delete it from service-tokens bucket
+ c := stb.Cursor()
+ for token, id := c.First(); token != nil; token, id = c.Next() {
+ if string(id) == string(serviceID) {
+ return stb.Delete(token)
+ }
+ }
+ return errors.New("service id not found in the service-tokens bucket")
+
+ // TODO: Cascade, i.e. delete all events for the given service
+ // Maybe this should be done in the HTTP request handler, though?
+ })
+}
+
+// Add adds a new service to monitor
+// Returns error if the token is unavailable and if the transaction fails in any way
+func Add(token string, service *Service) error {
+ db := database.GetDB()
+ return db.Update(func(tx *bbolt.Tx) error {
+ var err error
+ var b, sb, stb *bbolt.Bucket
+ var serviceIDint uint64
+ var serviceID []byte
+
+ // Get the services root bucket
+ if b = tx.Bucket(database.BucketKeyServices); b == nil {
+ log.Panic("The services bucket does not exist")
+ }
+ // Get the service-tokens root bucket
+ if stb = tx.Bucket(database.BucketKeyServiceTokens); stb == nil {
+ log.Panic("The service-tokens bucket does not exist")
+ }
+
+ // Check if the service token is available, return error otherwise
+ serviceToken := []byte(token)
+ if serviceID = stb.Get(serviceToken); serviceID != nil {
+ return errors.New("a service has already been registered for the given token")
+ }
+
+ // Create a new service bucket, sb, and populate it with data from service
+ if serviceIDint, err = b.NextSequence(); err != nil {
+ return err
+ }
+ serviceID = []byte(strconv.FormatUint(serviceIDint, 10))
+ if sb, err = b.CreateBucket(serviceID); err != nil {
+ return err
+ }
+ if err = sb.Put(keyServiceName, []byte(service.Name)); err != nil {
+ return err
+ }
+ if err = sb.Put(keyServiceDescription, []byte(service.Description)); err != nil {
+ return err
+ }
+ periodStr := strconv.FormatUint(service.ExpectationPeriod, 10)
+ if err = sb.Put(keyServicePeriod, []byte(periodStr)); err != nil {
+ return err
+ }
+
+ // Put an entry in the service-tokens bucket to map the token to the service
+ return stb.Put([]byte(token), serviceID)
+ })
+}
+
+//
+// Package-local helpers
+//
+
+// fromBucket populates the service struct with data from the given service bucket
+// TODO: Consider failing on missing fields and generally choosing an approach more similar to Event.fromBucket
+func (service *Service) fromBucket(id []byte, sb *bbolt.Bucket) {
+ // Ignoring this error, because it shouldn't be possible
+ idInt := database.BytesToUint64(id)
+ service.ID = idInt
+ if name := sb.Get(keyServiceName); name != nil {
+ service.Name = string(name)
+ }
+ if description := sb.Get(keyServiceDescription); description != nil {
+ service.Description = string(description)
+ }
+ if period := sb.Get(keyServicePeriod); period != nil {
+ // Quick fix: Convert to string, then int
+ // Uses default value 0 if an error occurs
+ intPeriod, err := strconv.ParseUint(string(period), 10, 64)
+ if err == nil {
+ service.ExpectationPeriod = intPeriod
+ } else {
+ log.Println("Couldn't convert period to int for service")
+ service.ExpectationPeriod = 0
+ }
+ }
+}
+
+// get returns the service structure associated with the []byte-formatted service ID
+func get(id []byte) *Service {
+ var service Service
+ db := database.GetDB()
+ err := db.View(func(tx *bbolt.Tx) error {
+
+ // Get the root services bucket and the requested service bucket
+ var b, sb *bbolt.Bucket
+ if b = tx.Bucket(database.BucketKeyServices); b == nil {
+ log.Panic("The services bucket does not exist")
+ }
+ if sb = b.Bucket(id); sb == nil {
+ return errors.New("no service found for the given id")
+ }
+
+ // Get service information from the bucket
+ service.fromBucket(id, sb)
+ return nil
+ })
+ if err != nil {
+ log.Println(err)
+ return nil
+ }
+ return &service
+}
+
+// getIDFromToken looks up the given token in the service-tokens bucket and returns
+// the ID if it's found, otherwise returning nil
+func getIDFromToken(token string) []byte {
+ var id []byte
+ db := database.GetDB()
+ db.View(func(tx *bbolt.Tx) error {
+ stb := tx.Bucket(database.BucketKeyServiceTokens)
+ if stb == nil {
+ log.Panic("The service-tokens bucket does not exist")
+ }
+ id = stb.Get([]byte(token))
+ return nil
+ })
+ return id
+}
diff --git a/internal/services/services_test.go b/internal/services/services_test.go
@@ -0,0 +1,143 @@
+package services
+
+import (
+ "fmt"
+ "os"
+ "sermoni/internal/database"
+ "strconv"
+ "testing"
+)
+
+// intID gets uint64 from bytes
+func intID(id []byte) uint64 {
+ idInt, err := strconv.ParseUint(string(id), 10, 64)
+ if err != nil {
+ return 0
+ }
+ return idInt
+}
+
+func (s1 *Service) equals(s2 *Service) bool {
+ switch {
+ case s1.ID != s2.ID:
+ return false
+ case s1.Name != s2.Name:
+ return false
+ case s1.Description != s2.Description:
+ return false
+ case s1.ExpectationPeriod != s2.ExpectationPeriod:
+ return false
+ default:
+ return true
+ }
+}
+
+var (
+ token1 = "my-great-token"
+ token2 = "my-other-token"
+ token3 = "my-third-token"
+)
+
+var testServices = []*Service{
+ {
+ Name: "tester @ dev-computer",
+ Description: "This describes the service in more detail",
+ ExpectationPeriod: 282342,
+ },
+ {Name: "tester2", ExpectationPeriod: 300003},
+ {Name: "third @ tester"},
+}
+
+func TestAddService(t *testing.T) {
+ err := Add(token1, testServices[0])
+ if err != nil {
+ fmt.Println(err)
+ t.Fatal("unexpected error when adding service")
+ }
+ if err = Add(token2, testServices[1]); err != nil {
+ fmt.Println(err)
+ t.Fatal("unexpected error when adding second service")
+ }
+ if err = Add(token2, testServices[1]); err == nil {
+ t.Fatal("no error returned when trying to re-use a service token")
+ }
+ err = Add(token3, testServices[2])
+ if err != nil {
+ t.Fatal("unexpected error when adding third service")
+ }
+
+ // Simulate ID generation for testServices after adding them to DB, to avoid
+ // possible interferrence (shouldn't be a problem, but doesn't hurt to be sure).
+ // bbolt should always start ID sequences on 1, so this assumes that the service ID
+ // equals the testService index + 1
+ for i, service := range testServices {
+ service.ID = uint64(i) + 1
+ }
+}
+
+func TestDeleteService(t *testing.T) {
+ var di uint64 = 1 // Deletion index
+ err := Delete(di + 1)
+ if err != nil {
+ fmt.Println(err)
+ t.Fatal("unexpected error when trying to delete service")
+ }
+ if err = Delete(di + 1); err == nil {
+ t.Fatal("no error returned when trying to delete non-existing service")
+ }
+
+ // Assert that the service token is deleted too
+ if service := GetByToken(token2); service != nil {
+ t.Fatal("the service token was not deleted")
+ }
+
+ // Delete from testServices too
+ testServices = append(testServices[:di], testServices[di+1:]...)
+}
+
+func TestGetByID(t *testing.T) {
+ var gi uint64 = 0 // Get index
+ testService := testServices[gi]
+ service := GetByID(gi + 1)
+ if !service.equals(testService) {
+ t.Fatal("stored service doesn't match original")
+ }
+ if service = GetByID(23423); service != nil {
+ t.Fatal("returned service for invalid id")
+ }
+}
+
+func TestGetByToken(t *testing.T) {
+ testService := testServices[1]
+ service := GetByToken(token3)
+ if !service.equals(testService) {
+ t.Fatal("stored service doesn't match original")
+ }
+ if service = GetByToken("not-a-token"); service != nil {
+ t.Fatal("returned service for invalid token")
+ }
+}
+
+func TestGetAll(t *testing.T) {
+ services := GetAll()
+ for i, service := range services {
+ if !service.equals(testServices[i]) {
+ t.Fatal("stored service doesn't match original")
+ }
+ }
+ if services[0].equals(testServices[1]) {
+ t.Fatal("unexpected match between two services")
+ }
+}
+
+func TestMain(m *testing.M) {
+ // (Re)create the test database
+ testDB := "test.db"
+ os.Remove(testDB)
+ if err := database.Init(testDB); err != nil {
+ print("Couldn't initialize test database")
+ os.Exit(1)
+ }
+ defer database.Close()
+ os.Exit(m.Run())
+}
diff --git a/main.go b/main.go
@@ -1,21 +0,0 @@
-package main
-
-import (
- "flag"
- "fmt"
- "log"
- "sermoni/database"
-)
-
-func main() {
- // TODO: Use getopt package instead of flags?
- port := flag.Int("p", 8080, "Port")
- dbFile := flag.String("d", "sermoni.db", "Database file")
- //password := flag.String("w", "", "Password for the web interface")
- flag.Parse()
- if err := database.Init(*dbFile); err != nil {
- log.Fatal(err)
- }
- defer database.Close()
- fmt.Printf("Server running on port %v\n", *port)
-}
diff --git a/services/services.go b/services/services.go
@@ -1,220 +0,0 @@
-package services
-
-import (
- "errors"
- "log"
- "sermoni/database"
- "strconv"
-
- "go.etcd.io/bbolt"
-)
-
-var (
- keyServiceID = []byte("id")
- keyServiceName = []byte("name")
- keyServiceDescription = []byte("description")
- keyServicePeriod = []byte("period")
-)
-
-// Service describes a service that is expected to report
-type Service struct {
- ID uint64 `json:"id"` // service id, an integer that represents the service
- Name string `json:"name"` // service name, usually on the format 'service @ server'
- Description string `json:"description"` // more detailed description of the service
- ExpectationPeriod uint64 `json:"period"` // set if the service is expected to report periodically, format is UnixTime (milli?)
-}
-
-// GetByToken returns the service structure associated with the token string, if there
-// are any matching entries in service-tokens bucket. Returns nil if there are no matches
-func GetByToken(token string) *Service {
- id := getIDFromToken(token)
- if id == nil {
- log.Printf("no service found for the token '%v'\n", token)
- return nil
- }
- return get(id)
-}
-
-// GetByID returns the service structure associated with the given uint64-formatted
-// service ID, if that service exists. Otherwise returns nil
-func GetByID(id uint64) *Service {
- return get([]byte(strconv.FormatUint(id, 10)))
-}
-
-// GetAll returns all services in the database (TODO)
-func GetAll() (services []*Service) {
- db := database.GetDB()
- db.View(func(tx *bbolt.Tx) error {
- b := tx.Bucket(database.BucketKeyServices)
- stb := tx.Bucket(database.BucketKeyServiceTokens)
- if b == nil {
- log.Panic("The services bucket does not exist")
- }
- if stb == nil {
- log.Panic("The service-tokens bucket does not exist")
- }
-
- // Go through all k-v pairs in the service *tokens* bucket, in order to get service bucket IDs
- // Use the ID to get the service bucket and create service fromBucket
- return stb.ForEach(func(_, id []byte) error {
- sb := b.Bucket(id)
- service := new(Service)
- service.fromBucket(id, sb)
- services = append(services, service)
- return nil
- })
- })
- return
-}
-
-// Delete deletes the given service if it exists
-func Delete(intID uint64) error {
- db := database.GetDB()
- serviceID := []byte(strconv.FormatUint(intID, 10))
- return db.Update(func(tx *bbolt.Tx) error {
- var b, stb *bbolt.Bucket
- if b = tx.Bucket(database.BucketKeyServices); b == nil {
- log.Panic("The services bucket does not exist")
- }
- if stb = tx.Bucket(database.BucketKeyServiceTokens); b == nil {
- log.Panic("The service-tokens bucket does not exist")
- }
-
- // Delete the entry from root services bucket
- if b.Bucket(serviceID) == nil {
- return errors.New("no service for the given id")
- }
- if err := b.DeleteBucket(serviceID); err != nil {
- return err
- }
-
- // Find the token entry and delete it from service-tokens bucket
- c := stb.Cursor()
- for token, id := c.First(); token != nil; token, id = c.Next() {
- if string(id) == string(serviceID) {
- return stb.Delete(token)
- }
- }
- return errors.New("service id not found in the service-tokens bucket")
- })
-}
-
-// Add adds a new service to monitor
-// Returns error if the token is unavailable and if the transaction fails in any way
-func Add(token string, service *Service) error {
- db := database.GetDB()
- return db.Update(func(tx *bbolt.Tx) error {
- var err error
- var b, sb, stb *bbolt.Bucket
- var serviceIDint uint64
- var serviceID []byte
-
- // Get the services root bucket
- if b = tx.Bucket(database.BucketKeyServices); b == nil {
- log.Panic("The services bucket does not exist")
- }
- // Get the service-tokens root bucket
- if stb = tx.Bucket(database.BucketKeyServiceTokens); stb == nil {
- log.Panic("The service-tokens bucket does not exist")
- }
-
- // Check if the service token is available, return error otherwise
- serviceToken := []byte(token)
- if serviceID = stb.Get(serviceToken); serviceID != nil {
- return errors.New("a service has already been registered for the given token")
- }
-
- // Create a new service bucket, sb, and populate it with data from service
- if serviceIDint, err = b.NextSequence(); err != nil {
- return err
- }
- serviceID = []byte(strconv.FormatUint(serviceIDint, 10))
- if sb, err = b.CreateBucket(serviceID); err != nil {
- return err
- }
- if err = sb.Put(keyServiceName, []byte(service.Name)); err != nil {
- return err
- }
- if err = sb.Put(keyServiceDescription, []byte(service.Description)); err != nil {
- return err
- }
- periodStr := strconv.FormatUint(service.ExpectationPeriod, 10)
- if err = sb.Put(keyServicePeriod, []byte(periodStr)); err != nil {
- return err
- }
-
- // Put an entry in the service-tokens bucket to map the token to the service
- return stb.Put([]byte(token), serviceID)
- })
-}
-
-//
-// Package-local helpers
-//
-
-// fromBucket populates the service struct with data from the given service bucket
-// TODO: Consider failing on missing fields and generally choosing an approach more similar to Event.fromBucket
-func (service *Service) fromBucket(id []byte, sb *bbolt.Bucket) {
- // Ignoring this error, because it shouldn't be possible
- idInt := database.BytesToUint64(id)
- service.ID = idInt
- if name := sb.Get(keyServiceName); name != nil {
- service.Name = string(name)
- }
- if description := sb.Get(keyServiceDescription); description != nil {
- service.Description = string(description)
- }
- if period := sb.Get(keyServicePeriod); period != nil {
- // Quick fix: Convert to string, then int
- // Uses default value 0 if an error occurs
- intPeriod, err := strconv.ParseUint(string(period), 10, 64)
- if err == nil {
- service.ExpectationPeriod = intPeriod
- } else {
- log.Println("Couldn't convert period to int for service")
- service.ExpectationPeriod = 0
- }
- }
-}
-
-// get returns the service structure associated with the []byte-formatted service ID
-func get(id []byte) *Service {
- var service Service
- db := database.GetDB()
- err := db.View(func(tx *bbolt.Tx) error {
-
- // Get the root services bucket and the requested service bucket
- var b, sb *bbolt.Bucket
- if b = tx.Bucket(database.BucketKeyServices); b == nil {
- log.Panic("The services bucket does not exist")
- }
- if sb = b.Bucket(id); sb == nil {
- return errors.New("no service found for the given id")
- }
-
- // Get service information from the bucket
- service.fromBucket(id, sb)
- return nil
- })
- if err != nil {
- log.Println(err)
- return nil
- }
- return &service
-}
-
-// getIDFromToken looks up the given token in the service-tokens bucket and returns
-// the ID if it's found, otherwise returning nil
-func getIDFromToken(token string) []byte {
- var id []byte
- db := database.GetDB()
- db.View(func(tx *bbolt.Tx) error {
- stb := tx.Bucket(database.BucketKeyServiceTokens)
- if stb == nil {
- log.Panic("The service-tokens bucket does not exist")
- }
- id = stb.Get([]byte(token))
- return nil
- })
- return id
-}
diff --git a/services/services_test.go b/services/services_test.go
@@ -1,143 +0,0 @@
-package services
-
-import (
- "fmt"
- "os"
- "sermoni/database"
- "strconv"
- "testing"
-)
-
-// intID gets uint64 from bytes
-func intID(id []byte) uint64 {
- idInt, err := strconv.ParseUint(string(id), 10, 64)
- if err != nil {
- return 0
- }
- return idInt
-}
-
-func (s1 *Service) equals(s2 *Service) bool {
- switch {
- case s1.ID != s2.ID:
- return false
- case s1.Name != s2.Name:
- return false
- case s1.Description != s2.Description:
- return false
- case s1.ExpectationPeriod != s2.ExpectationPeriod:
- return false
- default:
- return true
- }
-}
-
-var (
- token1 = "my-great-token"
- token2 = "my-other-token"
- token3 = "my-third-token"
-)
-
-var testServices = []*Service{
- {
- Name: "tester @ dev-computer",
- Description: "This describes the service in more detail",
- ExpectationPeriod: 282342,
- },
- {Name: "tester2", ExpectationPeriod: 300003},
- {Name: "third @ tester"},
-}
-
-func TestAddService(t *testing.T) {
- err := Add(token1, testServices[0])
- if err != nil {
- fmt.Println(err)
- t.Fatal("unexpected error when adding service")
- }
- if err = Add(token2, testServices[1]); err != nil {
- fmt.Println(err)
- t.Fatal("unexpected error when adding second service")
- }
- if err = Add(token2, testServices[1]); err == nil {
- t.Fatal("no error returned when trying to re-use a service token")
- }
- err = Add(token3, testServices[2])
- if err != nil {
- t.Fatal("unexpected error when adding third service")
- }
-
- // Simulate ID generation for testServices after adding them to DB, to avoid
- // possible interferrence (shouldn't be a problem, but doesn't hurt to be sure).
- // bbolt should always start ID sequences on 1, so this assumes that the service ID
- // equals the testService index + 1
- for i, service := range testServices {
- service.ID = uint64(i) + 1
- }
-}
-
-func TestDeleteService(t *testing.T) {
- var di uint64 = 1 // Deletion index
- err := Delete(di + 1)
- if err != nil {
- fmt.Println(err)
- t.Fatal("unexpected error when trying to delete service")
- }
- if err = Delete(di + 1); err == nil {
- t.Fatal("no error returned when trying to delete non-existing service")
- }
-
- // Assert that the service token is deleted too
- if service := GetByToken(token2); service != nil {
- t.Fatal("the service token was not deleted")
- }
-
- // Delete from testServices too
- testServices = append(testServices[:di], testServices[di+1:]...)
-}
-
-func TestGetByID(t *testing.T) {
- var gi uint64 = 0 // Get index
- testService := testServices[gi]
- service := GetByID(gi + 1)
- if !service.equals(testService) {
- t.Fatal("stored service doesn't match original")
- }
- if service = GetByID(23423); service != nil {
- t.Fatal("returned service for invalid id")
- }
-}
-
-func TestGetByToken(t *testing.T) {
- testService := testServices[1]
- service := GetByToken(token3)
- if !service.equals(testService) {
- t.Fatal("stored service doesn't match original")
- }
- if service = GetByToken("not-a-token"); service != nil {
- t.Fatal("returned service for invalid token")
- }
-}
-
-func TestGetAll(t *testing.T) {
- services := GetAll()
- for i, service := range services {
- if !service.equals(testServices[i]) {
- t.Fatal("stored service doesn't match original")
- }
- }
- if services[0].equals(testServices[1]) {
- t.Fatal("unexpected match between two services")
- }
-}
-
-func TestMain(m *testing.M) {
- // (Re)create the test database
- testDB := "test.db"
- os.Remove(testDB)
- if err := database.Init(testDB); err != nil {
- print("Couldn't initialize test database")
- os.Exit(1)
- }
- defer database.Close()
- os.Exit(m.Run())
-}
diff --git a/proto.html b/ui/proto.html
diff --git a/style.css b/ui/style.css