From ef072700a4eb0f0a4a52693f539ef71676712374 Mon Sep 17 00:00:00 2001 From: "Mariano Z." Date: Tue, 20 Aug 2024 21:11:30 -0300 Subject: [PATCH] feat: added a bunch of shit --- .goreleaser.yaml | 13 ++++++ cmd/root.go | 10 ++++- cmd/version.go | 43 ++++++++++++++++++ internal/program/parser.go | 37 +++------------ internal/program/program.go | 11 +++-- internal/program/sync.go | 3 +- internal/storage/db.go | 89 ++++++++++++++++++++++++++----------- internal/storage/model.go | 2 +- 8 files changed, 144 insertions(+), 64 deletions(-) create mode 100644 cmd/version.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f4ed0e0..41422dc 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -22,6 +22,19 @@ builds: - linux - darwin + ldflags: "-s -w -X github.com/marianozunino/sdm-ui/cmd.Version={{.Version}}" + +release: + github: + owner: marianozunino + name: sdm-ui + name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" + + # You can disable this pipe in order to not upload any artifacts to + # GitHub. + # Defaults to false. + disable: false + archives: - format: tar.gz # this name template makes the OS and Arch compatible with the results of `uname`. diff --git a/cmd/root.go b/cmd/root.go index 16500fd..be08f0b 100755 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,8 +45,14 @@ var confData conf = conf{} var rootCmd = &cobra.Command{ Use: "sdm-ui", Short: "SDM UI - Wrapper for SDM CLI", - Long: `SDM UI is a custom wrapper around StrongDM (SDM) -designed to improve the developer experience (DX) on Linux.`, + Long: ` + ___ ___ __ __ _ _ ___ +/ __| \| \/ | | | | |_ _| +\__ \ |) | |\/| | | |_| || | +|___/___/|_| |_| \___/|___| ` + VersionFromBuild() + ` + +SDM UI is a custom wrapper around StrongDM (SDM) designed to improve the developer experience (DX) on Linux.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // You can bind cobra and viper in a few locations, but PersistencePreRunE on the root command works well return initializeConfig(cmd) diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..566136d --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "fmt" + "runtime/debug" + + "github.com/spf13/cobra" +) + +var Version = "0.0.0" + +func init() { + rootCmd.AddCommand(versionCmd) +} + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of sdm ui", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + version := VersionFromBuild() + fmt.Printf("SDM UI Version: %s\n", version) + }, +} + +// Version returns the version of txeh binary +func VersionFromBuild() (version string) { + // Version is managed with goreleaser + if Version != "0.0.0" { + return Version + } + // Version is managed by "go install" + b, ok := debug.ReadBuildInfo() + if !ok { + return "unknown" + } + if b == nil { + version = "nil" + } else { + version = b.Main.Version + } + return version +} diff --git a/internal/program/parser.go b/internal/program/parser.go index aec5739..a1f4ecb 100644 --- a/internal/program/parser.go +++ b/internal/program/parser.go @@ -2,7 +2,6 @@ package program import ( "encoding/json" - "fmt" "github.com/marianozunino/sdm-ui/internal/storage" "github.com/rs/zerolog/log" @@ -47,43 +46,21 @@ func parseDataSources(rawResources string) []storage.DataSource { var dataSources []storage.DataSource for _, resource := range resources { - if !isValidType(resource.Type) { - log.Debug().Msgf("Skipping invalid resource type: %s", resource.Type) - continue - } - dataSource := storage.DataSource{ Name: resource.Name, Status: resource.ConnectionStatus, Type: resource.Type, Tags: resource.Tags, - Address: formatAddress(resource.Type, resource.Message), + Address: resource.Address, + WebURL: resource.WebURL, } - dataSources = append(dataSources, dataSource) - } - return dataSources -} + if resource.Address == "" { + dataSource.Address = resource.Message + } -// isValidType checks if the resource type is one of the recognized types. -func isValidType(resourceType string) bool { - switch ResourceType(resourceType) { - case TypeRedis, TypePostgres, TypeAmazonEKS, TypeAmazonES, TypeAthena, TypeAmazonMQAMQP, TypeRawTCP: - return true - default: - return false + dataSources = append(dataSources, dataSource) } -} -// formatAddress formats the address for certain resource types. -func formatAddress(resourceType string, message string) string { - switch ResourceType(resourceType) { - case TypeAmazonES: - return fmt.Sprintf("http://%s/_plugin/kibana/app/kibana", message) - case TypeRawTCP: - return fmt.Sprintf("https://%s", message) - default: - return message - } + return dataSources } - diff --git a/internal/program/program.go b/internal/program/program.go index 76d735c..2f53a5c 100644 --- a/internal/program/program.go +++ b/internal/program/program.go @@ -96,12 +96,17 @@ func (p *Program) validateAccount() error { func printDataSources(dataSources []storage.DataSource, w io.Writer) { const format = "%v\t%v\t%v\n" - tw := tabwriter.NewWriter(w, 0, 8, 1, '\t', 0) + tw := tabwriter.NewWriter(w, 0, 8, 2, '\t', 0) for _, ds := range dataSources { - status := "🔒" + status := "🔌" + if ds.Status == "connected" { - status = "✅" + status = "⚡" + } + + if ds.WebURL != "" { + status = "🌐" } fmt.Fprintf(tw, format, ds.Name, ellipsize(ds.Address, 20), status) diff --git a/internal/program/sync.go b/internal/program/sync.go index 94e35a0..3b65655 100644 --- a/internal/program/sync.go +++ b/internal/program/sync.go @@ -2,7 +2,6 @@ package program import ( "bytes" - "fmt" "github.com/rs/zerolog/log" ) @@ -15,7 +14,7 @@ func (p *Program) Sync() error { statusesBuffer.Reset() return p.sdmWrapper.Status(statusesBuffer) }); err != nil { - fmt.Println("[sync] Failed to sync with SDM") + log.Debug().Msg("Failed to sync with SDM") return err } diff --git a/internal/storage/db.go b/internal/storage/db.go index 05567ff..e85b9b6 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -3,12 +3,18 @@ package storage import ( "fmt" "path/filepath" + "strconv" + "strings" "github.com/rs/zerolog/log" bolt "go.etcd.io/bbolt" ) -var datasourceBucketKey = []byte("datasource") +const ( + datasourceBucketPrefix = "datasource" + currentDBVersion = 1 // increment this whenever the database schema changes + retentionPeriod = 2 +) type Storage struct { *bolt.DB @@ -19,27 +25,25 @@ type Storage struct { func NewStorage(account string, path string) (*Storage, error) { dbPath := filepath.Join(path, "sdm-sources.db") log.Debug().Msgf("Opening database at %s", dbPath) - db, err := bolt.Open(dbPath, 0600, nil) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } - storage := &Storage{ DB: db, account: account, } - if err := storage.ensureBucketExists(); err != nil { return nil, err } + storage.removeOldBuckets(retentionPeriod) return storage, nil } // ensureBucketExists ensures that the bucket for the given account exists in the database. func (s *Storage) ensureBucketExists() error { - bucketKey := buildBucketKey(s.account) + bucketKey := buildBucketKey(s.account, currentDBVersion) return s.Update(func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists(bucketKey) if err != nil { @@ -49,34 +53,31 @@ func (s *Storage) ensureBucketExists() error { }) } -// buildBucketKey constructs a bucket key using the account and the datasource bucket key. -func buildBucketKey(account string) []byte { - return []byte(fmt.Sprintf("%s:%s", account, datasourceBucketKey)) +// buildBucketKey constructs a bucket key using the account and the database version. +func buildBucketKey(account string, version int) []byte { + return []byte(fmt.Sprintf("%s:%s:v%d", account, datasourceBucketPrefix, version)) } // StoreServers stores the provided datasources for the specified account. func (s *Storage) StoreServers(datasources []DataSource) error { + bucketKey := buildBucketKey(s.account, currentDBVersion) return s.Update(func(tx *bolt.Tx) error { log.Debug().Msgf("Storing %d datasources", len(datasources)) - - bucket := tx.Bucket(buildBucketKey(s.account)) + bucket := tx.Bucket(bucketKey) if bucket == nil { return fmt.Errorf("bucket for account %s not found", s.account) } - for _, ds := range datasources { // Encode the DataSource and handle any errors encodedData, err := ds.Encode() if err != nil { return fmt.Errorf("failed to encode datasource %s: %w", ds.Name, err) } - // Store the encoded DataSource in the bucket if err := bucket.Put(ds.Key(), encodedData); err != nil { return fmt.Errorf("failed to store datasource %s: %w", ds.Name, err) } } - log.Debug().Msgf("Successfully stored %d datasources", len(datasources)) return nil }) @@ -85,14 +86,12 @@ func (s *Storage) StoreServers(datasources []DataSource) error { // RetrieveDatasources retrieves all datasources for the specified account. func (s *Storage) RetrieveDatasources() ([]DataSource, error) { var datasources []DataSource - bucketKey := buildBucketKey(s.account) - + bucketKey := buildBucketKey(s.account, currentDBVersion) err := s.View(func(tx *bolt.Tx) error { bucket := tx.Bucket(bucketKey) if bucket == nil { return fmt.Errorf("bucket for account %s not found", s.account) } - cursor := bucket.Cursor() for k, v := cursor.First(); k != nil; k, v = cursor.Next() { var ds DataSource @@ -102,43 +101,81 @@ func (s *Storage) RetrieveDatasources() ([]DataSource, error) { } datasources = append(datasources, ds) } - return nil }) - return datasources, err } // GetDatasource retrieves a single datasource by name for the specified account. func (s *Storage) GetDatasource(name string) (DataSource, error) { var datasource DataSource - bucketKey := buildBucketKey(s.account) - + bucketKey := buildBucketKey(s.account, currentDBVersion) err := s.View(func(tx *bolt.Tx) error { bucket := tx.Bucket(bucketKey) if bucket == nil { return fmt.Errorf("bucket for account %s not found", bucketKey) } - value := bucket.Get([]byte(name)) if value == nil { return fmt.Errorf("datasource %s not found", name) } - if err := datasource.Decode(value); err != nil { return fmt.Errorf("failed to decode datasource %s: %w", name, err) } - return nil }) - return datasource, err } +// removeOldBuckets removes old buckets that are older than the specified retention period. +func (s *Storage) removeOldBuckets(retentionPeriod int) error { + return s.Update(func(tx *bolt.Tx) error { + c := tx.Cursor() + for k, _ := c.First(); k != nil; k, _ = c.Next() { + parts := strings.Split(string(k), ":") + + if len(parts) == 2 { + log.Debug().Msgf("Removing bucket without version: %s", k) + if err := tx.DeleteBucket([]byte(k)); err != nil { + return fmt.Errorf("failed to delete bucket %s: %w", k, err) + } + continue + } else if len(parts) == 3 { + var version int + // Bucket has a version + var err error + version, err = strconv.Atoi(parts[len(parts)-1][1:]) + if err != nil { + log.Error().Msgf("Failed to parse version from bucket: %s, error: %v", k, err) + continue + } + + if currentDBVersion-version > retentionPeriod { + log.Debug().Msgf("Removing old bucket: %s", k) + if err := tx.DeleteBucket([]byte(k)); err != nil { + return fmt.Errorf("failed to delete bucket %s: %w", k, err) + } + } + } else { + log.Error().Msgf("Failed to parse bucket: %s", k) + continue + } + + } + return nil + }) +} + func (s *Storage) Wipe() error { return s.Update(func(tx *bolt.Tx) error { - if err := tx.DeleteBucket(buildBucketKey(s.account)); err != nil { - return fmt.Errorf("failed to delete bucket for account %s: %w", s.account, err) + c := tx.Cursor() + for k, _ := c.First(); k != nil; k, _ = c.Next() { + log.Debug().Msgf("Removing bucket: %s", k) + if strings.HasPrefix(string(k), s.account) { + if err := tx.DeleteBucket([]byte(k)); err != nil { + return fmt.Errorf("failed to delete bucket %s: %w", k, err) + } + } } return nil }) diff --git a/internal/storage/model.go b/internal/storage/model.go index 4b78fbf..852c29a 100644 --- a/internal/storage/model.go +++ b/internal/storage/model.go @@ -12,6 +12,7 @@ type DataSource struct { Address string Type string Tags string + WebURL string } // Encode serializes the DataSource into a byte slice. @@ -37,4 +38,3 @@ func (ds *DataSource) Decode(data []byte) error { func (ds DataSource) Key() []byte { return []byte(ds.Name) } -