Skip to content

Commit

Permalink
BBolt has now been preliminary integrated. It's not yet robust but it…
Browse files Browse the repository at this point in the history
…'s a WIP
  • Loading branch information
deckarep committed Dec 25, 2023
1 parent 97cd8e3 commit 21c37cf
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 14 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ tips
vhs_demos/*.gif

testmode/*.json

*.db.bolt
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ linters:
- goimports
- govet
- misspell
- musttag
- revive

# Configuration for how we run golangci-lint
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
.PHONY: test build lint gen all
.PHONY: test build lint gen all clean

# Define the default goal. When you run "make" without argument, it will run the "all" target.
default: all

# Capture additional arguments which can optionally be passed in.
ARGS ?=

clean:
rm *db.bolt

# Test the code.
test:
go test -v ./...
Expand All @@ -20,7 +23,7 @@ gen:

# Build the project: run the linter and then build.
build: lint
go build -v ./...
go build
@echo "Build: Successful"

# Run all steps: build and then run the application.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/tailscale/hujson v0.0.0-20220506213045-af5ed07155e5 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/sys v0.15.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
Expand Down
229 changes: 229 additions & 0 deletions pkg/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
Open Source Initiative OSI - The MIT License (MIT):Licensing
The MIT License (MIT)
Copyright (c) 2023 - 2024 Ralph Caraveo (deckarep@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

package pkg

import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"tips/pkg/tailscale_cli"

"github.com/tailscale/tailscale-client-go/tailscale"

"github.com/charmbracelet/log"

bolt "go.etcd.io/bbolt"
)

const (

// These buckets only point to keys where the actual data exists in those respective buckets.
// TODO:

// These two buckets contain FULL data.
devicesBucket = "devices.full"
enrichedBucket = "enriched.full"
)

type DB struct {
tailnetScope string
hdl *bolt.DB
}

func NewDB(tailnetScope string) DB {
return DB{
tailnetScope: tailnetScope,
}
}

func (d *DB) TailnetScope() string {
return d.tailnetScope
}

func (d *DB) File() string {
return fmt.Sprintf("%s.db.bolt", d.tailnetScope)
}

func (d *DB) Open() error {
db, err := bolt.Open(d.File(), 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return err
}
d.hdl = db
return nil
}

func (d *DB) Close() error {
if d.hdl != nil {
return d.hdl.Close()
}
return nil
}

func (d *DB) Erase() error {
// TODO: destroy the local file.
return nil
}

func (d *DB) Exists(ctx context.Context) (bool, error) {
return fileExistsAndIsRecent(d.File(), time.Second*15)
}

func (d *DB) IndexDevices(ctx context.Context, devList []tailscale.Device, enrichedDevList map[string]tailscale_cli.DeviceInfo) error {
// Start a writable transaction.
err := d.hdl.Update(func(tx *bolt.Tx) error {
// Create all buckets.

// If the bucket already exists, it will return a reference to it.
devicesBucket, err := tx.CreateBucketIfNotExists([]byte(devicesBucket))
if err != nil {
return err
}

enrichedBucket, err := tx.CreateBucketIfNotExists([]byte(enrichedBucket))
if err != nil {
return err
}

// Iterate over all devices.
for _, dev := range devList {
// Encode as JSON: in the future encode as Proto/more compact form.
encoded, err := json.Marshal(dev)
if err != nil {
return err
}

// The key is the data point ID converted to a byte slice.
// The value is the encoded JSON data.
err = devicesBucket.Put([]byte(dev.Name), encoded)
if err != nil {
return err
}
}

// Iterate over all enriched info.
for _, enr := range enrichedDevList {
// Encode as JSON: in the future encode as Proto/more compact form.
encoded, err := json.Marshal(enr)
if err != nil {
return err
}

// The key is the data point ID converted to a byte slice.
// The value is the encoded JSON data.
err = enrichedBucket.Put([]byte(enr.NodeKey), encoded)
if err != nil {
return err
}
}

return nil
})

if err != nil {
log.Fatal("error updating the database index", "error", err)
}

return nil
}

func (d *DB) FindDevices(ctx context.Context) ([]tailscale.Device, map[string]tailscale_cli.DeviceInfo, error) {
// 0. First populate all devices.
// TODO: index metadata like the size of devices then we can instantiate with the correct capacity.
devList := make([]tailscale.Device, 0)
err := d.hdl.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := tx.Bucket([]byte(devicesBucket))
if b == nil {
return errors.New("bucket is unknown: " + devicesBucket)
}
c := b.Cursor()

// This is a linear scan over all key/values
for k, v := c.First(); k != nil; k, v = c.Next() {
//fmt.Printf("key=%s, value=%s\n", k, v)
//fmt.Printf("key=%s\n", k)

var dev tailscale.Device
err := json.Unmarshal(v, &dev)
if err != nil {
return err
}

devList = append(devList, dev)
}

// TODO: need to set this up.
// Prefix scan (use this in the future)
//prefix := []byte("1234")
//for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
// fmt.Printf("key=%s, value=%s\n", k, v)
//}

return nil
})
if err != nil {
return nil, nil, err
}

// 2. Next populate all enriched data
enrichedDevs := make(map[string]tailscale_cli.DeviceInfo)
err = d.hdl.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := tx.Bucket([]byte(enrichedBucket))
c := b.Cursor()

// This is a linear scan over all key/values
for k, v := c.First(); k != nil; k, v = c.Next() {
//fmt.Printf("key=%s, value=%s\n", k, v)
//fmt.Printf("key=%s\n", k)

var dev tailscale_cli.DeviceInfo
err := json.Unmarshal(v, &dev)
if err != nil {
return err
}

enrichedDevs[string(k)] = dev
}

// TODO: need to set this up.
// Prefix scan (use this in the future)
//prefix := []byte("1234")
//for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
// fmt.Printf("key=%s, value=%s\n", k, v)
//}

return nil
})
if err != nil {
log.Fatal("error occurred attempting to lookup enriched devices", "error", err)
}

return devList, enrichedDevs, nil
}
53 changes: 52 additions & 1 deletion pkg/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,66 @@ package pkg

import (
"context"
"os"
"time"
"tips/pkg/tailscale_cli"

"github.com/charmbracelet/log"
"github.com/tailscale/tailscale-client-go/tailscale"
)

func fileExistsAndIsRecent(filePath string, duration time.Duration) (bool, error) {
// Check if the file exists
info, err := os.Stat(filePath)
if os.IsNotExist(err) {
// The file does not exist
return false, nil
} else if err != nil {
// There was some other error getting the file info
return false, err
}

// Check the time since the file was created
creationTime := info.ModTime()
if time.Since(creationTime) <= duration {
// The file is recent enough
return true, nil
}

// The file exists but is not recent
return false, nil
}

func DevicesResource(ctx context.Context, client *tailscale.Client) ([]tailscale.Device, map[string]tailscale_cli.DeviceInfo, error) {
cfg := CtxAsConfig(ctx, CtxKeyConfig)
startTime := time.Now()
defer func() {
cfg.TailscaleAPI.ElapsedTime = time.Since(startTime)
}()

// 0. Check cache - return cached results if cache timeout not yet expired.
// 0. Check this index first.
indexedDB := NewDB("deckarep@gmail.com")

existsAndRecent, err := indexedDB.Exists(ctx)
if err != nil {
log.Warn("problem checking for bolt db file", "error", err)
}

err = indexedDB.Open()
if err != nil {
return nil, nil, err
}
defer indexedDB.Close()

if true {
if devList, enrichedDevs, err := indexedDB.FindDevices(ctx); existsAndRecent && err == nil {
log.Warn("found a bolt file and its recent enough so that will be used!")
return devList, enrichedDevs, nil
}
}
log.Warn("bolt file has expired or must be regenerated")

// TODO: 0. Check cache config - return cached results if cache timeout not yet expired.
if cfg.NoCache {
log.Warn("--nocache is not yet support, but this should force a refresh of data in cache")
}
Expand All @@ -60,5 +105,11 @@ func DevicesResource(ctx context.Context, client *tailscale.Client) ([]tailscale
log.Debug("unable to get enriched data from tailscale cli", "error", err)
}

// 3. Index this data.
err = indexedDB.IndexDevices(ctx, devList, enrichedDevices)
if err != nil {
log.Debug("unable to index the devices", "error", err)
}

return devList, enrichedDevices, nil
}
Loading

0 comments on commit 21c37cf

Please sign in to comment.