sermoni

"Service monitor" / cronjob status service
Log | Files | Refs

commit 8737e753f8175907af1e8315311feabb8cb4cd2f
parent e7885cf53e8c8c30814ce861d2e677157ff9e928
Author: Vetle Haflan <vetle@haflan.dev>
Date:   Fri, 10 Apr 2020 21:51:05 +0200

Lots of DB updates to support new structure idea

Including a revert of the previous BucketWrapper refactoring.
Said refactoring doesn't fit well with the new DB structure, which should
contain a root bucket for tokens -> service id mappings, and therefore
use multiple root buckets per transaction

Diffstat:
Mdatabase/database.go | 46++++++++++++----------------------------------
Mgo.mod | 1+
Mmain.go | 10----------
Mservices/services.go | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Aservices/services_test.go | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 249 insertions(+), 89 deletions(-)

diff --git a/database/database.go b/database/database.go @@ -17,9 +17,10 @@ const ( // bbolt keys var ( - BucketKeyConfig = []byte("config") // bucket key for config bucket key - BucketKeyServices = []byte("services") // bucket key for services bucket - BucketKeyEvents = []byte("events") // bucket key for events bucket + BucketKeyConfig = []byte("config") // bucket key for config bucket key + BucketKeyEvents = []byte("events") // bucket key for events bucket + BucketKeyServices = []byte("services") // bucket key for services bucket + BucketKeyServiceTokens = []byte("service-tokens") // bucket key for service-tokens bucket keyPassHash = []byte("passhash") keyPageTitle = []byte("pagetitle") @@ -51,6 +52,9 @@ func Init(dbFileName string) error { if _, err := tx.CreateBucketIfNotExists(BucketKeyEvents); err != nil { return err } + if _, err := tx.CreateBucketIfNotExists(BucketKeyServiceTokens); err != nil { + return err + } return nil }) if err != nil { @@ -118,34 +122,7 @@ func Close() { db.Close() } -// BucketOperation operates on the given (root level) bucket if it exists, using the -// DB.Update function if update is set true, otherwise using DB.View. -// An error is returned if no bucket can be found for the bucketKey or any other -// error occurs in the wrapped transaction -func bucketOperation(update bool, bucketKey []byte, fn func(*bbolt.Bucket) error) error { - var operation func(func(*bbolt.Tx) error) error - if update { - operation = db.Update - } else { - operation = db.View - } - return operation(func(tx *bbolt.Tx) error { - bucket := tx.Bucket(bucketKey) - if bucket == nil { - return errors.New("the given bucket does not exist") - } - return fn(bucket) - }) -} - -// BucketUpdate wraps DB.Update with a general way of handling errors -// if the bucket does not exist -func BucketUpdate(bucketKey []byte, fn func(*bbolt.Bucket) error) error { - return bucketOperation(true, bucketKey, fn) -} - -// BucketView wraps DB.View with a general way of handling errors -// if the bucket does not exist -func BucketView(bucketKey []byte, fn func(*bbolt.Bucket) error) error { - return bucketOperation(true, bucketKey, fn) -} +// GetDB gets the database structure +func GetDB() *bbolt.DB { + return db +}+ \ No newline at end of file diff --git a/go.mod b/go.mod @@ -3,6 +3,7 @@ module sermoni go 1.13 require ( + github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 // indirect go.etcd.io/bbolt v1.3.4 golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 ) diff --git a/main.go b/main.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "sermoni/database" - "sermoni/services" ) func main() { @@ -19,13 +18,4 @@ func main() { } defer database.Close() fmt.Printf("Server running on port %v\n", *port) - services.Add("testing", services.Service{ - Name: "Test name", - Description: "This is the description, yay", - ExpectationPeriod: 282342, - }) - fmt.Printf("ADD ERR: %v\n", services.Add("testing", services.Service{Name: "This"})) - fmt.Printf("none service: %+v\n", services.Get("none")) - fmt.Printf("DELETE ERR: %v\n", services.Delete("none")) - fmt.Printf("test service: %+v\n", services.Get("testing")) } diff --git a/services/services.go b/services/services.go @@ -10,6 +10,7 @@ import ( ) var ( + keyServiceID = []byte("id") keyServiceName = []byte("name") keyServiceDescription = []byte("description") keyServicePeriod = []byte("period") @@ -17,67 +18,104 @@ var ( // 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?) } -// Get returns a service struct if the identifier matches any -// keys in the services bucket. Returns nil if there are no matching buckets -func Get(identifier string) *Service { - var service Service - err := database.BucketView(database.BucketKeyServices, func(b *bbolt.Bucket) error { - sb := b.Bucket([]byte(identifier)) - if sb == nil { - return errors.New("no bucket found for the given id") - } - 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 - log.Printf("Couldn't convert period to int for service with id '%v'\n", identifier) - } else { - service.ExpectationPeriod = 0 - } - } - return nil - }) - if err != nil { - log.Println(err) + +// 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 &service + 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() []*Service { + db := database.GetDB() + var services []*Service + 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 services } // Delete deletes the given service if it exists -func Delete(identifier string) error { - return database.BucketUpdate(database.BucketKeyServices, func(b *bbolt.Bucket) error { - serviceKey := []byte(identifier) - if b.Bucket(serviceKey) == nil { +func Delete(intID uint64) error { + db := database.GetDB() + id := []byte(strconv.FormatUint(intID, 10)) + return db.Update(func(tx *bbolt.Tx) error { + var b *bbolt.Bucket + if b = tx.Bucket(database.BucketKeyServices); b == nil { + log.Panic("The services bucket does not exist") + } + if b.Bucket(id) == nil { return errors.New("no service for the given id") } - return b.DeleteBucket(serviceKey) + return b.DeleteBucket(id) }) } // Add adds a new service to monitor -func Add(identifier string, service Service) error { - return database.BucketUpdate(database.BucketKeyServices, func(b *bbolt.Bucket) error { - serviceKey := []byte(identifier) - if b.Bucket(serviceKey) != nil { - return errors.New("a service has already been registered for the given id") - } - // Create the service bucket, sb - sb, err := b.CreateBucket(serviceKey) - if err != nil { +// 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 { @@ -90,6 +128,80 @@ func Add(identifier string, service Service) error { 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 +func (service *Service) fromBucket(id []byte, sb *bbolt.Bucket) { + // Ignoring this error, because it shouldn't be possible + idInt, _ := strconv.ParseUint(string(id), 10, 64) + 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 + log.Printf("Couldn't convert period to int for service") + } else { + 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 @@ -0,0 +1,79 @@ +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 TestAddService(t *testing.T) { + token1 := "my-great-token" + token2 := "my-other-token" + token3 := "my-third-token" + err := Add(token1, Service{ + Name: "tester @ dev-computer", + Description: "This describes the service in more detail", + ExpectationPeriod: 282342, + }) + if err != nil { + t.Fatal("unexpected error when adding service") + } + if err = Add(token2, Service{Name: "tester2"}); err != nil { + t.Fatal("unexpected error when adding second service") + } + if err = Add(token2, Service{Name: "another tester"}); err == nil { + t.Fatal("no error returned when trying to re-use a service token") + } + err = Add(token3, Service{Name: "third @ tester", ExpectationPeriod: 300003}) + if err != nil { + t.Fatal("unexpected error when adding third service") + } + + /* + // Add new + fmt.Printf("DELETE ERR: %v\n", services.Delete( + services.IntID(services.GetIDFromToken("token1")))) + fmt.Printf("ADD ERR: %v\n", services fmt.Printf("ADD ERR: %v\n", services.Add("token2", services.Service{Name: "This"})) + fmt.Printf("ADD ERR: %v\n", services.Add("token2", services.Service{Name: "This again"})) + fmt.Printf("token1: %+v\n", services.GetByToken("token1")) + fmt.Printf("DELETE ERR: %v\n", services.Delete( + services.IntID(services.GetIDFromToken("token1")))) + fmt.Printf("token1: %+v\n", services.GetByToken("token1")) + fmt.Printf("DELETE ERR: %v\n", services.Delete( + services.IntID(services.GetIDFromToken("token1")))) + */ +} + +func TestDeleteService(t *testing.T) { + return +} + +func TestGetAll(t *testing.T) { + services := GetAll() + for _, service := range services { + fmt.Printf("%+v\n", service) + } +} + +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()) +}