-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9d7977b
commit 0ad022e
Showing
16 changed files
with
807 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package dao | ||
|
||
// Describe describes a resource. | ||
func Describe(resource, path string) (string, error) { | ||
return "Hello", nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package dao | ||
|
||
import "regexp" | ||
|
||
var ( | ||
fuzzyRx = regexp.MustCompile(`\A\-f`) | ||
) | ||
|
||
// IsFuzzySelector checks if filter is fuzzy or not. | ||
func IsFuzzySelector(s string) bool { | ||
if s == "" { | ||
return false | ||
} | ||
return fuzzyRx.MatchString(s) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
package model | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"reflect" | ||
"regexp" | ||
"strings" | ||
"sync/atomic" | ||
"time" | ||
|
||
backoff "github.com/cenkalti/backoff/v4" | ||
"github.com/one2nc/cloud-lens/internal/dao" | ||
"github.com/rs/zerolog/log" | ||
"github.com/sahilm/fuzzy" | ||
) | ||
|
||
// Describe tracks describable resources. | ||
type Describe struct { | ||
resource string | ||
inUpdate int32 | ||
path string | ||
query string | ||
lines []string | ||
refreshRate time.Duration | ||
listeners []ResourceViewerListener | ||
} | ||
|
||
// NewDescribe returns a new describe resource model. | ||
func NewDescribe(res string, path string) *Describe { | ||
return &Describe{ | ||
resource: res, | ||
path: path, | ||
refreshRate: defaultReaderRefreshRate, | ||
} | ||
} | ||
|
||
// GetPath returns the active resource path. | ||
func (d *Describe) GetPath() string { | ||
return d.path | ||
} | ||
|
||
// SetOptions toggle model options. | ||
func (d *Describe) SetOptions(context.Context, ViewerToggleOpts) {} | ||
|
||
// Filter filters the model. | ||
func (d *Describe) Filter(q string) { | ||
d.query = q | ||
d.filterChanged(d.lines) | ||
} | ||
|
||
func (d *Describe) filterChanged(lines []string) { | ||
d.fireResourceChanged(lines, d.filter(d.query, lines)) | ||
} | ||
|
||
func (d *Describe) filter(q string, lines []string) fuzzy.Matches { | ||
if q == "" { | ||
return nil | ||
} | ||
if dao.IsFuzzySelector(q) { | ||
return d.fuzzyFilter(strings.TrimSpace(q[2:]), lines) | ||
} | ||
return d.rxFilter(q, lines) | ||
} | ||
|
||
func (*Describe) fuzzyFilter(q string, lines []string) fuzzy.Matches { | ||
return fuzzy.Find(q, lines) | ||
} | ||
|
||
func (*Describe) rxFilter(q string, lines []string) fuzzy.Matches { | ||
rx, err := regexp.Compile(`(?i)` + q) | ||
if err != nil { | ||
return nil | ||
} | ||
matches := make(fuzzy.Matches, 0, len(lines)) | ||
for i, l := range lines { | ||
if loc := rx.FindStringIndex(l); len(loc) == 2 { | ||
matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc}) | ||
} | ||
} | ||
|
||
return matches | ||
} | ||
|
||
func (d *Describe) fireResourceChanged(lines []string, matches fuzzy.Matches) { | ||
for _, l := range d.listeners { | ||
l.ResourceChanged(lines, matches) | ||
} | ||
} | ||
|
||
func (d *Describe) fireResourceFailed(err error) { | ||
for _, l := range d.listeners { | ||
l.ResourceFailed(err) | ||
} | ||
} | ||
|
||
// ClearFilter clear out the filter. | ||
func (d *Describe) ClearFilter() { | ||
} | ||
|
||
// Peek returns current model state. | ||
func (d *Describe) Peek() []string { | ||
return d.lines | ||
} | ||
|
||
// Refresh updates model data. | ||
func (d *Describe) Refresh(ctx context.Context) error { | ||
return d.refresh(ctx) | ||
} | ||
|
||
// Watch watches for describe data changes. | ||
func (d *Describe) Watch(ctx context.Context) error { | ||
if err := d.refresh(ctx); err != nil { | ||
return err | ||
} | ||
go d.updater(ctx) | ||
return nil | ||
} | ||
|
||
func (d *Describe) updater(ctx context.Context) { | ||
defer log.Debug().Msgf("Describe canceled -- %q", d.resource) | ||
|
||
backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval) | ||
delay := defaultReaderRefreshRate | ||
for { | ||
select { | ||
case <-ctx.Done(): | ||
return | ||
case <-time.After(delay): | ||
if err := d.refresh(ctx); err != nil { | ||
d.fireResourceFailed(err) | ||
if delay = backOff.NextBackOff(); delay == backoff.Stop { | ||
log.Error().Err(err).Msgf("Describe gave up!") | ||
return | ||
} | ||
} else { | ||
backOff.Reset() | ||
delay = defaultReaderRefreshRate | ||
} | ||
} | ||
} | ||
} | ||
|
||
func (d *Describe) refresh(ctx context.Context) error { | ||
if !atomic.CompareAndSwapInt32(&d.inUpdate, 0, 1) { | ||
log.Debug().Msgf("Dropping update...") | ||
return nil | ||
} | ||
defer atomic.StoreInt32(&d.inUpdate, 0) | ||
|
||
if err := d.reconcile(ctx); err != nil { | ||
log.Error().Err(err).Msgf("reconcile failed %q", d.resource) | ||
d.fireResourceFailed(err) | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (d *Describe) reconcile(ctx context.Context) error { | ||
s, err := d.describe(ctx, d.resource, d.path) | ||
if err != nil { | ||
return err | ||
} | ||
lines := strings.Split(s, "\n") | ||
if reflect.DeepEqual(lines, d.lines) { | ||
return nil | ||
} | ||
d.lines = lines | ||
d.fireResourceChanged(d.lines, d.filter(d.query, d.lines)) | ||
|
||
return nil | ||
} | ||
|
||
// Describe describes a given resource. | ||
func (d *Describe) describe(ctx context.Context, resource, path string) (string, error) { | ||
meta, err := getMeta(ctx, resource) | ||
if err != nil { | ||
return "", err | ||
} | ||
desc, ok := meta.DAO.(dao.Describer) | ||
if !ok { | ||
return "", fmt.Errorf("no describer") | ||
} | ||
|
||
return desc.Describe(path) | ||
} | ||
|
||
// AddListener adds a new model listener. | ||
func (d *Describe) AddListener(l ResourceViewerListener) { | ||
d.listeners = append(d.listeners, l) | ||
} | ||
|
||
// RemoveListener delete a listener from the list. | ||
func (d *Describe) RemoveListener(l ResourceViewerListener) { | ||
victim := -1 | ||
for i, lis := range d.listeners { | ||
if lis == l { | ||
victim = i | ||
break | ||
} | ||
} | ||
|
||
if victim >= 0 { | ||
d.listeners = append(d.listeners[:victim], d.listeners[victim+1:]...) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package model | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/cenkalti/backoff" | ||
) | ||
|
||
func NewExpBackOff(ctx context.Context, start, max time.Duration) backoff.BackOffContext { | ||
bf := backoff.NewExponentialBackOff() | ||
bf.InitialInterval, bf.MaxElapsedTime = start, max | ||
return backoff.WithContext(bf, ctx) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.