From 1b3b9444080e433d5e30e79aeff0585c09e3362c Mon Sep 17 00:00:00 2001 From: Louis Garman Date: Tue, 14 Jan 2025 07:36:16 +0000 Subject: [PATCH 1/5] wip --- internal/module/service_test.go | 4 ++-- internal/resource/id.go | 10 +++------- internal/tui/table/table.go | 12 ++++++------ internal/tui/workspace/state_func.go | 2 +- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/internal/module/service_test.go b/internal/module/service_test.go index 5902300..61f83ab 100644 --- a/internal/module/service_test.go +++ b/internal/module/service_test.go @@ -71,11 +71,11 @@ digraph { assert.Len(t, vpc.Dependencies(), 0) // redis if assert.Len(t, redis.Dependencies(), 1) { - assert.Equal(t, vpc.ID, redis.Dependencies()[0].GetID()) + assert.Equal(t, vpc.ID, redis.Dependencies()[0]) } // mysql if assert.Len(t, mysql.Dependencies(), 1) { - assert.Equal(t, vpc.ID, mysql.Dependencies()[0].GetID()) + assert.Equal(t, vpc.ID, mysql.Dependencies()[0]) } // backend if assert.Len(t, backend.Dependencies(), 3) { diff --git a/internal/resource/id.go b/internal/resource/id.go index b3b4b58..1e55182 100644 --- a/internal/resource/id.go +++ b/internal/resource/id.go @@ -30,16 +30,12 @@ func NewID(kind Kind) ID { } } +// String provides a human readable description. func (id ID) String() string { return fmt.Sprintf("#%d", id.Serial) } -// GetID allows ID to be accessed via an interface value. -func (id ID) GetID() ID { +// GetID returns a comparable value. +func (id ID) GetID() any { return id } - -// GetKind allows Kind to be accessed via an interface value. -func (id ID) GetKind() Kind { - return id.Kind -} diff --git a/internal/tui/table/table.go b/internal/tui/table/table.go index eba2de2..4f70937 100644 --- a/internal/tui/table/table.go +++ b/internal/tui/table/table.go @@ -28,7 +28,7 @@ const ( ) // Model defines a state for the table widget. -type Model[V resource.Resource] struct { +type Model[V fmt.Stringer] struct { cols []Column rows []Row[V] rowRenderer RowRenderer[V] @@ -88,7 +88,7 @@ type RenderedRow map[ColumnKey]string type SortFunc[V any] func(V, V) int // New creates a new model for the table widget. -func New[V resource.Resource](cols []Column, fn RowRenderer[V], width, height int, opts ...Option[V]) Model[V] { +func New[V fmt.Stringer](cols []Column, fn RowRenderer[V], width, height int, opts ...Option[V]) Model[V] { filter := textinput.New() filter.Prompt = "Filter: " @@ -121,17 +121,17 @@ func New[V resource.Resource](cols []Column, fn RowRenderer[V], width, height in return m } -type Option[V resource.Resource] func(m *Model[V]) +type Option[V fmt.Stringer] func(m *Model[V]) // WithSortFunc configures the table to sort rows using the given func. -func WithSortFunc[V resource.Resource](sortFunc func(V, V) int) Option[V] { +func WithSortFunc[V fmt.Stringer](sortFunc func(V, V) int) Option[V] { return func(m *Model[V]) { m.sortFunc = sortFunc } } // WithSelectable sets whether rows are selectable. -func WithSelectable[V resource.Resource](s bool) Option[V] { +func WithSelectable[V fmt.Stringer](s bool) Option[V] { return func(m *Model[V]) { m.selectable = s } @@ -139,7 +139,7 @@ func WithSelectable[V resource.Resource](s bool) Option[V] { // WithPreview configures the table to automatically populate the bottom right // pane with a model corresponding to the current row. -func WithPreview[V resource.Resource](maker tui.Kind) Option[V] { +func WithPreview[V fmt.Stringer](maker tui.Kind) Option[V] { return func(m *Model[V]) { m.previewKind = &maker } diff --git a/internal/tui/workspace/state_func.go b/internal/tui/workspace/state_func.go index a093d6a..e7cf005 100644 --- a/internal/tui/workspace/state_func.go +++ b/internal/tui/workspace/state_func.go @@ -13,7 +13,7 @@ func (m resourceList) createStateCommand(fn stateFunc, addrs ...state.ResourceAd // Make N copies of the workspace ID where N is the number of addresses workspaceIDs := make([]resource.ID, len(addrs)) for i := range workspaceIDs { - workspaceIDs[i] = m.workspace.GetID() + workspaceIDs[i] = m.workspace.ID } f := newStateTaskFunc(fn, addrs...) return m.CreateTasks(f.createTask, workspaceIDs...) From 6f5aa6e99bd101895c49ff1197521924571e375e Mon Sep 17 00:00:00 2001 From: Louis Garman Date: Wed, 15 Jan 2025 07:44:16 +0000 Subject: [PATCH 2/5] wip --- internal/logging/enricher.go | 2 +- internal/logging/enricher_test.go | 2 +- internal/logging/logger.go | 2 +- internal/module/module.go | 2 +- internal/module/service.go | 22 ++--- internal/module/service_test.go | 2 +- internal/plan/plan.go | 2 +- internal/plan/plan_test.go | 4 +- internal/plan/service.go | 14 +-- internal/pubsub/broker.go | 4 +- internal/resource/event.go | 2 +- internal/resource/id.go | 11 ++- internal/resource/resource.go | 2 - internal/resource/table.go | 12 +-- internal/state/reloader.go | 4 +- internal/state/resource.go | 4 +- internal/state/service.go | 14 +-- internal/state/state.go | 4 +- internal/task/enqueuer.go | 2 +- internal/task/enqueuer_test.go | 2 +- internal/task/service.go | 10 +- internal/task/spec.go | 2 +- internal/tui/actions.go | 12 +-- internal/tui/explorer/model.go | 8 +- internal/tui/explorer/selector.go | 6 +- internal/tui/explorer/selector_test.go | 4 +- internal/tui/explorer/tracker.go | 6 +- internal/tui/explorer/tree.go | 6 +- internal/tui/helpers.go | 16 +-- internal/tui/logs/list.go | 2 +- internal/tui/logs/model.go | 2 +- internal/tui/messages.go | 2 +- internal/tui/model.go | 8 +- internal/tui/pane_manager.go | 2 +- internal/tui/table/table.go | 124 +++++++++++------------- internal/tui/table/table_test.go | 50 +++++----- internal/tui/task/cancel.go | 2 +- internal/tui/task/group.go | 4 +- internal/tui/task/group_list.go | 2 +- internal/tui/task/list.go | 40 ++++---- internal/tui/task/model.go | 14 +-- internal/tui/workspace/resource.go | 10 +- internal/tui/workspace/resource_list.go | 22 ++--- internal/tui/workspace/state_func.go | 6 +- internal/workspace/cost.go | 2 +- internal/workspace/reloader.go | 7 +- internal/workspace/reloader_test.go | 22 +++-- internal/workspace/service.go | 22 ++--- 48 files changed, 260 insertions(+), 266 deletions(-) diff --git a/internal/logging/enricher.go b/internal/logging/enricher.go index 2d53335..ad80d3d 100644 --- a/internal/logging/enricher.go +++ b/internal/logging/enricher.go @@ -39,7 +39,7 @@ type ReferenceUpdater[T any] struct { } type Getter[T any] interface { - Get(resource.ID) (T, error) + Get(resource.Identity) (T, error) } func (e *ReferenceUpdater[T]) UpdateArgs(args ...any) []any { diff --git a/internal/logging/enricher_test.go b/internal/logging/enricher_test.go index 5881d5e..1b925eb 100644 --- a/internal/logging/enricher_test.go +++ b/internal/logging/enricher_test.go @@ -67,6 +67,6 @@ type fakeResourceGetter struct { res *fakeResource } -func (f *fakeResourceGetter) Get(resource.ID) (*fakeResource, error) { +func (f *fakeResourceGetter) Get(resource.Identity) (*fakeResource, error) { return f.res, nil } diff --git a/internal/logging/logger.go b/internal/logging/logger.go index 428616b..1be728d 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -99,6 +99,6 @@ func (l *Logger) List() []Message { } // Get retrieves a log message by ID. -func (l *Logger) Get(id resource.ID) (Message, error) { +func (l *Logger) Get(id resource.Identity) (Message, error) { return l.writer.table.Get(id) } diff --git a/internal/module/module.go b/internal/module/module.go index 70c5825..fc0263e 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -21,7 +21,7 @@ type Module struct { // Path relative to pug working directory Path string // The module's current workspace. - CurrentWorkspaceID *resource.ID + CurrentWorkspaceID resource.Identity // The module's backend type Backend string diff --git a/internal/module/service.go b/internal/module/service.go index 01d4090..fa4a055 100644 --- a/internal/module/service.go +++ b/internal/module/service.go @@ -39,10 +39,10 @@ type taskCreator interface { } type moduleTable interface { - Add(id resource.ID, row *Module) - Update(id resource.ID, updater func(existing *Module) error) (*Module, error) - Delete(id resource.ID) - Get(id resource.ID) (*Module, error) + Add(id resource.Identity, row *Module) + Update(id resource.Identity, updater func(existing *Module) error) (*Module, error) + Delete(id resource.Identity) + Get(id resource.Identity) (*Module, error) List() []*Module } @@ -205,7 +205,7 @@ func (s *Service) loadTerragruntDependenciesFromDigraph(r io.Reader) error { const InitTask task.Identifier = "init" // Init invokes terraform init on the module. -func (s *Service) Init(moduleID resource.ID, upgrade bool) (task.Spec, error) { +func (s *Service) Init(moduleID resource.Identity, upgrade bool) (task.Spec, error) { mod, err := s.table.Get(moduleID) if err != nil { return task.Spec{}, err @@ -230,7 +230,7 @@ func (s *Service) Init(moduleID resource.ID, upgrade bool) (task.Spec, error) { return spec, nil } -func (s *Service) Format(moduleID resource.ID) (task.Spec, error) { +func (s *Service) Format(moduleID resource.Identity) (task.Spec, error) { mod, err := s.table.Get(moduleID) if err != nil { return task.Spec{}, err @@ -247,7 +247,7 @@ func (s *Service) Format(moduleID resource.ID) (task.Spec, error) { return spec, nil } -func (s *Service) Validate(moduleID resource.ID) (task.Spec, error) { +func (s *Service) Validate(moduleID resource.Identity) (task.Spec, error) { mod, err := s.table.Get(moduleID) if err != nil { return task.Spec{}, err @@ -268,7 +268,7 @@ func (s *Service) List() []*Module { return s.table.List() } -func (s *Service) Get(id resource.ID) (*Module, error) { +func (s *Service) Get(id resource.Identity) (*Module, error) { return s.table.Get(id) } @@ -282,16 +282,16 @@ func (s *Service) GetByPath(path string) (*Module, error) { } // SetCurrent sets the current workspace for the module. -func (s *Service) SetCurrent(moduleID, workspaceID resource.ID) error { +func (s *Service) SetCurrent(moduleID, workspaceID resource.Identity) error { _, err := s.table.Update(moduleID, func(existing *Module) error { - existing.CurrentWorkspaceID = &workspaceID + existing.CurrentWorkspaceID = workspaceID return nil }) return err } // Execute a program in a module's directory. -func (s *Service) Execute(moduleID resource.ID, program string, args ...string) (task.Spec, error) { +func (s *Service) Execute(moduleID resource.Identity, program string, args ...string) (task.Spec, error) { mod, err := s.table.Get(moduleID) if err != nil { return task.Spec{}, err diff --git a/internal/module/service_test.go b/internal/module/service_test.go index 61f83ab..b81d18d 100644 --- a/internal/module/service_test.go +++ b/internal/module/service_test.go @@ -101,7 +101,7 @@ func (f *fakeModuleTable) List() []*Module { return f.modules } -func (f *fakeModuleTable) Update(id resource.ID, updater func(*Module) error) (*Module, error) { +func (f *fakeModuleTable) Update(id resource.Identity, updater func(*Module) error) (*Module, error) { for _, mod := range f.modules { if mod.ID == id { if err := updater(mod); err != nil { diff --git a/internal/plan/plan.go b/internal/plan/plan.go index 29feff6..6b58f40 100644 --- a/internal/plan/plan.go +++ b/internal/plan/plan.go @@ -56,7 +56,7 @@ type factory struct { terragrunt bool } -func (f *factory) newPlan(workspaceID resource.ID, opts CreateOptions) (*plan, error) { +func (f *factory) newPlan(workspaceID resource.Identity, opts CreateOptions) (*plan, error) { ws, err := f.workspaces.Get(workspaceID) if err != nil { return nil, fmt.Errorf("retrieving workspace: %w", err) diff --git a/internal/plan/plan_test.go b/internal/plan/plan_test.go index f3641f6..e018026 100644 --- a/internal/plan/plan_test.go +++ b/internal/plan/plan_test.go @@ -61,7 +61,7 @@ type fakeModuleGetter struct { mod *module.Module } -func (f *fakeModuleGetter) Get(resource.ID) (*module.Module, error) { +func (f *fakeModuleGetter) Get(resource.Identity) (*module.Module, error) { return f.mod, nil } @@ -69,6 +69,6 @@ type fakeWorkspaceGetter struct { ws *workspace.Workspace } -func (f *fakeWorkspaceGetter) Get(resource.ID) (*workspace.Workspace, error) { +func (f *fakeWorkspaceGetter) Get(resource.Identity) (*workspace.Workspace, error) { return f.ws, nil } diff --git a/internal/plan/service.go b/internal/plan/service.go index 518914d..4b44ad2 100644 --- a/internal/plan/service.go +++ b/internal/plan/service.go @@ -39,11 +39,11 @@ type ServiceOptions struct { } type moduleGetter interface { - Get(moduleID resource.ID) (*module.Module, error) + Get(moduleID resource.Identity) (*module.Module, error) } type workspaceGetter interface { - Get(workspaceID resource.ID) (*workspace.Workspace, error) + Get(workspaceID resource.Identity) (*workspace.Workspace, error) } func NewService(opts ServiceOptions) *Service { @@ -94,7 +94,7 @@ func (s *Service) ReloadAfterApply(sub <-chan resource.Event[*task.Task]) { // Plan creates a task spec to create a plan, i.e. `terraform plan -out // plan.file`. -func (s *Service) Plan(workspaceID resource.ID, opts CreateOptions) (task.Spec, error) { +func (s *Service) Plan(workspaceID resource.Identity, opts CreateOptions) (task.Spec, error) { opts.planFile = true plan, err := s.newPlan(workspaceID, opts) if err != nil { @@ -108,7 +108,7 @@ func (s *Service) Plan(workspaceID resource.ID, opts CreateOptions) (task.Spec, // Apply creates a task spec to auto-apply a plan, i.e. `terraform apply`. To // apply an existing plan, see ApplyPlan. -func (s *Service) Apply(workspaceID resource.ID, opts CreateOptions) (task.Spec, error) { +func (s *Service) Apply(workspaceID resource.Identity, opts CreateOptions) (task.Spec, error) { plan, err := s.newPlan(workspaceID, opts) if err != nil { return task.Spec{}, err @@ -119,7 +119,7 @@ func (s *Service) Apply(workspaceID resource.ID, opts CreateOptions) (task.Spec, // ApplyPlan creates a task spec to apply an existing plan, i.e. `terraform // apply existing.plan`. The taskID is the ID of a plan task, which must have // finished successfully. -func (s *Service) ApplyPlan(taskID resource.ID) (task.Spec, error) { +func (s *Service) ApplyPlan(taskID resource.Identity) (task.Spec, error) { planTask, err := s.tasks.Get(taskID) if err != nil { return task.Spec{}, err @@ -144,11 +144,11 @@ func IsApplyable(t *task.Task) error { return nil } -func (s *Service) Get(runID resource.ID) (*plan, error) { +func (s *Service) Get(runID resource.Identity) (*plan, error) { return s.table.Get(runID) } -func (s *Service) getByTaskID(taskID resource.ID) (*plan, error) { +func (s *Service) getByTaskID(taskID resource.Identity) (*plan, error) { for _, plan := range s.List() { if plan.taskID != nil && *plan.taskID == taskID { return plan, nil diff --git a/internal/pubsub/broker.go b/internal/pubsub/broker.go index 5c07f9d..b6585be 100644 --- a/internal/pubsub/broker.go +++ b/internal/pubsub/broker.go @@ -17,7 +17,7 @@ type Logger interface { } // Broker allows clients to publish events and subscribe to events -type Broker[T resource.Resource] struct { +type Broker[T any] struct { subs map[chan resource.Event[T]]struct{} // subscriptions mu sync.Mutex // sync access to map done chan struct{} // close when broker is shutting down @@ -25,7 +25,7 @@ type Broker[T resource.Resource] struct { } // NewBroker constructs a pub/sub broker. -func NewBroker[T resource.Resource](logger Logger) *Broker[T] { +func NewBroker[T any](logger Logger) *Broker[T] { b := &Broker[T]{ subs: make(map[chan resource.Event[T]]struct{}), done: make(chan struct{}), diff --git a/internal/resource/event.go b/internal/resource/event.go index 2397037..d145a32 100644 --- a/internal/resource/event.go +++ b/internal/resource/event.go @@ -11,7 +11,7 @@ type ( EventType string // Event represents an event in the lifecycle of a resource - Event[T Resource] struct { + Event[T any] struct { Type EventType Payload T } diff --git a/internal/resource/id.go b/internal/resource/id.go index 1e55182..131f22d 100644 --- a/internal/resource/id.go +++ b/internal/resource/id.go @@ -5,6 +5,13 @@ import ( "sync" ) +// Identity is anything that uniquely differentiates a resource from another. +type Identity any + +type Identifiable interface { + GetID() Identity +} + var ( // nextID provides the next ID for each kind nextID map[Kind]uint = make(map[Kind]uint) @@ -35,7 +42,7 @@ func (id ID) String() string { return fmt.Sprintf("#%d", id.Serial) } -// GetID returns a comparable value. -func (id ID) GetID() any { +// GetID implements Identifiable +func (id ID) GetID() Identity { return id } diff --git a/internal/resource/resource.go b/internal/resource/resource.go index dd11501..59d92cb 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -4,8 +4,6 @@ package resource type Resource interface { // GetID retrieves the unique identifier for the resource. GetID() ID - // GetKind retrieves the kind of resource. - GetKind() Kind // String is a human-readable identifier for the resource. Not necessarily // unique across pug. String() string diff --git a/internal/resource/table.go b/internal/resource/table.go index 5baa74c..1bd3702 100644 --- a/internal/resource/table.go +++ b/internal/resource/table.go @@ -9,7 +9,7 @@ import ( // Table is an in-memory database table that emits events upon changes. type Table[T any] struct { - rows map[ID]T + rows map[Identity]T mu sync.RWMutex pub Publisher[T] @@ -17,12 +17,12 @@ type Table[T any] struct { func NewTable[T any](pub Publisher[T]) *Table[T] { return &Table[T]{ - rows: make(map[ID]T), + rows: make(map[Identity]T), pub: pub, } } -func (t *Table[T]) Add(id ID, row T) { +func (t *Table[T]) Add(id Identity, row T) { t.mu.Lock() defer t.mu.Unlock() @@ -30,7 +30,7 @@ func (t *Table[T]) Add(id ID, row T) { t.pub.Publish(CreatedEvent, row) } -func (t *Table[T]) Update(id ID, updater func(existing T) error) (T, error) { +func (t *Table[T]) Update(id Identity, updater func(existing T) error) (T, error) { t.mu.Lock() defer t.mu.Unlock() @@ -48,7 +48,7 @@ func (t *Table[T]) Update(id ID, updater func(existing T) error) (T, error) { return row, nil } -func (t *Table[T]) Delete(id ID) { +func (t *Table[T]) Delete(id Identity) { t.mu.Lock() defer t.mu.Unlock() @@ -57,7 +57,7 @@ func (t *Table[T]) Delete(id ID) { t.pub.Publish(DeletedEvent, row) } -func (t *Table[T]) Get(id ID) (T, error) { +func (t *Table[T]) Get(id Identity) (T, error) { t.mu.RLock() defer t.mu.RUnlock() diff --git a/internal/state/reloader.go b/internal/state/reloader.go index 70f34bc..e511dae 100644 --- a/internal/state/reloader.go +++ b/internal/state/reloader.go @@ -13,7 +13,7 @@ type reloader struct { // Reload creates a task to repopulate the local cache of the state of the given // workspace. -func (r *reloader) Reload(workspaceID resource.ID) (task.Spec, error) { +func (r *reloader) Reload(workspaceID resource.Identity) (task.Spec, error) { return r.createTaskSpec(workspaceID, task.Spec{ Execution: task.Execution{ TerraformCommand: []string{"state", "pull"}, @@ -42,7 +42,7 @@ func (r *reloader) Reload(workspaceID resource.ID) (task.Spec, error) { }) } -func (s *Service) CreateReloadTask(workspaceID resource.ID) (*task.Task, error) { +func (s *Service) CreateReloadTask(workspaceID resource.Identity) (*task.Task, error) { spec, err := s.Reload(workspaceID) if err != nil { return nil, fmt.Errorf("creating reload task spec: %w", err) diff --git a/internal/state/resource.go b/internal/state/resource.go index e434381..1b1098b 100644 --- a/internal/state/resource.go +++ b/internal/state/resource.go @@ -10,7 +10,7 @@ import ( type Resource struct { resource.ID - WorkspaceID resource.ID + WorkspaceID resource.Identity Address ResourceAddress Attributes map[string]any Tainted bool @@ -20,7 +20,7 @@ func (r *Resource) String() string { return string(r.Address) } -func newResource(workspaceID resource.ID, addr ResourceAddress, attrs json.RawMessage) (*Resource, error) { +func newResource(workspaceID resource.Identity, addr ResourceAddress, attrs json.RawMessage) (*Resource, error) { res := &Resource{ ID: resource.NewID(resource.StateResource), WorkspaceID: workspaceID, diff --git a/internal/state/service.go b/internal/state/service.go index 27b5457..ca60b72 100644 --- a/internal/state/service.go +++ b/internal/state/service.go @@ -44,14 +44,14 @@ func NewService(opts ServiceOptions) *Service { } // Get retrieves the state for a workspace. -func (s *Service) Get(workspaceID resource.ID) (*State, error) { +func (s *Service) Get(workspaceID resource.Identity) (*State, error) { return s.cache.Get(workspaceID) } // GetResource retrieves a state resource. // // TODO: this is massively inefficient -func (s *Service) GetResource(resourceID resource.ID) (*Resource, error) { +func (s *Service) GetResource(resourceID resource.Identity) (*Resource, error) { for _, state := range s.cache.List() { for _, res := range state.Resources { if res.ID == resourceID { @@ -62,7 +62,7 @@ func (s *Service) GetResource(resourceID resource.ID) (*Resource, error) { return nil, resource.ErrNotFound } -func (s *Service) Delete(workspaceID resource.ID, addrs ...ResourceAddress) (task.Spec, error) { +func (s *Service) Delete(workspaceID resource.Identity, addrs ...ResourceAddress) (task.Spec, error) { addrStrings := make([]string, len(addrs)) for i, addr := range addrs { addrStrings[i] = string(addr) @@ -83,7 +83,7 @@ func (s *Service) Delete(workspaceID resource.ID, addrs ...ResourceAddress) (tas }) } -func (s *Service) Taint(workspaceID resource.ID, addr ResourceAddress) (task.Spec, error) { +func (s *Service) Taint(workspaceID resource.Identity, addr ResourceAddress) (task.Spec, error) { return s.createTaskSpec(workspaceID, task.Spec{ Blocking: true, Execution: task.Execution{ @@ -100,7 +100,7 @@ func (s *Service) Taint(workspaceID resource.ID, addr ResourceAddress) (task.Spe }) } -func (s *Service) Untaint(workspaceID resource.ID, addr ResourceAddress) (task.Spec, error) { +func (s *Service) Untaint(workspaceID resource.Identity, addr ResourceAddress) (task.Spec, error) { return s.createTaskSpec(workspaceID, task.Spec{ Blocking: true, Execution: task.Execution{ @@ -117,7 +117,7 @@ func (s *Service) Untaint(workspaceID resource.ID, addr ResourceAddress) (task.S }) } -func (s *Service) Move(workspaceID resource.ID, src, dest ResourceAddress) (task.Spec, error) { +func (s *Service) Move(workspaceID resource.Identity, src, dest ResourceAddress) (task.Spec, error) { return s.createTaskSpec(workspaceID, task.Spec{ Blocking: true, Execution: task.Execution{ @@ -135,7 +135,7 @@ func (s *Service) Move(workspaceID resource.ID, src, dest ResourceAddress) (task } // TODO: move this logic into task.Create -func (s *Service) createTaskSpec(workspaceID resource.ID, opts task.Spec) (task.Spec, error) { +func (s *Service) createTaskSpec(workspaceID resource.Identity, opts task.Spec) (task.Spec, error) { ws, err := s.workspaces.Get(workspaceID) if err != nil { return task.Spec{}, err diff --git a/internal/state/state.go b/internal/state/state.go index 55a2fe5..5b6abb4 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -14,14 +14,14 @@ import ( type State struct { resource.ID - WorkspaceID resource.ID + WorkspaceID resource.Identity Resources map[ResourceAddress]*Resource Serial int64 TerraformVersion string Lineage string } -func newState(workspaceID resource.ID, r io.Reader) (*State, error) { +func newState(workspaceID resource.Identity, r io.Reader) (*State, error) { // Default to a serial of -1 to indicate that there is no state yet. state := &State{ ID: resource.NewID(resource.State), diff --git a/internal/task/enqueuer.go b/internal/task/enqueuer.go index 735c980..3a97ba7 100644 --- a/internal/task/enqueuer.go +++ b/internal/task/enqueuer.go @@ -23,7 +23,7 @@ type enqueuer struct { type enqueuerTaskService interface { taskLister - Get(taskID resource.ID) (*Task, error) + Get(taskID resource.Identity) (*Task, error) } func StartEnqueuer(tasks *Service) { diff --git a/internal/task/enqueuer_test.go b/internal/task/enqueuer_test.go index d3bacd5..b2b580d 100644 --- a/internal/task/enqueuer_test.go +++ b/internal/task/enqueuer_test.go @@ -140,7 +140,7 @@ func (f *fakeEnqueuerTaskService) List(opts ListOptions) []*Task { return nil } -func (f *fakeEnqueuerTaskService) Get(id resource.ID) (*Task, error) { +func (f *fakeEnqueuerTaskService) Get(id resource.Identity) (*Task, error) { for _, task := range append(append(f.pending, f.active...), f.other...) { if id == task.ID { return task, nil diff --git a/internal/task/service.go b/internal/task/service.go index 1304e6f..b54d9c1 100644 --- a/internal/task/service.go +++ b/internal/task/service.go @@ -113,7 +113,7 @@ func (s *Service) AddGroup(group *Group) { } // Enqueue moves the task onto the global queue for processing. -func (s *Service) Enqueue(taskID resource.ID) (*Task, error) { +func (s *Service) Enqueue(taskID resource.Identity) (*Task, error) { task, err := s.tasks.Update(taskID, func(existing *Task) error { existing.updateState(Queued) return nil @@ -191,15 +191,15 @@ func (s *Service) ListGroups() []*Group { return s.groups.List() } -func (s *Service) Get(taskID resource.ID) (*Task, error) { +func (s *Service) Get(taskID resource.Identity) (*Task, error) { return s.tasks.Get(taskID) } -func (s *Service) GetGroup(groupID resource.ID) (*Group, error) { +func (s *Service) GetGroup(groupID resource.Identity) (*Group, error) { return s.groups.Get(groupID) } -func (s *Service) Cancel(taskID resource.ID) (*Task, error) { +func (s *Service) Cancel(taskID resource.Identity) (*Task, error) { task, err := func() (*Task, error) { task, err := s.tasks.Get(taskID) if err != nil { @@ -216,7 +216,7 @@ func (s *Service) Cancel(taskID resource.ID) (*Task, error) { return task, nil } -func (s *Service) Delete(taskID resource.ID) error { +func (s *Service) Delete(taskID resource.Identity) error { // TODO: only allow deleting task if in finished state (error message should // instruct user to cancel task first). s.tasks.Delete(taskID) diff --git a/internal/task/spec.go b/internal/task/spec.go index 8e5bd69..4e9c575 100644 --- a/internal/task/spec.go +++ b/internal/task/spec.go @@ -71,7 +71,7 @@ type Spec struct { } // SpecFunc is a function that creates a spec. -type SpecFunc func(resource.ID) (Spec, error) +type SpecFunc func(resource.Identity) (Spec, error) // Execution specifies the program and arguments to execute type Execution struct { diff --git a/internal/tui/actions.go b/internal/tui/actions.go index 28aedab..a62249e 100644 --- a/internal/tui/actions.go +++ b/internal/tui/actions.go @@ -19,8 +19,8 @@ type ActionHandler struct { } type IDRetriever interface { - GetModuleIDs() ([]resource.ID, error) - GetWorkspaceIDs() ([]resource.ID, error) + GetModuleIDs() ([]resource.Identity, error) + GetWorkspaceIDs() ([]resource.Identity, error) } func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { @@ -42,7 +42,7 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { if err != nil { return ReportError(err) } - fn := func(moduleID resource.ID) (task.Spec, error) { + fn := func(moduleID resource.Identity) (task.Spec, error) { return m.Modules.Init(moduleID, upgrade) } return m.CreateTasks(fn, ids...) @@ -62,7 +62,7 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { parts := strings.Split(v, " ") prog := parts[0] args := parts[1:] - fn := func(moduleID resource.ID) (task.Spec, error) { + fn := func(moduleID resource.Identity) (task.Spec, error) { return m.Modules.Execute(moduleID, prog, args...) } return m.CreateTasks(fn, ids...) @@ -92,7 +92,7 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { if err != nil { return ReportError(err) } - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.Plans.Plan(workspaceID, createPlanOptions) } return m.CreateTasks(fn, ids...) @@ -105,7 +105,7 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { if err != nil { return ReportError(err) } - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.Plans.Apply(workspaceID, createPlanOptions) } return YesNoPrompt( diff --git a/internal/tui/explorer/model.go b/internal/tui/explorer/model.go index 369e06d..3751118 100644 --- a/internal/tui/explorer/model.go +++ b/internal/tui/explorer/model.go @@ -26,7 +26,7 @@ type Maker struct { Helpers *tui.Helpers } -func (mm *Maker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { +func (mm *Maker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { builder := &treeBuilder{ wd: mm.Workdir, helpers: mm.Helpers, @@ -308,7 +308,7 @@ func (m model) filterVisible() bool { // respective *current* workspaces. An error is returned if all modules don't // have a current workspace or if any other type of rows are selected or are // currently the cursor row. -func (m model) GetWorkspaceIDs() ([]resource.ID, error) { +func (m model) GetWorkspaceIDs() ([]resource.Identity, error) { kind, ids := m.tracker.getSelectedOrCurrentIDs() switch kind { case resource.Workspace: @@ -322,7 +322,7 @@ func (m model) GetWorkspaceIDs() ([]resource.ID, error) { if mod.CurrentWorkspaceID == nil { return nil, errors.New("modules must have a current workspace") } - ids[i] = *mod.CurrentWorkspaceID + ids[i] = mod.CurrentWorkspaceID } return ids, nil default: @@ -335,7 +335,7 @@ func (m model) GetWorkspaceIDs() ([]resource.ID, error) { // the IDs of their respective parent modules; if the rows are modules then it // returns their IDs. An error is returned if the rows are not workspaces or // modules. -func (m model) GetModuleIDs() ([]resource.ID, error) { +func (m model) GetModuleIDs() ([]resource.Identity, error) { kind, ids := m.tracker.getSelectedOrCurrentIDs() switch kind { case resource.Module: diff --git a/internal/tui/explorer/selector.go b/internal/tui/explorer/selector.go index 3c5a850..aa0b1df 100644 --- a/internal/tui/explorer/selector.go +++ b/internal/tui/explorer/selector.go @@ -15,7 +15,7 @@ var ( // not directories), and resources must be of the same kind. // selected. type selector struct { - selections map[resource.ID]struct{} + selections map[resource.Identity]struct{} kind *resource.Kind } @@ -40,7 +40,7 @@ func (s *selector) reindex(nodes []node) { if len(s.selections) == 0 { return } - selections := make(map[resource.ID]struct{}, len(s.selections)) + selections := make(map[resource.Identity]struct{}, len(s.selections)) for _, n := range nodes { id, ok := n.ID().(resource.ID) if !ok { @@ -133,7 +133,7 @@ func (s *selector) remove(n node) { } func (s *selector) removeAll() { - s.selections = make(map[resource.ID]struct{}) + s.selections = make(map[resource.Identity]struct{}) s.kind = nil } diff --git a/internal/tui/explorer/selector_test.go b/internal/tui/explorer/selector_test.go index fe3da37..e6c9404 100644 --- a/internal/tui/explorer/selector_test.go +++ b/internal/tui/explorer/selector_test.go @@ -11,7 +11,7 @@ func TestSelector_isSelected(t *testing.T) { mod1 := moduleNode{id: resource.NewID(resource.Module)} mod2 := moduleNode{id: resource.NewID(resource.Module)} - s := selector{selections: make(map[resource.ID]struct{})} + s := selector{selections: make(map[resource.Identity]struct{})} s.add(mod1) assert.True(t, s.isSelected(mod1)) @@ -22,7 +22,7 @@ func TestSelector_reindex(t *testing.T) { mod1 := moduleNode{id: resource.NewID(resource.Module)} mod2 := moduleNode{id: resource.NewID(resource.Module)} - s := selector{selections: make(map[resource.ID]struct{})} + s := selector{selections: make(map[resource.Identity]struct{})} s.add(mod1) s.add(mod2) diff --git a/internal/tui/explorer/tracker.go b/internal/tui/explorer/tracker.go index 51030af..840d9a2 100644 --- a/internal/tui/explorer/tracker.go +++ b/internal/tui/explorer/tracker.go @@ -26,7 +26,7 @@ type tracker struct { func newTracker(tree *tree, height int) *tracker { t := &tracker{ selector: &selector{ - selections: make(map[resource.ID]struct{}), + selections: make(map[resource.Identity]struct{}), }, } t.reindex(tree, height) @@ -123,14 +123,14 @@ func (t *tracker) selectRange() error { return t.selector.addRange(t.cursorNode, t.cursorIndex, t.nodes...) } -func (t *tracker) getSelectedOrCurrentIDs() (resource.Kind, []resource.ID) { +func (t *tracker) getSelectedOrCurrentIDs() (resource.Kind, []resource.Identity) { if len(t.selections) == 0 { id, ok := t.cursorNode.ID().(resource.ID) if !ok { // TODO: consider returning error return -1, nil } - return id.Kind, []resource.ID{id} + return id.Kind, []resource.Identity{id} } return *t.selector.kind, maps.Keys(t.selections) } diff --git a/internal/tui/explorer/tree.go b/internal/tui/explorer/tree.go index e3ece27..55f10e5 100644 --- a/internal/tui/explorer/tree.go +++ b/internal/tui/explorer/tree.go @@ -44,14 +44,14 @@ func (b *treeBuilder) newTree(filter string) (*tree, string) { modules := b.moduleService.List() workspaces := b.workspaceService.List(workspace.ListOptions{}) // Create set of current workspaces for assignment below. - currentWorkspaces := make(map[resource.ID]bool) + currentWorkspaces := make(map[resource.Identity]bool) for _, mod := range modules { if mod.CurrentWorkspaceID != nil { - currentWorkspaces[*mod.CurrentWorkspaceID] = true + currentWorkspaces[mod.CurrentWorkspaceID] = true } } // Arrange workspaces by module, for attachment to modules in tree below. - workspaceNodes := make(map[resource.ID][]workspaceNode, len(modules)) + workspaceNodes := make(map[resource.Identity][]workspaceNode, len(modules)) for _, ws := range workspaces { wsNode := workspaceNode{ id: ws.ID, diff --git a/internal/tui/helpers.go b/internal/tui/helpers.go index 015a035..a864668 100644 --- a/internal/tui/helpers.go +++ b/internal/tui/helpers.go @@ -38,7 +38,7 @@ func (h *Helpers) ModuleCurrentWorkspace(mod *module.Module) *workspace.Workspac if mod.CurrentWorkspaceID == nil { return nil } - ws, err := h.Workspaces.Get(*mod.CurrentWorkspaceID) + ws, err := h.Workspaces.Get(mod.CurrentWorkspaceID) if err != nil { h.Logger.Error("retrieving current workspace for module", "error", err, "module", mod) return nil @@ -46,11 +46,11 @@ func (h *Helpers) ModuleCurrentWorkspace(mod *module.Module) *workspace.Workspac return ws } -func (h *Helpers) CurrentWorkspaceName(workspaceID *resource.ID) string { +func (h *Helpers) CurrentWorkspaceName(workspaceID resource.Identity) string { if workspaceID == nil { return "-" } - ws, err := h.Workspaces.Get(*workspaceID) + ws, err := h.Workspaces.Get(workspaceID) if err != nil { h.Logger.Error("rendering current workspace name", "error", err) return "" @@ -62,7 +62,7 @@ func (h *Helpers) ModuleCurrentResourceCount(mod *module.Module) string { if mod.CurrentWorkspaceID == nil { return "" } - ws, err := h.Workspaces.Get(*mod.CurrentWorkspaceID) + ws, err := h.Workspaces.Get(mod.CurrentWorkspaceID) if err != nil { h.Logger.Error("rendering module current workspace resource count", "error", err) return "" @@ -78,7 +78,7 @@ func (h *Helpers) WorkspaceCurrentCheckmark(ws *workspace.Workspace) string { h.Logger.Error("rendering current workspace checkmark", "error", err) return "" } - if mod.CurrentWorkspaceID != nil && *mod.CurrentWorkspaceID == ws.ID { + if mod.CurrentWorkspaceID != nil && mod.CurrentWorkspaceID == ws.ID { return "✓" } return "" @@ -304,7 +304,7 @@ func (h *Helpers) GroupReport(group *task.Group, table bool) string { // each invocation. If there is more than one id then a task group is created // and the user sent to the task group's page; otherwise if only id is provided, // the user is sent to the task's page. -func (h *Helpers) CreateTasks(fn task.SpecFunc, ids ...resource.ID) tea.Cmd { +func (h *Helpers) CreateTasks(fn task.SpecFunc, ids ...resource.Identity) tea.Cmd { return func() tea.Msg { switch len(ids) { case 0: @@ -363,7 +363,7 @@ func (h *Helpers) createTaskGroup(specs ...task.Spec) tea.Msg { return NewNavigationMsg(TaskGroupKind, WithParent(group.ID)) } -func (h *Helpers) Move(workspaceID resource.ID, from state.ResourceAddress) tea.Cmd { +func (h *Helpers) Move(workspaceID resource.Identity, from state.ResourceAddress) tea.Cmd { return CmdHandler(PromptMsg{ Prompt: "Enter destination address: ", InitialValue: string(from), @@ -371,7 +371,7 @@ func (h *Helpers) Move(workspaceID resource.ID, from state.ResourceAddress) tea. if v == "" { return nil } - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return h.States.Move(workspaceID, from, state.ResourceAddress(v)) } return h.CreateTasks(fn, workspaceID) diff --git a/internal/tui/logs/list.go b/internal/tui/logs/list.go index 11cf2e2..66a885b 100644 --- a/internal/tui/logs/list.go +++ b/internal/tui/logs/list.go @@ -37,7 +37,7 @@ type ListMaker struct { Helpers *tui.Helpers } -func (m *ListMaker) Make(_ resource.ID, width, height int) (tui.ChildModel, error) { +func (m *ListMaker) Make(_ resource.Identity, width, height int) (tui.ChildModel, error) { columns := []table.Column{ timeColumn, levelColumn, diff --git a/internal/tui/logs/model.go b/internal/tui/logs/model.go index b1f13e1..5779110 100644 --- a/internal/tui/logs/model.go +++ b/internal/tui/logs/model.go @@ -33,7 +33,7 @@ type Maker struct { Helpers *tui.Helpers } -func (mm *Maker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { +func (mm *Maker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { msg, err := mm.Logger.Get(id) if err != nil { return nil, err diff --git a/internal/tui/messages.go b/internal/tui/messages.go index ac4b063..bab635a 100644 --- a/internal/tui/messages.go +++ b/internal/tui/messages.go @@ -22,7 +22,7 @@ func NewNavigationMsg(kind Kind, opts ...NavigateOption) NavigationMsg { type NavigateOption func(msg *NavigationMsg) -func WithParent(parent resource.ID) NavigateOption { +func WithParent(parent resource.Identity) NavigateOption { return func(msg *NavigationMsg) { msg.Page.ID = parent } diff --git a/internal/tui/model.go b/internal/tui/model.go index 8481694..8c4fe7d 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -14,16 +14,16 @@ type ChildModel interface { // Maker makes new models type Maker interface { - Make(id resource.ID, width, height int) (ChildModel, error) + Make(id resource.Identity, width, height int) (ChildModel, error) } // Page identifies an instance of a model type Page struct { // The model kind. Identifies the model maker to construct the page. Kind Kind - // The ID of the resource for a model. In the case of global listings of - // modules, workspaces, etc, this is the global resource. - ID resource.ID + // ID of resource for a model. If the model does not have a single resource + // but is say a listing of resources, then this is nil. + ID resource.Identity } // ModelHelpBindings is implemented by models that surface further help bindings diff --git a/internal/tui/pane_manager.go b/internal/tui/pane_manager.go index ec8e720..f0d8bcf 100644 --- a/internal/tui/pane_manager.go +++ b/internal/tui/pane_manager.go @@ -53,7 +53,7 @@ type pane struct { } type tablePane interface { - PreviewCurrentRow() (Kind, resource.ID, bool) + PreviewCurrentRow() (Kind, resource.Identity, bool) } // NewPaneManager constructs the pane manager with at least the explorer, which diff --git a/internal/tui/table/table.go b/internal/tui/table/table.go index 4f70937..9f9c9ff 100644 --- a/internal/tui/table/table.go +++ b/internal/tui/table/table.go @@ -28,23 +28,23 @@ const ( ) // Model defines a state for the table widget. -type Model[V fmt.Stringer] struct { +type Model[V resource.Identifiable] struct { cols []Column - rows []Row[V] + rows []V rowRenderer RowRenderer[V] - rendered map[resource.ID]RenderedRow + rendered map[resource.Identity]RenderedRow border lipgloss.Border borderColor lipgloss.TerminalColor currentRowIndex int - currentRowID resource.ID + currentRowID resource.Identity // items are the unfiltered set of items available to the table. - items map[resource.ID]V + items map[resource.Identity]V sortFunc SortFunc[V] - selected map[resource.ID]V + selected map[resource.Identity]V selectable bool filter textinput.Model @@ -75,11 +75,6 @@ type Column struct { type ColumnKey string -type Row[V any] struct { - ID resource.ID - Value V -} - type RowRenderer[V any] func(V) RenderedRow // RenderedRow provides the rendered string for each column in a row. @@ -88,15 +83,15 @@ type RenderedRow map[ColumnKey]string type SortFunc[V any] func(V, V) int // New creates a new model for the table widget. -func New[V fmt.Stringer](cols []Column, fn RowRenderer[V], width, height int, opts ...Option[V]) Model[V] { +func New[V resource.Identifiable](cols []Column, fn RowRenderer[V], width, height int, opts ...Option[V]) Model[V] { filter := textinput.New() filter.Prompt = "Filter: " m := Model[V]{ rowRenderer: fn, - items: make(map[resource.ID]V), - rendered: make(map[resource.ID]RenderedRow), - selected: make(map[resource.ID]V), + items: make(map[resource.Identity]V), + rendered: make(map[resource.Identity]RenderedRow), + selected: make(map[resource.Identity]V), selectable: true, filter: filter, border: lipgloss.NormalBorder(), @@ -121,17 +116,17 @@ func New[V fmt.Stringer](cols []Column, fn RowRenderer[V], width, height int, op return m } -type Option[V fmt.Stringer] func(m *Model[V]) +type Option[V resource.Identifiable] func(m *Model[V]) // WithSortFunc configures the table to sort rows using the given func. -func WithSortFunc[V fmt.Stringer](sortFunc func(V, V) int) Option[V] { +func WithSortFunc[V resource.Identifiable](sortFunc func(V, V) int) Option[V] { return func(m *Model[V]) { m.sortFunc = sortFunc } } // WithSelectable sets whether rows are selectable. -func WithSelectable[V fmt.Stringer](s bool) Option[V] { +func WithSelectable[V resource.Identifiable](s bool) Option[V] { return func(m *Model[V]) { m.selectable = s } @@ -139,7 +134,7 @@ func WithSelectable[V fmt.Stringer](s bool) Option[V] { // WithPreview configures the table to automatically populate the bottom right // pane with a model corresponding to the current row. -func WithPreview[V fmt.Stringer](maker tui.Kind) Option[V] { +func WithPreview[V resource.Identifiable](maker tui.Kind) Option[V] { return func(m *Model[V]) { m.previewKind = &maker } @@ -252,7 +247,8 @@ func (m Model[V]) Update(msg tea.Msg) (Model[V], tea.Cmd) { return m, nil } -func (m *Model[V]) PreviewCurrentRow() (tui.Kind, resource.ID, bool) { +// PreviewCurrentRow +func (m *Model[V]) PreviewCurrentRow() (tui.Kind, resource.Identity, bool) { if _, ok := m.CurrentRow(); !ok { return 0, resource.ID{}, false } @@ -321,37 +317,27 @@ func (m *Model[V]) SetBorderStyle(border lipgloss.Border, color lipgloss.Termina // CurrentRow returns the current row the user has highlighted. If the table is // empty then false is returned. -func (m Model[V]) CurrentRow() (Row[V], bool) { +func (m Model[V]) CurrentRow() (V, bool) { if m.currentRowIndex < 0 || m.currentRowIndex >= len(m.rows) { - return *new(Row[V]), false + return *new(V), false } return m.rows[m.currentRowIndex], true } // SelectedOrCurrent returns either the selected rows, or if there are no // selections, the current row -func (m Model[V]) SelectedOrCurrent() []Row[V] { +func (m Model[V]) SelectedOrCurrent() []V { if len(m.selected) > 0 { - rows := make([]Row[V], len(m.selected)) + rows := make([]V, len(m.selected)) var i int - for k, v := range m.selected { - rows[i] = Row[V]{ID: k, Value: v} + for _, v := range m.selected { + rows[i] = v i++ } return rows } if row, ok := m.CurrentRow(); ok { - return []Row[V]{row} - } - return nil -} - -func (m Model[V]) SelectedOrCurrentIDs() []resource.ID { - if len(m.selected) > 0 { - return maps.Keys(m.selected) - } - if row, ok := m.CurrentRow(); ok { - return []resource.ID{row.ID} + return []V{row} } return nil } @@ -365,16 +351,16 @@ func (m *Model[V]) ToggleSelection() { if !ok { return } - if _, isSelected := m.selected[current.ID]; isSelected { - delete(m.selected, current.ID) + if _, isSelected := m.selected[current]; isSelected { + delete(m.selected, current) } else { - m.selected[current.ID] = current.Value + m.selected[current.GetID()] = current } } -// ToggleSelectionByID toggles the selection of the row with the given id. If -// the id does not exist no action is taken. -func (m *Model[V]) ToggleSelectionByID(id resource.ID) { +// ToggleSelectionByID toggles the selection of the row with the given ID. If +// the ID does not exist no action is taken. +func (m *Model[V]) ToggleSelectionByID(id resource.Identity) { if !m.selectable { return } @@ -394,9 +380,8 @@ func (m *Model[V]) SelectAll() { if !m.selectable { return } - for _, row := range m.rows { - m.selected[row.ID] = row.Value + m.selected[row] = row } } @@ -405,8 +390,7 @@ func (m *Model[V]) DeselectAll() { if !m.selectable { return } - - m.selected = make(map[resource.ID]V) + m.selected = make(map[resource.Identity]V) } // SelectRange selects a range of rows. If the current row is *below* a selected @@ -430,7 +414,7 @@ func (m *Model[V]) SelectRange() { n = m.currentRowIndex - first + 1 break } - if _, ok := m.selected[row.ID]; !ok { + if _, ok := m.selected[row.GetID()]; !ok { // Ignore unselected rows continue } @@ -445,14 +429,14 @@ func (m *Model[V]) SelectRange() { first = i + 1 } for _, row := range m.rows[first : first+n] { - m.selected[row.ID] = row.Value + m.selected[row.GetID()] = row } } // SetItems overwrites all existing items in the table with items. func (m *Model[V]) SetItems(items ...V) { - m.items = make(map[resource.ID]V) - m.rendered = make(map[resource.ID]RenderedRow) + m.items = make(map[resource.Identity]V) + m.rendered = make(map[resource.Identity]RenderedRow) m.AddItems(items...) } @@ -469,11 +453,11 @@ func (m *Model[V]) AddItems(items ...V) { } func (m *Model[V]) removeItem(item V) { - delete(m.rendered, item.GetID()) - delete(m.items, item.GetID()) - delete(m.selected, item.GetID()) + delete(m.rendered, item) + delete(m.items, item) + delete(m.selected, item) for i, row := range m.rows { - if row.ID == item.GetID() { + if row.GetID() == item.GetID() { // TODO: this might well produce a memory leak. See note: // https://go.dev/wiki/SliceTricks#delete-without-preserving-order m.rows = append(m.rows[:i], m.rows[i+1:]...) @@ -492,31 +476,31 @@ func (m *Model[V]) removeItem(item V) { } func (m *Model[V]) setRows(items ...V) { - selected := make(map[resource.ID]V) - m.rows = make([]Row[V], 0, len(items)) + selected := make(map[resource.Identity]V) + m.rows = make([]V, 0, len(items)) for _, item := range items { - if m.filterVisible() && !m.matchFilter(item.GetID()) { + if m.filterVisible() && !m.matchFilter(item) { // Skip item that doesn't match filter continue } - m.rows = append(m.rows, Row[V]{ID: item.GetID(), Value: item}) + m.rows = append(m.rows, item) if m.selectable { - if _, ok := m.selected[item.GetID()]; ok { - selected[item.GetID()] = item + if _, ok := m.selected[item]; ok { + selected[item] = item } } } m.selected = selected // Sort rows in-place if m.sortFunc != nil { - slices.SortFunc(m.rows, func(i, j Row[V]) int { - return m.sortFunc(i.Value, j.Value) + slices.SortFunc(m.rows, func(i, j V) int { + return m.sortFunc(i, j) }) } // Track current row index m.currentRowIndex = -1 for i, row := range m.rows { - if row.ID == m.currentRowID { + if row.GetID() == m.currentRowID { m.currentRowIndex = i break } @@ -526,15 +510,15 @@ func (m *Model[V]) setRows(items ...V) { // first row. if len(m.rows) > 0 && m.currentRowIndex == -1 { m.currentRowIndex = 0 - m.currentRowID = m.rows[m.currentRowIndex].ID + m.currentRowID = m.rows[m.currentRowIndex].GetID() } m.setStart() } // matchFilter returns true if the item with the given ID matches the filter // value. -func (m *Model[V]) matchFilter(id resource.ID) bool { - for _, col := range m.rendered[id] { +func (m *Model[V]) matchFilter(item V) bool { + for _, col := range m.rendered[item.GetID()] { // Remove ANSI escapes code before filtering stripped := internal.StripAnsi(col) if strings.Contains(stripped, m.filter.Value()) { @@ -559,7 +543,7 @@ func (m *Model[V]) MoveDown(n int) { func (m *Model[V]) moveCurrentRow(n int) { if len(m.rows) > 0 { m.currentRowIndex = clamp(m.currentRowIndex+n, 0, len(m.rows)-1) - m.currentRowID = m.rows[m.currentRowIndex].ID + m.currentRowID = m.rows[m.currentRowIndex].GetID() m.setStart() } } @@ -611,7 +595,7 @@ func (m *Model[V]) renderRow(rowIdx int) string { current bool selected bool ) - if _, ok := m.selected[row.ID]; ok { + if _, ok := m.selected[row.GetID()]; ok { selected = true } current = rowIdx == m.currentRowIndex @@ -626,7 +610,7 @@ func (m *Model[V]) renderRow(rowIdx int) string { foreground = tui.SelectedForeground } - cells := m.rendered[row.ID] + cells := m.rendered[row.GetID()] styledCells := make([]string, len(m.cols)) for i, col := range m.cols { content := cells[col.Key] diff --git a/internal/tui/table/table_test.go b/internal/tui/table/table_test.go index f58dc8f..98af065 100644 --- a/internal/tui/table/table_test.go +++ b/internal/tui/table/table_test.go @@ -55,7 +55,7 @@ func TestTable_CurrentRow(t *testing.T) { got, ok := tbl.CurrentRow() require.True(t, ok) - assert.Equal(t, resource0, got.Value) + assert.Equal(t, resource0, got) } func TestTable_ToggleSelection(t *testing.T) { @@ -70,55 +70,55 @@ func TestTable_ToggleSelection(t *testing.T) { func TestTable_SelectRange(t *testing.T) { tests := []struct { name string - selected []resource.ID + selected []resource.Identity cursor int - want []resource.ID + want []resource.Identity }{ { name: "select no range when nothing is selected, and cursor is on first row", - selected: []resource.ID{}, - want: []resource.ID{}, + selected: []resource.Identity{}, + want: []resource.Identity{}, }, { name: "select no range when nothing is selected, and cursor is on last row", - selected: []resource.ID{}, - want: []resource.ID{}, + selected: []resource.Identity{}, + want: []resource.Identity{}, }, { name: "select no range when cursor is on the only selected row", - selected: []resource.ID{resource0.ID}, - want: []resource.ID{resource0.ID}, + selected: []resource.Identity{resource0.ID}, + want: []resource.Identity{resource0.ID}, }, { name: "select all rows between selected top row and cursor on last row", - selected: []resource.ID{resource0.ID}, // first row - cursor: 5, // last row - want: []resource.ID{resource0.ID, resource1.ID, resource2.ID, resource3.ID, resource4.ID, resource5.ID}, + selected: []resource.Identity{resource0.ID}, // first row + cursor: 5, // last row + want: []resource.Identity{resource0.ID, resource1.ID, resource2.ID, resource3.ID, resource4.ID, resource5.ID}, }, { name: "select rows between selected top row and cursor in third row", - selected: []resource.ID{resource0.ID}, // first row - cursor: 2, // third row - want: []resource.ID{resource0.ID, resource1.ID, resource2.ID}, + selected: []resource.Identity{resource0.ID}, // first row + cursor: 2, // third row + want: []resource.Identity{resource0.ID, resource1.ID, resource2.ID}, }, { name: "select rows between selected top row and cursor in third row, ignoring selected last row", - selected: []resource.ID{resource0.ID, resource5.ID}, // first and last row - cursor: 2, // third row - want: []resource.ID{resource0.ID, resource1.ID, resource2.ID, resource5.ID}, + selected: []resource.Identity{resource0.ID, resource5.ID}, // first and last row + cursor: 2, // third row + want: []resource.Identity{resource0.ID, resource1.ID, resource2.ID, resource5.ID}, }, { name: "select rows between cursor in third row and selected last row", - selected: []resource.ID{resource5.ID}, // last row - cursor: 2, // third row - want: []resource.ID{resource2.ID, resource3.ID, resource4.ID, resource5.ID}, + selected: []resource.Identity{resource5.ID}, // last row + cursor: 2, // third row + want: []resource.Identity{resource2.ID, resource3.ID, resource4.ID, resource5.ID}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tbl := setupTest() - for _, key := range tt.selected { - tbl.ToggleSelectionByID(key) + for _, id := range tt.selected { + tbl.ToggleSelectionByID(id) } tbl.currentRowIndex = tt.cursor @@ -132,8 +132,8 @@ func TestTable_SelectRange(t *testing.T) { } } -func sortStrings(i, j resource.ID) int { - if i.String() < j.String() { +func sortStrings(i, j resource.Identity) int { + if i.(resource.ID).String() < j.(resource.ID).String() { return -1 } return 1 diff --git a/internal/tui/task/cancel.go b/internal/tui/task/cancel.go index c1267d0..5ea0e1f 100644 --- a/internal/tui/task/cancel.go +++ b/internal/tui/task/cancel.go @@ -11,7 +11,7 @@ import ( ) // cancel task(s) -func cancel(tasks *task.Service, taskIDs ...resource.ID) tea.Cmd { +func cancel(tasks *task.Service, taskIDs ...resource.Identity) tea.Cmd { var ( prompt string cmd tea.Cmd diff --git a/internal/tui/task/group.go b/internal/tui/task/group.go index ebb29fa..08bd8be 100644 --- a/internal/tui/task/group.go +++ b/internal/tui/task/group.go @@ -16,7 +16,7 @@ type groupTaskMaker struct { *Maker } -func (m *groupTaskMaker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { +func (m *groupTaskMaker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { return m.make(id, width, height, false) } @@ -37,7 +37,7 @@ func NewGroupMaker(tasks *task.Service, plans *plan.Service, taskMaker *Maker, h } } -func (mm *GroupMaker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { +func (mm *GroupMaker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { group, err := mm.taskListMaker.Tasks.GetGroup(id) if err != nil { return nil, err diff --git a/internal/tui/task/group_list.go b/internal/tui/task/group_list.go index cdc9ecb..5316e07 100644 --- a/internal/tui/task/group_list.go +++ b/internal/tui/task/group_list.go @@ -29,7 +29,7 @@ type GroupListMaker struct { Helpers *tui.Helpers } -func (m *GroupListMaker) Make(_ resource.ID, width, height int) (tui.ChildModel, error) { +func (m *GroupListMaker) Make(_ resource.Identity, width, height int) (tui.ChildModel, error) { columns := []table.Column{ taskGroupID, commandColumn, diff --git a/internal/tui/task/list.go b/internal/tui/task/list.go index b5b9c79..1fe5c70 100644 --- a/internal/tui/task/list.go +++ b/internal/tui/task/list.go @@ -38,7 +38,7 @@ type ListTaskMaker struct { *Maker } -func (m *ListTaskMaker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { +func (m *ListTaskMaker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { return m.make(id, width, height, false) } @@ -61,7 +61,7 @@ type ListMaker struct { Helpers *tui.Helpers } -func (mm *ListMaker) Make(_ resource.ID, width, height int) (tui.ChildModel, error) { +func (mm *ListMaker) Make(_ resource.Identity, width, height int) (tui.ChildModel, error) { columns := []table.Column{ table.ModuleColumn, table.WorkspaceColumn, @@ -123,7 +123,11 @@ func (m *List) Update(msg tea.Msg) tea.Cmd { case tea.KeyMsg: switch { case key.Matches(msg, keys.Common.Cancel): - taskIDs := m.SelectedOrCurrentIDs() + rows := m.SelectedOrCurrent() + taskIDs := make([]resource.Identity, len(rows)) + for i, row := range rows { + taskIDs[i] = row.ID + } return cancel(m.tasks, taskIDs...) case key.Matches(msg, keys.Common.AutoApply): ids, err := m.allPlans() @@ -138,7 +142,7 @@ func (m *List) Update(msg tea.Msg) tea.Cmd { rows := m.SelectedOrCurrent() specs := make([]task.Spec, len(rows)) for i, row := range rows { - specs[i] = row.Value.Spec + specs[i] = row.Spec } return tui.YesNoPrompt( fmt.Sprintf("Retry %d tasks?", len(rows)), @@ -175,11 +179,11 @@ func (m List) HelpBindings() []key.Binding { return bindings } -func (m List) allPlans() ([]resource.ID, error) { +func (m List) allPlans() ([]resource.Identity, error) { rows := m.SelectedOrCurrent() - ids := make([]resource.ID, len(rows)) + ids := make([]resource.Identity, len(rows)) for i, row := range rows { - if err := plan.IsApplyable(row.Value); err != nil { + if err := plan.IsApplyable(row); err != nil { return nil, fmt.Errorf("at least one task is not applyable: %w", err) } ids[i] = row.ID @@ -187,38 +191,38 @@ func (m List) allPlans() ([]resource.ID, error) { return ids, nil } -func (m List) GetModuleIDs() ([]resource.ID, error) { +func (m List) GetModuleIDs() ([]resource.Identity, error) { rows := m.SelectedOrCurrent() - ids := make([]resource.ID, len(rows)) + ids := make([]resource.Identity, len(rows)) for i, row := range rows { - if row.Value.ModuleID == nil { + if row.ModuleID == nil { return nil, errors.New("valid only on modules") } - ids[i] = *row.Value.ModuleID + ids[i] = *row.ModuleID } return ids, nil } -func (m List) GetWorkspaceIDs() ([]resource.ID, error) { +func (m List) GetWorkspaceIDs() ([]resource.Identity, error) { rows := m.SelectedOrCurrent() - ids := make([]resource.ID, len(rows)) + ids := make([]resource.Identity, len(rows)) for i, row := range rows { - if row.Value.WorkspaceID != nil { - ids[i] = *row.Value.WorkspaceID - } else if row.Value.ModuleID == nil { + if row.WorkspaceID != nil { + ids[i] = *row.WorkspaceID + } else if row.ModuleID == nil { return nil, errors.New("valid only on tasks associated with a module or a workspace") } else { // task has a module ID but no workspace ID, so find out if its // module has a current workspace, and if so, use that. Otherwise // return error - mod, err := m.Modules.Get(*row.Value.ModuleID) + mod, err := m.Modules.Get(*row.ModuleID) if err != nil { return nil, err } if mod.CurrentWorkspaceID == nil { return nil, errors.New("valid only on tasks associated with a module with a current workspace, or a workspace") } - ids[i] = *mod.CurrentWorkspaceID + ids[i] = mod.CurrentWorkspaceID } } return ids, nil diff --git a/internal/tui/task/model.go b/internal/tui/task/model.go index 0dded54..080f8a6 100644 --- a/internal/tui/task/model.go +++ b/internal/tui/task/model.go @@ -32,11 +32,11 @@ type Maker struct { showInfo bool } -func (mm *Maker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { +func (mm *Maker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { return mm.make(id, width, height, true) } -func (mm *Maker) make(id resource.ID, width, height int, border bool) (tui.ChildModel, error) { +func (mm *Maker) make(id resource.Identity, width, height int, border bool) (tui.ChildModel, error) { task, err := mm.Tasks.Get(id) if err != nil { return nil, err @@ -330,16 +330,16 @@ type outputMsg struct { eof bool } -func (m Model) GetModuleIDs() ([]resource.ID, error) { +func (m Model) GetModuleIDs() ([]resource.Identity, error) { if m.task.ModuleID == nil { return nil, errors.New("valid only on modules") } - return []resource.ID{*m.task.ModuleID}, nil + return []resource.Identity{*m.task.ModuleID}, nil } -func (m Model) GetWorkspaceIDs() ([]resource.ID, error) { +func (m Model) GetWorkspaceIDs() ([]resource.Identity, error) { if m.task.WorkspaceID != nil { - return []resource.ID{*m.task.WorkspaceID}, nil + return []resource.Identity{*m.task.WorkspaceID}, nil } else if m.task.ModuleID == nil { return nil, errors.New("valid only on tasks associated with a module or a workspace") } else { @@ -353,6 +353,6 @@ func (m Model) GetWorkspaceIDs() ([]resource.ID, error) { if mod.CurrentWorkspaceID == nil { return nil, errors.New("valid only on tasks associated with a module with a current workspace, or a workspace") } - return []resource.ID{*mod.CurrentWorkspaceID}, nil + return []resource.Identity{mod.CurrentWorkspaceID}, nil } } diff --git a/internal/tui/workspace/resource.go b/internal/tui/workspace/resource.go index ea4fd5a..9e52f70 100644 --- a/internal/tui/workspace/resource.go +++ b/internal/tui/workspace/resource.go @@ -23,7 +23,7 @@ type ResourceMaker struct { disableBorders bool } -func (mm *ResourceMaker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { +func (mm *ResourceMaker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { stateResource, err := mm.States.GetResource(id) if err != nil { return nil, err @@ -77,19 +77,19 @@ func (m *resourceModel) Update(msg tea.Msg) tea.Cmd { case tea.KeyMsg: switch { case key.Matches(msg, resourcesKeys.Taint): - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.states.Taint(workspaceID, m.resource.Address) } return m.CreateTasks(fn, m.resource.WorkspaceID) case key.Matches(msg, resourcesKeys.Untaint): - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.states.Untaint(workspaceID, m.resource.Address) } return m.CreateTasks(fn, m.resource.WorkspaceID) case key.Matches(msg, resourcesKeys.Move): return m.Move(m.resource.WorkspaceID, m.resource.Address) case key.Matches(msg, keys.Common.Delete): - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.states.Delete(workspaceID, m.resource.Address) } return tui.YesNoPrompt( @@ -103,7 +103,7 @@ func (m *resourceModel) Update(msg tea.Msg) tea.Cmd { case key.Matches(msg, keys.Common.Plan): // Create a targeted plan. createRunOptions.TargetAddrs = []state.ResourceAddress{m.resource.Address} - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.plans.Plan(workspaceID, createRunOptions) } return m.CreateTasks(fn, m.resource.WorkspaceID) diff --git a/internal/tui/workspace/resource_list.go b/internal/tui/workspace/resource_list.go index b874622..b8b6a2b 100644 --- a/internal/tui/workspace/resource_list.go +++ b/internal/tui/workspace/resource_list.go @@ -33,7 +33,7 @@ type ResourceListMaker struct { Helpers *tui.Helpers } -func (mm *ResourceListMaker) Make(workspaceID resource.ID, width, height int) (tui.ChildModel, error) { +func (mm *ResourceListMaker) Make(workspaceID resource.Identity, width, height int) (tui.ChildModel, error) { ws, err := mm.Workspaces.Get(workspaceID) if err != nil { return nil, err @@ -151,7 +151,7 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { // no rows; do nothing return nil } - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.states.Delete(workspaceID, addrs...) } return tui.YesNoPrompt( @@ -166,7 +166,7 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { return m.createStateCommand(m.states.Untaint, addrs...) case key.Matches(msg, resourcesKeys.Move): if row, ok := m.CurrentRow(); ok { - from := row.Value.Address + from := row.Address return m.Move(m.workspace.ID, from) } case key.Matches(msg, keys.Common.PlanDestroy): @@ -178,7 +178,7 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { createRunOptions.TargetAddrs = m.selectedOrCurrentAddresses() // NOTE: even if the user hasn't selected any rows, we still proceed // to create a run without targeted resources. - fn := func(workspaceID resource.ID) (task.Spec, error) { + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.plans.Plan(workspaceID, createRunOptions) } return m.CreateTasks(fn, m.workspace.ID) @@ -189,8 +189,8 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { case key.Matches(msg, keys.Common.AutoApply): // Create a targeted apply. createRunOptions.TargetAddrs = m.selectedOrCurrentAddresses() - resourceIDs := m.SelectedOrCurrentIDs() - fn := func(workspaceID resource.ID) (task.Spec, error) { + resourceIDs := m.SelectedOrCurrent() + fn := func(workspaceID resource.Identity) (task.Spec, error) { return m.plans.Apply(workspaceID, createRunOptions) } return tui.YesNoPrompt( @@ -258,7 +258,7 @@ func (m resourceList) selectedOrCurrentAddresses() []state.ResourceAddress { addrs := make([]state.ResourceAddress, len(rows)) var i int for _, v := range rows { - addrs[i] = v.Value.Address + addrs[i] = v.Address i++ } return addrs @@ -283,10 +283,10 @@ func (m *resourceList) BorderText() map[tui.BorderPosition]string { } } -func (m *resourceList) GetModuleIDs() ([]resource.ID, error) { - return []resource.ID{m.workspace.ModuleID}, nil +func (m *resourceList) GetModuleIDs() ([]resource.Identity, error) { + return []resource.Identity{m.workspace.ModuleID}, nil } -func (m *resourceList) GetWorkspaceIDs() ([]resource.ID, error) { - return []resource.ID{m.workspace.ID}, nil +func (m *resourceList) GetWorkspaceIDs() ([]resource.Identity, error) { + return []resource.Identity{m.workspace.ID}, nil } diff --git a/internal/tui/workspace/state_func.go b/internal/tui/workspace/state_func.go index e7cf005..7d2ac84 100644 --- a/internal/tui/workspace/state_func.go +++ b/internal/tui/workspace/state_func.go @@ -7,11 +7,11 @@ import ( "github.com/leg100/pug/internal/task" ) -type stateFunc func(workspaceID resource.ID, addr state.ResourceAddress) (task.Spec, error) +type stateFunc func(workspaceID resource.Identity, addr state.ResourceAddress) (task.Spec, error) func (m resourceList) createStateCommand(fn stateFunc, addrs ...state.ResourceAddress) tea.Cmd { // Make N copies of the workspace ID where N is the number of addresses - workspaceIDs := make([]resource.ID, len(addrs)) + workspaceIDs := make([]resource.Identity, len(addrs)) for i := range workspaceIDs { workspaceIDs[i] = m.workspace.ID } @@ -32,7 +32,7 @@ type stateTaskFunc struct { i int } -func (f *stateTaskFunc) createTask(workspaceID resource.ID) (task.Spec, error) { +func (f *stateTaskFunc) createTask(workspaceID resource.Identity) (task.Spec, error) { t, err := f.fn(workspaceID, f.addrs[f.i]) f.i++ return t, err diff --git a/internal/workspace/cost.go b/internal/workspace/cost.go index ab8e0d1..96bd8fd 100644 --- a/internal/workspace/cost.go +++ b/internal/workspace/cost.go @@ -21,7 +21,7 @@ type costTaskSpecCreator struct { // Cost creates a task that retrieves a breakdown of the costs of the // infrastructure deployed by the workspace. -func (s *costTaskSpecCreator) Cost(workspaceIDs ...resource.ID) (task.Spec, error) { +func (s *costTaskSpecCreator) Cost(workspaceIDs ...resource.Identity) (task.Spec, error) { if len(workspaceIDs) == 0 { return task.Spec{}, errors.New("no workspaces specified") } diff --git a/internal/workspace/reloader.go b/internal/workspace/reloader.go index 2cfc342..1ec9877 100644 --- a/internal/workspace/reloader.go +++ b/internal/workspace/reloader.go @@ -33,7 +33,7 @@ func (s ReloadSummary) LogValue() slog.Value { ) } -func (r *reloader) createReloadTask(moduleID resource.ID) error { +func (r *reloader) createReloadTask(moduleID resource.Identity) error { spec, err := r.Reload(moduleID) if err != nil { return err @@ -47,7 +47,7 @@ func (r *reloader) createReloadTask(moduleID resource.ID) error { // workspaces and pruning any workspaces no longer found to exist. // // TODO: separate into Load and Reload -func (r *reloader) Reload(moduleID resource.ID) (task.Spec, error) { +func (r *reloader) Reload(moduleID resource.Identity) (task.Spec, error) { mod, err := r.modules.Get(moduleID) if err != nil { return task.Spec{}, err @@ -109,8 +109,7 @@ func (r *reloader) resetWorkspaces(mod *module.Module, discovered []string, curr if err != nil { return nil, nil, fmt.Errorf("cannot find current workspace: %s: %w", current, err) } - err = r.modules.SetCurrent(mod.ID, currentWorkspace.ID) - if err != nil { + if err := r.modules.SetCurrent(mod.ID, currentWorkspace.ID); err != nil { return nil, nil, err } return diff --git a/internal/workspace/reloader_test.go b/internal/workspace/reloader_test.go index 2a132ce..9b4d56e 100644 --- a/internal/workspace/reloader_test.go +++ b/internal/workspace/reloader_test.go @@ -30,18 +30,20 @@ func TestWorkspace_parseList(t *testing.T) { func TestWorkspace_resetWorkspaces(t *testing.T) { mod := &module.Module{Path: "a/b/c"} + dev, err := New(mod, "dev") require.NoError(t, err) + staging, err := New(mod, "staging") require.NoError(t, err) - var gotCurrent resource.ID table := &fakeWorkspaceTable{ existing: []*Workspace{dev, staging}, } + moduleService := &fakeModuleService{} reloader := &reloader{ &Service{ - modules: &fakeModuleService{current: &gotCurrent}, + modules: moduleService, table: table, }, } @@ -49,40 +51,40 @@ func TestWorkspace_resetWorkspaces(t *testing.T) { require.NoError(t, err) // expect staging to be dropped - assert.Equal(t, []resource.ID{staging.ID}, table.deleted) + assert.Equal(t, []resource.Identity{staging.ID}, table.deleted) // expect prod to be added assert.Len(t, table.added, 1) assert.Equal(t, "prod", table.added[0].Name) // expect dev to have been made the current workspace - assert.Equal(t, dev.ID, gotCurrent) + assert.Equal(t, dev.ID, moduleService.current) } type fakeModuleService struct { - current *resource.ID + current resource.Identity modules } -func (f *fakeModuleService) SetCurrent(moduleID, workspaceID resource.ID) error { - *f.current = workspaceID +func (f *fakeModuleService) SetCurrent(moduleID, workspaceID resource.Identity) error { + f.current = workspaceID return nil } type fakeWorkspaceTable struct { existing []*Workspace added []*Workspace - deleted []resource.ID + deleted []resource.Identity workspaceTable } -func (f *fakeWorkspaceTable) Add(id resource.ID, row *Workspace) { +func (f *fakeWorkspaceTable) Add(id resource.Identity, row *Workspace) { f.added = append(f.added, row) } -func (f *fakeWorkspaceTable) Delete(id resource.ID) { +func (f *fakeWorkspaceTable) Delete(id resource.Identity) { f.deleted = append(f.deleted, id) } diff --git a/internal/workspace/service.go b/internal/workspace/service.go index 3c882e7..0a39e9f 100644 --- a/internal/workspace/service.go +++ b/internal/workspace/service.go @@ -34,17 +34,17 @@ type ServiceOptions struct { } type workspaceTable interface { - Add(id resource.ID, row *Workspace) - Update(id resource.ID, updater func(existing *Workspace) error) (*Workspace, error) - Delete(id resource.ID) - Get(id resource.ID) (*Workspace, error) + Add(id resource.Identity, row *Workspace) + Update(id resource.Identity, updater func(existing *Workspace) error) (*Workspace, error) + Delete(id resource.Identity) + Get(id resource.Identity) (*Workspace, error) List() []*Workspace } type modules interface { - Get(id resource.ID) (*module.Module, error) + Get(id resource.Identity) (*module.Module, error) GetByPath(path string) (*module.Module, error) - SetCurrent(moduleID, workspaceID resource.ID) error + SetCurrent(moduleID, workspaceID resource.Identity) error Reload() ([]string, []string, error) List() []*module.Module } @@ -142,7 +142,7 @@ func (s *Service) Create(path, name string) (task.Spec, error) { }, nil } -func (s *Service) Get(workspaceID resource.ID) (*Workspace, error) { +func (s *Service) Get(workspaceID resource.Identity) (*Workspace, error) { return s.table.Get(workspaceID) } @@ -157,13 +157,13 @@ func (s *Service) GetByName(modulePath, name string) (*Workspace, error) { type ListOptions struct { // Filter by ID of workspace's module. - ModuleID *resource.ID + ModuleID resource.Identity } func (s *Service) List(opts ListOptions) []*Workspace { var existing []*Workspace for _, ws := range s.table.List() { - if opts.ModuleID != nil && *opts.ModuleID != ws.ModuleID { + if opts.ModuleID != nil && opts.ModuleID != ws.ModuleID { continue } existing = append(existing, ws) @@ -174,7 +174,7 @@ func (s *Service) List(opts ListOptions) []*Workspace { // SelectWorkspace runs the `terraform workspace select ` // command, which sets the current workspace for the module. Once that's // finished it then updates the current workspace in pug itself too. -func (s *Service) SelectWorkspace(workspaceID resource.ID) error { +func (s *Service) SelectWorkspace(workspaceID resource.Identity) error { ws, err := s.table.Get(workspaceID) if err != nil { return err @@ -207,7 +207,7 @@ func (s *Service) SelectWorkspace(workspaceID resource.ID) error { } // Delete a workspace. Asynchronous. -func (s *Service) Delete(workspaceID resource.ID) (task.Spec, error) { +func (s *Service) Delete(workspaceID resource.Identity) (task.Spec, error) { ws, err := s.table.Get(workspaceID) if err != nil { return task.Spec{}, fmt.Errorf("deleting workspace: %w", err) From 473c0a065a2d2e6276a7f69e6f894c4d475d6bfb Mon Sep 17 00:00:00 2001 From: Louis Garman Date: Wed, 15 Jan 2025 09:19:53 +0000 Subject: [PATCH 3/5] wip --- internal/logging/enricher.go | 6 +- internal/logging/enricher_test.go | 18 +++--- internal/logging/logger.go | 2 +- internal/logging/message.go | 16 ++--- internal/logging/sort.go | 2 +- internal/logging/writer.go | 4 +- internal/module/module.go | 6 +- internal/module/service.go | 28 ++++----- internal/module/service_test.go | 2 +- internal/plan/plan.go | 21 ++++--- internal/plan/plan_test.go | 4 +- internal/plan/service.go | 22 +++---- internal/resource/id.go | 46 +------------- internal/resource/id_monotonic.go | 41 +++++++++++++ internal/resource/id_test.go | 8 +-- internal/resource/resource.go | 2 +- internal/resource/table.go | 12 ++-- internal/state/reloader.go | 4 +- internal/state/resource.go | 8 +-- internal/state/service.go | 20 +++---- internal/state/state.go | 8 +-- internal/state/state_test.go | 1 - internal/task/dependency_graph_builder.go | 4 +- .../task/dependency_graph_builder_test.go | 34 ++++++----- internal/task/enqueuer.go | 14 ++--- internal/task/enqueuer_test.go | 28 ++++----- internal/task/group.go | 8 +-- internal/task/service.go | 12 ++-- internal/task/service_test.go | 10 ++-- internal/task/spec.go | 8 +-- internal/task/task.go | 16 +++-- internal/tui/actions.go | 12 ++-- internal/tui/explorer/model.go | 6 +- internal/tui/explorer/nodes.go | 4 +- internal/tui/explorer/selector.go | 14 ++--- internal/tui/explorer/selector_test.go | 12 ++-- internal/tui/explorer/tracker.go | 8 +-- internal/tui/explorer/tree.go | 4 +- internal/tui/explorer/tree_test.go | 12 ++-- internal/tui/helpers.go | 14 ++--- internal/tui/logs/list.go | 2 +- internal/tui/logs/model.go | 8 +-- internal/tui/messages.go | 2 +- internal/tui/model.go | 4 +- internal/tui/pane_manager.go | 2 +- internal/tui/table/table.go | 30 +++++----- internal/tui/table/table_test.go | 60 +++++++++---------- internal/tui/task/cancel.go | 2 +- internal/tui/task/group.go | 4 +- internal/tui/task/group_list.go | 6 +- internal/tui/task/list.go | 24 ++++---- internal/tui/task/model.go | 16 ++--- internal/tui/top/model.go | 2 +- internal/tui/workspace/resource.go | 10 ++-- internal/tui/workspace/resource_list.go | 20 +++---- internal/tui/workspace/state_func.go | 6 +- internal/workspace/cost.go | 2 +- internal/workspace/reloader.go | 6 +- internal/workspace/reloader_test.go | 12 ++-- internal/workspace/service.go | 28 ++++----- internal/workspace/sort.go | 2 +- internal/workspace/workspace.go | 7 +-- 62 files changed, 375 insertions(+), 381 deletions(-) create mode 100644 internal/resource/id_monotonic.go diff --git a/internal/logging/enricher.go b/internal/logging/enricher.go index ad80d3d..008f32b 100644 --- a/internal/logging/enricher.go +++ b/internal/logging/enricher.go @@ -39,7 +39,7 @@ type ReferenceUpdater[T any] struct { } type Getter[T any] interface { - Get(resource.Identity) (T, error) + Get(resource.ID) (T, error) } func (e *ReferenceUpdater[T]) UpdateArgs(args ...any) []any { @@ -47,7 +47,7 @@ func (e *ReferenceUpdater[T]) UpdateArgs(args ...any) []any { // Where an argument is of type resource.ID, try and retrieve the // resource corresponding to the ID and replace argument with the // resource. - if id, ok := arg.(resource.ID); ok { + if id, ok := arg.(resource.MonotonicID); ok { t, err := e.Get(id) if err != nil { continue @@ -68,7 +68,7 @@ func (e *ReferenceUpdater[T]) UpdateArgs(args ...any) []any { if !f.IsValid() { continue } - id, ok := f.Interface().(resource.ID) + id, ok := f.Interface().(resource.MonotonicID) if !ok { continue } diff --git a/internal/logging/enricher_test.go b/internal/logging/enricher_test.go index 1b925eb..f59bc19 100644 --- a/internal/logging/enricher_test.go +++ b/internal/logging/enricher_test.go @@ -8,7 +8,7 @@ import ( ) func TestReferenceUpdater(t *testing.T) { - res := &fakeResource{ID: resource.NewID(resource.Module)} + res := &fakeResource{MonotonicID: resource.NewMonotonicID(resource.Module)} updater := &ReferenceUpdater[*fakeResource]{ Getter: &fakeResourceGetter{res: res}, Name: "fake", @@ -16,7 +16,7 @@ func TestReferenceUpdater(t *testing.T) { } t.Run("replace resource id with resource", func(t *testing.T) { - args := []any{"fake", res.ID} + args := []any{"fake", res.MonotonicID} got := updater.UpdateArgs(args...) want := []any{"fake", res} @@ -25,10 +25,10 @@ func TestReferenceUpdater(t *testing.T) { t.Run("add resource when referenced from struct with pointer field", func(t *testing.T) { type logMsgArg struct { - FakeResourceID *resource.ID + FakeResourceID *resource.MonotonicID } - args := []any{"arg1", logMsgArg{FakeResourceID: &res.ID}} + args := []any{"arg1", logMsgArg{FakeResourceID: &res.MonotonicID}} got := updater.UpdateArgs(args...) want := append(args, "fake", res) @@ -37,10 +37,10 @@ func TestReferenceUpdater(t *testing.T) { t.Run("add resource when referenced from struct with non-pointer field", func(t *testing.T) { type logMsgArg struct { - FakeResourceID resource.ID + FakeResourceID resource.MonotonicID } - args := []any{"arg1", logMsgArg{FakeResourceID: res.ID}} + args := []any{"arg1", logMsgArg{FakeResourceID: res.MonotonicID}} got := updater.UpdateArgs(args...) want := append(args, "fake", res) @@ -49,7 +49,7 @@ func TestReferenceUpdater(t *testing.T) { t.Run("handle nil pointer from struct", func(t *testing.T) { type logMsgArg struct { - FakeResourceID *resource.ID + FakeResourceID *resource.MonotonicID } args := []any{"arg1", logMsgArg{FakeResourceID: nil}} @@ -60,13 +60,13 @@ func TestReferenceUpdater(t *testing.T) { } type fakeResource struct { - resource.ID + resource.MonotonicID } type fakeResourceGetter struct { res *fakeResource } -func (f *fakeResourceGetter) Get(resource.Identity) (*fakeResource, error) { +func (f *fakeResourceGetter) Get(resource.ID) (*fakeResource, error) { return f.res, nil } diff --git a/internal/logging/logger.go b/internal/logging/logger.go index 1be728d..428616b 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -99,6 +99,6 @@ func (l *Logger) List() []Message { } // Get retrieves a log message by ID. -func (l *Logger) Get(id resource.Identity) (Message, error) { +func (l *Logger) Get(id resource.ID) (Message, error) { return l.writer.table.Get(id) } diff --git a/internal/logging/message.go b/internal/logging/message.go index 374fffc..ae6c5cd 100644 --- a/internal/logging/message.go +++ b/internal/logging/message.go @@ -8,23 +8,19 @@ import ( // Message is the event payload for a log message type Message struct { - // A message is a pug resource, but only insofar as it makes it easier to - // handle consistently alongside all other resources (modules, workspaces, - // etc) in the TUI. - resource.ID - + ID resource.MonotonicID Time time.Time Level string Message string `json:"msg"` Attributes []Attr } +func (m Message) GetID() resource.ID { return m.ID } + type Attr struct { + ID resource.MonotonicID Key string Value string - - // An attribute is a pug resource, but only insofar as it makes it easier to - // handle consistently alongside all other resources (modules, workspaces, - // etc) in the TUI. - resource.ID } + +func (a Attr) GetID() resource.ID { return a.ID } diff --git a/internal/logging/sort.go b/internal/logging/sort.go index 6a61009..51ce89f 100644 --- a/internal/logging/sort.go +++ b/internal/logging/sort.go @@ -2,7 +2,7 @@ package logging // BySerialDesc sorts log messages by their serial. func BySerialDesc(i, j Message) int { - if i.Serial < j.Serial { + if i.ID.Serial < j.ID.Serial { return 1 } return -1 diff --git a/internal/logging/writer.go b/internal/logging/writer.go index 4f33537..4bcd113 100644 --- a/internal/logging/writer.go +++ b/internal/logging/writer.go @@ -19,7 +19,7 @@ func (w *writer) Write(p []byte) (int, error) { d := logfmt.NewDecoder(bytes.NewReader(p)) for d.ScanRecord() { msg := Message{ - ID: resource.NewID(resource.Log), + ID: resource.NewMonotonicID(resource.Log), } for d.ScanKeyval() { switch string(d.Key()) { @@ -37,7 +37,7 @@ func (w *writer) Write(p []byte) (int, error) { msg.Attributes = append(msg.Attributes, Attr{ Key: string(d.Key()), Value: string(d.Value()), - ID: resource.NewID(resource.LogAttr), + ID: resource.NewMonotonicID(resource.LogAttr), }) } } diff --git a/internal/module/module.go b/internal/module/module.go index fc0263e..957cca7 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -16,12 +16,12 @@ import ( // Module is a terraform root module. type Module struct { - resource.ID + ID resource.MonotonicID // Path relative to pug working directory Path string // The module's current workspace. - CurrentWorkspaceID resource.Identity + CurrentWorkspaceID resource.ID // The module's backend type Backend string @@ -41,7 +41,7 @@ type Options struct { // New constructs a module. func New(opts Options) *Module { return &Module{ - ID: resource.NewID(resource.Module), + ID: resource.NewMonotonicID(resource.Module), Path: opts.Path, Backend: opts.Backend, } diff --git a/internal/module/service.go b/internal/module/service.go index fa4a055..bc48906 100644 --- a/internal/module/service.go +++ b/internal/module/service.go @@ -39,10 +39,10 @@ type taskCreator interface { } type moduleTable interface { - Add(id resource.Identity, row *Module) - Update(id resource.Identity, updater func(existing *Module) error) (*Module, error) - Delete(id resource.Identity) - Get(id resource.Identity) (*Module, error) + Add(id resource.ID, row *Module) + Update(id resource.ID, updater func(existing *Module) error) (*Module, error) + Delete(id resource.ID) + Get(id resource.ID) (*Module, error) List() []*Module } @@ -205,7 +205,7 @@ func (s *Service) loadTerragruntDependenciesFromDigraph(r io.Reader) error { const InitTask task.Identifier = "init" // Init invokes terraform init on the module. -func (s *Service) Init(moduleID resource.Identity, upgrade bool) (task.Spec, error) { +func (s *Service) Init(moduleID resource.ID, upgrade bool) (task.Spec, error) { mod, err := s.table.Get(moduleID) if err != nil { return task.Spec{}, err @@ -215,7 +215,7 @@ func (s *Service) Init(moduleID resource.Identity, upgrade bool) (task.Spec, err args = append(args, "-upgrade") } spec := task.Spec{ - ModuleID: &mod.ID, + ModuleID: mod.ID, Path: mod.Path, Identifier: InitTask, Execution: task.Execution{ @@ -230,13 +230,13 @@ func (s *Service) Init(moduleID resource.Identity, upgrade bool) (task.Spec, err return spec, nil } -func (s *Service) Format(moduleID resource.Identity) (task.Spec, error) { +func (s *Service) Format(moduleID resource.ID) (task.Spec, error) { mod, err := s.table.Get(moduleID) if err != nil { return task.Spec{}, err } spec := task.Spec{ - ModuleID: &mod.ID, + ModuleID: mod.ID, Path: mod.Path, Execution: task.Execution{ TerraformCommand: []string{"fmt"}, @@ -247,13 +247,13 @@ func (s *Service) Format(moduleID resource.Identity) (task.Spec, error) { return spec, nil } -func (s *Service) Validate(moduleID resource.Identity) (task.Spec, error) { +func (s *Service) Validate(moduleID resource.ID) (task.Spec, error) { mod, err := s.table.Get(moduleID) if err != nil { return task.Spec{}, err } spec := task.Spec{ - ModuleID: &mod.ID, + ModuleID: mod.ID, Path: mod.Path, Execution: task.Execution{ TerraformCommand: []string{"validate"}, @@ -268,7 +268,7 @@ func (s *Service) List() []*Module { return s.table.List() } -func (s *Service) Get(id resource.Identity) (*Module, error) { +func (s *Service) Get(id resource.ID) (*Module, error) { return s.table.Get(id) } @@ -282,7 +282,7 @@ func (s *Service) GetByPath(path string) (*Module, error) { } // SetCurrent sets the current workspace for the module. -func (s *Service) SetCurrent(moduleID, workspaceID resource.Identity) error { +func (s *Service) SetCurrent(moduleID, workspaceID resource.ID) error { _, err := s.table.Update(moduleID, func(existing *Module) error { existing.CurrentWorkspaceID = workspaceID return nil @@ -291,13 +291,13 @@ func (s *Service) SetCurrent(moduleID, workspaceID resource.Identity) error { } // Execute a program in a module's directory. -func (s *Service) Execute(moduleID resource.Identity, program string, args ...string) (task.Spec, error) { +func (s *Service) Execute(moduleID resource.ID, program string, args ...string) (task.Spec, error) { mod, err := s.table.Get(moduleID) if err != nil { return task.Spec{}, err } spec := task.Spec{ - ModuleID: &mod.ID, + ModuleID: mod.ID, Path: mod.Path, Execution: task.Execution{ Program: program, diff --git a/internal/module/service_test.go b/internal/module/service_test.go index b81d18d..61f83ab 100644 --- a/internal/module/service_test.go +++ b/internal/module/service_test.go @@ -101,7 +101,7 @@ func (f *fakeModuleTable) List() []*Module { return f.modules } -func (f *fakeModuleTable) Update(id resource.Identity, updater func(*Module) error) (*Module, error) { +func (f *fakeModuleTable) Update(id resource.ID, updater func(*Module) error) (*Module, error) { for _, mod := range f.modules { if mod.ID == id { if err := updater(mod); err != nil { diff --git a/internal/plan/plan.go b/internal/plan/plan.go index 6b58f40..44eb155 100644 --- a/internal/plan/plan.go +++ b/internal/plan/plan.go @@ -15,8 +15,7 @@ import ( ) type plan struct { - resource.ID - + ID resource.MonotonicID ModuleID resource.ID WorkspaceID resource.ID ModulePath string @@ -34,7 +33,7 @@ type plan struct { // taskID is the ID of the plan task, and is only set once the task is // created. - taskID *resource.ID + taskID resource.ID } type CreateOptions struct { @@ -56,7 +55,7 @@ type factory struct { terragrunt bool } -func (f *factory) newPlan(workspaceID resource.Identity, opts CreateOptions) (*plan, error) { +func (f *factory) newPlan(workspaceID resource.ID, opts CreateOptions) (*plan, error) { ws, err := f.workspaces.Get(workspaceID) if err != nil { return nil, fmt.Errorf("retrieving workspace: %w", err) @@ -66,7 +65,7 @@ func (f *factory) newPlan(workspaceID resource.Identity, opts CreateOptions) (*p return nil, fmt.Errorf("retrieving module: %w", err) } plan := &plan{ - ID: resource.NewID(resource.Plan), + ID: resource.NewMonotonicID(resource.Plan), ModuleID: mod.ID, WorkspaceID: ws.ID, ModulePath: mod.Path, @@ -78,7 +77,7 @@ func (f *factory) newPlan(workspaceID resource.Identity, opts CreateOptions) (*p moduleDependencies: mod.Dependencies(), } if opts.planFile { - plan.ArtefactsPath = filepath.Join(f.dataDir, fmt.Sprintf("%d", plan.Serial)) + plan.ArtefactsPath = filepath.Join(f.dataDir, fmt.Sprintf("%d", plan.ID.Serial)) if err := os.MkdirAll(plan.ArtefactsPath, 0o755); err != nil { return nil, fmt.Errorf("creating run artefacts directory: %w", err) } @@ -105,8 +104,8 @@ func (r *plan) planTaskSpec() task.Spec { // TODO: assert planFile is true first spec := task.Spec{ Identifier: PlanTask, - ModuleID: &r.ModuleID, - WorkspaceID: &r.WorkspaceID, + ModuleID: r.ModuleID, + WorkspaceID: r.WorkspaceID, Path: r.ModulePath, Env: r.envs, Execution: task.Execution{ @@ -117,7 +116,7 @@ func (r *plan) planTaskSpec() task.Spec { Blocking: true, Description: "plan", AfterCreate: func(t *task.Task) { - r.taskID = &t.ID + r.taskID = t.ID }, BeforeExited: func(t *task.Task) (task.Summary, error) { out, err := io.ReadAll(t.NewReader(false)) @@ -153,8 +152,8 @@ func (r *plan) applyTaskSpec() (task.Spec, error) { } spec := task.Spec{ Identifier: ApplyTask, - ModuleID: &r.ModuleID, - WorkspaceID: &r.WorkspaceID, + ModuleID: r.ModuleID, + WorkspaceID: r.WorkspaceID, Path: r.ModulePath, Execution: task.Execution{ TerraformCommand: []string{"apply"}, diff --git a/internal/plan/plan_test.go b/internal/plan/plan_test.go index e018026..f3641f6 100644 --- a/internal/plan/plan_test.go +++ b/internal/plan/plan_test.go @@ -61,7 +61,7 @@ type fakeModuleGetter struct { mod *module.Module } -func (f *fakeModuleGetter) Get(resource.Identity) (*module.Module, error) { +func (f *fakeModuleGetter) Get(resource.ID) (*module.Module, error) { return f.mod, nil } @@ -69,6 +69,6 @@ type fakeWorkspaceGetter struct { ws *workspace.Workspace } -func (f *fakeWorkspaceGetter) Get(resource.Identity) (*workspace.Workspace, error) { +func (f *fakeWorkspaceGetter) Get(resource.ID) (*workspace.Workspace, error) { return f.ws, nil } diff --git a/internal/plan/service.go b/internal/plan/service.go index 4b44ad2..33aca04 100644 --- a/internal/plan/service.go +++ b/internal/plan/service.go @@ -39,11 +39,11 @@ type ServiceOptions struct { } type moduleGetter interface { - Get(moduleID resource.Identity) (*module.Module, error) + Get(moduleID resource.ID) (*module.Module, error) } type workspaceGetter interface { - Get(workspaceID resource.Identity) (*workspace.Workspace, error) + Get(workspaceID resource.ID) (*workspace.Workspace, error) } func NewService(opts ServiceOptions) *Service { @@ -83,18 +83,18 @@ func (s *Service) ReloadAfterApply(sub <-chan resource.Event[*task.Task]) { if workspaceID == nil { continue } - if _, err := s.states.CreateReloadTask(*workspaceID); err != nil { - s.logger.Error("reloading state after apply", "error", err, "workspace", *workspaceID) + if _, err := s.states.CreateReloadTask(workspaceID); err != nil { + s.logger.Error("reloading state after apply", "error", err, "workspace", workspaceID) continue } - s.logger.Debug("reloading state after apply", "workspace", *workspaceID) + s.logger.Debug("reloading state after apply", "workspace", workspaceID) } } } // Plan creates a task spec to create a plan, i.e. `terraform plan -out // plan.file`. -func (s *Service) Plan(workspaceID resource.Identity, opts CreateOptions) (task.Spec, error) { +func (s *Service) Plan(workspaceID resource.ID, opts CreateOptions) (task.Spec, error) { opts.planFile = true plan, err := s.newPlan(workspaceID, opts) if err != nil { @@ -108,7 +108,7 @@ func (s *Service) Plan(workspaceID resource.Identity, opts CreateOptions) (task. // Apply creates a task spec to auto-apply a plan, i.e. `terraform apply`. To // apply an existing plan, see ApplyPlan. -func (s *Service) Apply(workspaceID resource.Identity, opts CreateOptions) (task.Spec, error) { +func (s *Service) Apply(workspaceID resource.ID, opts CreateOptions) (task.Spec, error) { plan, err := s.newPlan(workspaceID, opts) if err != nil { return task.Spec{}, err @@ -119,7 +119,7 @@ func (s *Service) Apply(workspaceID resource.Identity, opts CreateOptions) (task // ApplyPlan creates a task spec to apply an existing plan, i.e. `terraform // apply existing.plan`. The taskID is the ID of a plan task, which must have // finished successfully. -func (s *Service) ApplyPlan(taskID resource.Identity) (task.Spec, error) { +func (s *Service) ApplyPlan(taskID resource.ID) (task.Spec, error) { planTask, err := s.tasks.Get(taskID) if err != nil { return task.Spec{}, err @@ -144,13 +144,13 @@ func IsApplyable(t *task.Task) error { return nil } -func (s *Service) Get(runID resource.Identity) (*plan, error) { +func (s *Service) Get(runID resource.ID) (*plan, error) { return s.table.Get(runID) } -func (s *Service) getByTaskID(taskID resource.Identity) (*plan, error) { +func (s *Service) getByTaskID(taskID resource.ID) (*plan, error) { for _, plan := range s.List() { - if plan.taskID != nil && *plan.taskID == taskID { + if plan.taskID != nil && plan.taskID == taskID { return plan, nil } } diff --git a/internal/resource/id.go b/internal/resource/id.go index 131f22d..d4ba97a 100644 --- a/internal/resource/id.go +++ b/internal/resource/id.go @@ -1,48 +1,8 @@ package resource -import ( - "fmt" - "sync" -) - -// Identity is anything that uniquely differentiates a resource from another. -type Identity any +// ID uniquely identifies a Pug resource. +type ID any type Identifiable interface { - GetID() Identity -} - -var ( - // nextID provides the next ID for each kind - nextID map[Kind]uint = make(map[Kind]uint) - mu sync.Mutex -) - -// ID is a unique identifier for a pug resource. -type ID struct { - Serial uint - Kind Kind -} - -func NewID(kind Kind) ID { - mu.Lock() - defer mu.Unlock() - - id := nextID[kind] - nextID[kind]++ - - return ID{ - Serial: id, - Kind: kind, - } -} - -// String provides a human readable description. -func (id ID) String() string { - return fmt.Sprintf("#%d", id.Serial) -} - -// GetID implements Identifiable -func (id ID) GetID() Identity { - return id + GetID() ID } diff --git a/internal/resource/id_monotonic.go b/internal/resource/id_monotonic.go new file mode 100644 index 0000000..20f7f5c --- /dev/null +++ b/internal/resource/id_monotonic.go @@ -0,0 +1,41 @@ +package resource + +import ( + "fmt" + "sync" +) + +var ( + // nextMonotonicID provides the next monotonic ID for each kind + nextMonotonicID map[Kind]uint = make(map[Kind]uint) + mu sync.Mutex +) + +// MonotonicID is a unique identifier for a pug resource. +type MonotonicID struct { + Serial uint + Kind Kind +} + +func NewMonotonicID(kind Kind) MonotonicID { + mu.Lock() + defer mu.Unlock() + + id := nextMonotonicID[kind] + nextMonotonicID[kind]++ + + return MonotonicID{ + Serial: id, + Kind: kind, + } +} + +// String provides a human readable description. +func (id MonotonicID) String() string { + return fmt.Sprintf("#%d", id.Serial) +} + +// GetID implements Identifiable +func (id MonotonicID) GetID() ID { + return id +} diff --git a/internal/resource/id_test.go b/internal/resource/id_test.go index 5e9d15b..6bd3611 100644 --- a/internal/resource/id_test.go +++ b/internal/resource/id_test.go @@ -8,10 +8,10 @@ import ( ) func TestID_String(t *testing.T) { - mod := NewID(Module) - ws := NewID(Workspace) - run := NewID(Plan) - task := NewID(Task) + mod := NewMonotonicID(Module) + ws := NewMonotonicID(Workspace) + run := NewMonotonicID(Plan) + task := NewMonotonicID(Task) t.Run("string", func(t *testing.T) { assert.True(t, strings.HasPrefix(mod.String(), "#")) diff --git a/internal/resource/resource.go b/internal/resource/resource.go index 59d92cb..7410be0 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -3,7 +3,7 @@ package resource // Resource is a unique pug entity type Resource interface { // GetID retrieves the unique identifier for the resource. - GetID() ID + GetID() MonotonicID // String is a human-readable identifier for the resource. Not necessarily // unique across pug. String() string diff --git a/internal/resource/table.go b/internal/resource/table.go index 1bd3702..5baa74c 100644 --- a/internal/resource/table.go +++ b/internal/resource/table.go @@ -9,7 +9,7 @@ import ( // Table is an in-memory database table that emits events upon changes. type Table[T any] struct { - rows map[Identity]T + rows map[ID]T mu sync.RWMutex pub Publisher[T] @@ -17,12 +17,12 @@ type Table[T any] struct { func NewTable[T any](pub Publisher[T]) *Table[T] { return &Table[T]{ - rows: make(map[Identity]T), + rows: make(map[ID]T), pub: pub, } } -func (t *Table[T]) Add(id Identity, row T) { +func (t *Table[T]) Add(id ID, row T) { t.mu.Lock() defer t.mu.Unlock() @@ -30,7 +30,7 @@ func (t *Table[T]) Add(id Identity, row T) { t.pub.Publish(CreatedEvent, row) } -func (t *Table[T]) Update(id Identity, updater func(existing T) error) (T, error) { +func (t *Table[T]) Update(id ID, updater func(existing T) error) (T, error) { t.mu.Lock() defer t.mu.Unlock() @@ -48,7 +48,7 @@ func (t *Table[T]) Update(id Identity, updater func(existing T) error) (T, error return row, nil } -func (t *Table[T]) Delete(id Identity) { +func (t *Table[T]) Delete(id ID) { t.mu.Lock() defer t.mu.Unlock() @@ -57,7 +57,7 @@ func (t *Table[T]) Delete(id Identity) { t.pub.Publish(DeletedEvent, row) } -func (t *Table[T]) Get(id Identity) (T, error) { +func (t *Table[T]) Get(id ID) (T, error) { t.mu.RLock() defer t.mu.RUnlock() diff --git a/internal/state/reloader.go b/internal/state/reloader.go index e511dae..70f34bc 100644 --- a/internal/state/reloader.go +++ b/internal/state/reloader.go @@ -13,7 +13,7 @@ type reloader struct { // Reload creates a task to repopulate the local cache of the state of the given // workspace. -func (r *reloader) Reload(workspaceID resource.Identity) (task.Spec, error) { +func (r *reloader) Reload(workspaceID resource.ID) (task.Spec, error) { return r.createTaskSpec(workspaceID, task.Spec{ Execution: task.Execution{ TerraformCommand: []string{"state", "pull"}, @@ -42,7 +42,7 @@ func (r *reloader) Reload(workspaceID resource.Identity) (task.Spec, error) { }) } -func (s *Service) CreateReloadTask(workspaceID resource.Identity) (*task.Task, error) { +func (s *Service) CreateReloadTask(workspaceID resource.ID) (*task.Task, error) { spec, err := s.Reload(workspaceID) if err != nil { return nil, fmt.Errorf("creating reload task spec: %w", err) diff --git a/internal/state/resource.go b/internal/state/resource.go index 1b1098b..f4052c5 100644 --- a/internal/state/resource.go +++ b/internal/state/resource.go @@ -8,9 +8,9 @@ import ( // Resource is a pug state resource. type Resource struct { - resource.ID + resource.MonotonicID - WorkspaceID resource.Identity + WorkspaceID resource.ID Address ResourceAddress Attributes map[string]any Tainted bool @@ -20,9 +20,9 @@ func (r *Resource) String() string { return string(r.Address) } -func newResource(workspaceID resource.Identity, addr ResourceAddress, attrs json.RawMessage) (*Resource, error) { +func newResource(workspaceID resource.ID, addr ResourceAddress, attrs json.RawMessage) (*Resource, error) { res := &Resource{ - ID: resource.NewID(resource.StateResource), + MonotonicID: resource.NewMonotonicID(resource.StateResource), WorkspaceID: workspaceID, Address: addr, } diff --git a/internal/state/service.go b/internal/state/service.go index ca60b72..fb00e7c 100644 --- a/internal/state/service.go +++ b/internal/state/service.go @@ -44,17 +44,17 @@ func NewService(opts ServiceOptions) *Service { } // Get retrieves the state for a workspace. -func (s *Service) Get(workspaceID resource.Identity) (*State, error) { +func (s *Service) Get(workspaceID resource.ID) (*State, error) { return s.cache.Get(workspaceID) } // GetResource retrieves a state resource. // // TODO: this is massively inefficient -func (s *Service) GetResource(resourceID resource.Identity) (*Resource, error) { +func (s *Service) GetResource(resourceID resource.ID) (*Resource, error) { for _, state := range s.cache.List() { for _, res := range state.Resources { - if res.ID == resourceID { + if res.MonotonicID == resourceID { return res, nil } } @@ -62,7 +62,7 @@ func (s *Service) GetResource(resourceID resource.Identity) (*Resource, error) { return nil, resource.ErrNotFound } -func (s *Service) Delete(workspaceID resource.Identity, addrs ...ResourceAddress) (task.Spec, error) { +func (s *Service) Delete(workspaceID resource.ID, addrs ...ResourceAddress) (task.Spec, error) { addrStrings := make([]string, len(addrs)) for i, addr := range addrs { addrStrings[i] = string(addr) @@ -83,7 +83,7 @@ func (s *Service) Delete(workspaceID resource.Identity, addrs ...ResourceAddress }) } -func (s *Service) Taint(workspaceID resource.Identity, addr ResourceAddress) (task.Spec, error) { +func (s *Service) Taint(workspaceID resource.ID, addr ResourceAddress) (task.Spec, error) { return s.createTaskSpec(workspaceID, task.Spec{ Blocking: true, Execution: task.Execution{ @@ -100,7 +100,7 @@ func (s *Service) Taint(workspaceID resource.Identity, addr ResourceAddress) (ta }) } -func (s *Service) Untaint(workspaceID resource.Identity, addr ResourceAddress) (task.Spec, error) { +func (s *Service) Untaint(workspaceID resource.ID, addr ResourceAddress) (task.Spec, error) { return s.createTaskSpec(workspaceID, task.Spec{ Blocking: true, Execution: task.Execution{ @@ -117,7 +117,7 @@ func (s *Service) Untaint(workspaceID resource.Identity, addr ResourceAddress) ( }) } -func (s *Service) Move(workspaceID resource.Identity, src, dest ResourceAddress) (task.Spec, error) { +func (s *Service) Move(workspaceID resource.ID, src, dest ResourceAddress) (task.Spec, error) { return s.createTaskSpec(workspaceID, task.Spec{ Blocking: true, Execution: task.Execution{ @@ -135,7 +135,7 @@ func (s *Service) Move(workspaceID resource.Identity, src, dest ResourceAddress) } // TODO: move this logic into task.Create -func (s *Service) createTaskSpec(workspaceID resource.Identity, opts task.Spec) (task.Spec, error) { +func (s *Service) createTaskSpec(workspaceID resource.ID, opts task.Spec) (task.Spec, error) { ws, err := s.workspaces.Get(workspaceID) if err != nil { return task.Spec{}, err @@ -144,8 +144,8 @@ func (s *Service) createTaskSpec(workspaceID resource.Identity, opts task.Spec) if err != nil { return task.Spec{}, err } - opts.ModuleID = &mod.ID - opts.WorkspaceID = &ws.ID + opts.ModuleID = mod.ID + opts.WorkspaceID = ws.ID opts.Env = []string{ws.TerraformEnv()} opts.Path = mod.Path diff --git a/internal/state/state.go b/internal/state/state.go index 5b6abb4..5fbc520 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -12,19 +12,19 @@ import ( ) type State struct { - resource.ID + resource.MonotonicID - WorkspaceID resource.Identity + WorkspaceID resource.ID Resources map[ResourceAddress]*Resource Serial int64 TerraformVersion string Lineage string } -func newState(workspaceID resource.Identity, r io.Reader) (*State, error) { +func newState(workspaceID resource.ID, r io.Reader) (*State, error) { // Default to a serial of -1 to indicate that there is no state yet. state := &State{ - ID: resource.NewID(resource.State), + MonotonicID: resource.NewMonotonicID(resource.State), WorkspaceID: workspaceID, Serial: -1, } diff --git a/internal/state/state_test.go b/internal/state/state_test.go index 89d22cf..eb7e399 100644 --- a/internal/state/state_test.go +++ b/internal/state/state_test.go @@ -82,5 +82,4 @@ func TestState(t *testing.T) { } assert.Equal(t, wantAttrs, got.Resources[`time_sleep.wait_three_seconds["duration"]`].Attributes) }) - } diff --git a/internal/task/dependency_graph_builder.go b/internal/task/dependency_graph_builder.go index 14d7516..5bc48a1 100644 --- a/internal/task/dependency_graph_builder.go +++ b/internal/task/dependency_graph_builder.go @@ -21,12 +21,12 @@ func createDependentTasks(svc taskCreator, reverse bool, specs ...Spec) ([]*Task // the specs that belong to that module. Once the graph is built and // dependencies are established, only then are tasks created from the specs. for _, spec := range specs { - node, ok := b.nodes[*spec.ModuleID] + node, ok := b.nodes[spec.ModuleID] if !ok { node = &dependencyGraphNode{dependencies: spec.Dependencies.ModuleIDs} } node.specs = append(node.specs, spec) - b.nodes[*spec.ModuleID] = node + b.nodes[spec.ModuleID] = node } for id, v := range b.nodes { if !v.visited { diff --git a/internal/task/dependency_graph_builder_test.go b/internal/task/dependency_graph_builder_test.go index 372b400..ea0341f 100644 --- a/internal/task/dependency_graph_builder_test.go +++ b/internal/task/dependency_graph_builder_test.go @@ -15,19 +15,19 @@ func (f *fakeTaskCreator) Create(spec Spec) (*Task, error) { } func TestNewGroupWithDependencies(t *testing.T) { - vpcID := resource.NewID(resource.Module) - mysqlID := resource.NewID(resource.Module) - redisID := resource.NewID(resource.Module) - backendID := resource.NewID(resource.Module) - frontendID := resource.NewID(resource.Module) - mqID := resource.NewID(resource.Module) + vpcID := resource.NewMonotonicID(resource.Module) + mysqlID := resource.NewMonotonicID(resource.Module) + redisID := resource.NewMonotonicID(resource.Module) + backendID := resource.NewMonotonicID(resource.Module) + frontendID := resource.NewMonotonicID(resource.Module) + mqID := resource.NewMonotonicID(resource.Module) - vpcSpec := Spec{ModuleID: &vpcID, Dependencies: &Dependencies{}} - mysqlSpec := Spec{ModuleID: &mysqlID, Dependencies: &Dependencies{ModuleIDs: []resource.ID{vpcID}}} - redisSpec := Spec{ModuleID: &redisID, Dependencies: &Dependencies{ModuleIDs: []resource.ID{vpcID}}} - backendSpec := Spec{ModuleID: &backendID, Dependencies: &Dependencies{ModuleIDs: []resource.ID{vpcID, mysqlID, redisID}}} - frontendSpec := Spec{ModuleID: &frontendID, Dependencies: &Dependencies{ModuleIDs: []resource.ID{vpcID, backendID}}} - mqSpec := Spec{ModuleID: &mqID, Dependencies: &Dependencies{}} + vpcSpec := Spec{ModuleID: vpcID, Dependencies: &Dependencies{}} + mysqlSpec := Spec{ModuleID: mysqlID, Dependencies: &Dependencies{ModuleIDs: []resource.ID{vpcID}}} + redisSpec := Spec{ModuleID: redisID, Dependencies: &Dependencies{ModuleIDs: []resource.ID{vpcID}}} + backendSpec := Spec{ModuleID: backendID, Dependencies: &Dependencies{ModuleIDs: []resource.ID{vpcID, mysqlID, redisID}}} + frontendSpec := Spec{ModuleID: frontendID, Dependencies: &Dependencies{ModuleIDs: []resource.ID{vpcID, backendID}}} + mqSpec := Spec{ModuleID: mqID, Dependencies: &Dependencies{}} t.Run("normal order", func(t *testing.T) { got, err := createDependentTasks(&fakeTaskCreator{}, false, @@ -72,9 +72,11 @@ func TestNewGroupWithDependencies(t *testing.T) { }) } -func hasDependencies(t *testing.T, got []*Task, want resource.ID, deps ...resource.ID) resource.ID { +func hasDependencies(t *testing.T, got []*Task, wantModuleID resource.ID, deps ...resource.MonotonicID) resource.MonotonicID { + t.Helper() + for _, task := range got { - if task.ModuleID != nil && *task.ModuleID == want { + if task.ModuleID != nil && task.ModuleID == wantModuleID { // Module matches, so now check dependencies if assert.Len(t, task.DependsOn, len(deps)) { for _, dep := range deps { @@ -84,6 +86,6 @@ func hasDependencies(t *testing.T, got []*Task, want resource.ID, deps ...resour } } } - t.Fatalf("%s not found in %v", want, got) - return resource.ID{} + t.Fatalf("%s not found in %v", wantModuleID, got) + return resource.MonotonicID{} } diff --git a/internal/task/enqueuer.go b/internal/task/enqueuer.go index 3a97ba7..6fe24f1 100644 --- a/internal/task/enqueuer.go +++ b/internal/task/enqueuer.go @@ -23,7 +23,7 @@ type enqueuer struct { type enqueuerTaskService interface { taskLister - Get(taskID resource.Identity) (*Task, error) + Get(taskID resource.ID) (*Task, error) } func StartEnqueuer(tasks *Service) { @@ -58,10 +58,10 @@ func (e *enqueuer) enqueuable() []*Task { for _, t := range active { if t.Blocking { if t.ModuleID != nil { - blockedModules[*t.ModuleID] = struct{}{} + blockedModules[t.ModuleID] = struct{}{} } if t.WorkspaceID != nil { - blockedWorkspaces[*t.WorkspaceID] = struct{}{} + blockedWorkspaces[t.WorkspaceID] = struct{}{} } } } @@ -79,13 +79,13 @@ func (e *enqueuer) enqueuable() []*Task { continue } if t.WorkspaceID != nil { - if _, ok := blockedWorkspaces[*t.WorkspaceID]; ok { + if _, ok := blockedWorkspaces[t.WorkspaceID]; ok { // Don't enqueue task belonging to workspace blocked by another task continue } } if t.ModuleID != nil { - if _, ok := blockedModules[*t.ModuleID]; ok { + if _, ok := blockedModules[t.ModuleID]; ok { // Don't enqueue task belonging to module blocked by another task continue } @@ -102,12 +102,12 @@ func (e *enqueuer) enqueuable() []*Task { if t.WorkspaceID != nil { // Task blocks workspace; no further tasks belonging to workspace // shall be enqueued. - blockedWorkspaces[*t.WorkspaceID] = struct{}{} + blockedWorkspaces[t.WorkspaceID] = struct{}{} } if t.ModuleID != nil { // Task blocks module; no further tasks belonging to module // shall be enqueued. - blockedModules[*t.ModuleID] = struct{}{} + blockedModules[t.ModuleID] = struct{}{} } } } diff --git a/internal/task/enqueuer_test.go b/internal/task/enqueuer_test.go index b2b580d..5deda21 100644 --- a/internal/task/enqueuer_test.go +++ b/internal/task/enqueuer_test.go @@ -13,25 +13,25 @@ import ( func TestEnqueuer(t *testing.T) { t.Parallel() - mod1ID := resource.NewID(resource.Module) - ws1ID := resource.NewID(resource.Workspace) + mod1ID := resource.NewMonotonicID(resource.Module) + ws1ID := resource.NewMonotonicID(resource.Workspace) - mod1Task1 := newTestTask(t, Spec{ModuleID: &mod1ID}) - mod1TaskBlocking1 := newTestTask(t, Spec{ModuleID: &mod1ID, Blocking: true}) + mod1Task1 := newTestTask(t, Spec{ModuleID: mod1ID}) + mod1TaskBlocking1 := newTestTask(t, Spec{ModuleID: mod1ID, Blocking: true}) - ws1Task1 := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID}) - ws1Task2 := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID}) + ws1Task1 := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID}) + ws1Task2 := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID}) - ws1TaskBlocking1 := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID, Blocking: true}) - ws1TaskBlocking2 := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID, Blocking: true}) - ws1TaskBlocking3 := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID, Blocking: true}) - ws1TaskImmediate := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID, Immediate: true}) - ws1TaskDependOnTask1 := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID, dependsOn: []resource.ID{ws1Task1.ID}}) + ws1TaskBlocking1 := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID, Blocking: true}) + ws1TaskBlocking2 := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID, Blocking: true}) + ws1TaskBlocking3 := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID, Blocking: true}) + ws1TaskImmediate := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID, Immediate: true}) + ws1TaskDependOnTask1 := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID, dependsOn: []resource.ID{ws1Task1.ID}}) - ws1TaskCompleted := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID}) + ws1TaskCompleted := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID}) ws1TaskCompleted.updateState(Exited) - ws1TaskDependOnCompletedTask := newTestTask(t, Spec{ModuleID: &mod1ID, WorkspaceID: &ws1ID, dependsOn: []resource.ID{ws1TaskCompleted.ID}}) + ws1TaskDependOnCompletedTask := newTestTask(t, Spec{ModuleID: mod1ID, WorkspaceID: ws1ID, dependsOn: []resource.ID{ws1TaskCompleted.ID}}) tests := []struct { name string @@ -140,7 +140,7 @@ func (f *fakeEnqueuerTaskService) List(opts ListOptions) []*Task { return nil } -func (f *fakeEnqueuerTaskService) Get(id resource.Identity) (*Task, error) { +func (f *fakeEnqueuerTaskService) Get(id resource.ID) (*Task, error) { for _, task := range append(append(f.pending, f.active...), f.other...) { if id == task.ID { return task, nil diff --git a/internal/task/group.go b/internal/task/group.go index 1da5002..ff2a85b 100644 --- a/internal/task/group.go +++ b/internal/task/group.go @@ -10,7 +10,7 @@ import ( ) type Group struct { - resource.ID + resource.MonotonicID Created time.Time Command string @@ -23,8 +23,8 @@ func newGroup(service *Service, specs ...Spec) (*Group, error) { return nil, errors.New("no specs provided") } g := &Group{ - ID: resource.NewID(resource.TaskGroup), - Created: time.Now(), + MonotonicID: resource.NewMonotonicID(resource.TaskGroup), + Created: time.Now(), } // Validate specifications. There are some settings that are incompatible // with one another within a task group. @@ -86,7 +86,7 @@ func newGroup(service *Service, specs ...Spec) (*Group, error) { func (g *Group) String() string { return g.Command } -func (g *Group) IncludesTask(taskID resource.ID) bool { +func (g *Group) IncludesTask(taskID resource.MonotonicID) bool { return slices.ContainsFunc(g.Tasks, func(tgt *Task) bool { return tgt.ID == taskID }) diff --git a/internal/task/service.go b/internal/task/service.go index b54d9c1..d15eab6 100644 --- a/internal/task/service.go +++ b/internal/task/service.go @@ -109,11 +109,11 @@ func (s *Service) CreateGroup(specs ...Spec) (*Group, error) { // AddGroup adds a task group to the DB. func (s *Service) AddGroup(group *Group) { - s.groups.Add(group.ID, group) + s.groups.Add(group.MonotonicID, group) } // Enqueue moves the task onto the global queue for processing. -func (s *Service) Enqueue(taskID resource.Identity) (*Task, error) { +func (s *Service) Enqueue(taskID resource.ID) (*Task, error) { task, err := s.tasks.Update(taskID, func(existing *Task) error { existing.updateState(Queued) return nil @@ -191,15 +191,15 @@ func (s *Service) ListGroups() []*Group { return s.groups.List() } -func (s *Service) Get(taskID resource.Identity) (*Task, error) { +func (s *Service) Get(taskID resource.ID) (*Task, error) { return s.tasks.Get(taskID) } -func (s *Service) GetGroup(groupID resource.Identity) (*Group, error) { +func (s *Service) GetGroup(groupID resource.ID) (*Group, error) { return s.groups.Get(groupID) } -func (s *Service) Cancel(taskID resource.Identity) (*Task, error) { +func (s *Service) Cancel(taskID resource.ID) (*Task, error) { task, err := func() (*Task, error) { task, err := s.tasks.Get(taskID) if err != nil { @@ -216,7 +216,7 @@ func (s *Service) Cancel(taskID resource.Identity) (*Task, error) { return task, nil } -func (s *Service) Delete(taskID resource.Identity) error { +func (s *Service) Delete(taskID resource.ID) error { // TODO: only allow deleting task if in finished state (error message should // instruct user to cancel task first). s.tasks.Delete(taskID) diff --git a/internal/task/service_test.go b/internal/task/service_test.go index 7070054..c19309f 100644 --- a/internal/task/service_test.go +++ b/internal/task/service_test.go @@ -10,11 +10,11 @@ import ( func TestService_List(t *testing.T) { t.Parallel() - pending := &Task{ID: resource.NewID(resource.Task), State: Pending} - queued := &Task{ID: resource.NewID(resource.Task), State: Queued} - running := &Task{ID: resource.NewID(resource.Task), State: Running} - exited := &Task{ID: resource.NewID(resource.Task), State: Exited} - errored := &Task{ID: resource.NewID(resource.Task), State: Errored} + pending := &Task{ID: resource.NewMonotonicID(resource.Task), State: Pending} + queued := &Task{ID: resource.NewMonotonicID(resource.Task), State: Queued} + running := &Task{ID: resource.NewMonotonicID(resource.Task), State: Running} + exited := &Task{ID: resource.NewMonotonicID(resource.Task), State: Exited} + errored := &Task{ID: resource.NewMonotonicID(resource.Task), State: Errored} tests := []struct { name string diff --git a/internal/task/spec.go b/internal/task/spec.go index 4e9c575..e1171f1 100644 --- a/internal/task/spec.go +++ b/internal/task/spec.go @@ -6,13 +6,13 @@ import "github.com/leg100/pug/internal/resource" type Spec struct { // ModuleID is the ID of the module the task belongs to. If nil, the task // does not belong to a module - ModuleID *resource.ID + ModuleID resource.ID // WorkspaceID is the ID of the workspace the task belongs to. If nil, the // task does not belong to a workspace. - WorkspaceID *resource.ID + WorkspaceID resource.ID // TaskGroupID specifies the ID of the task group this task is to belong to. // Nil means the task does not belong to a group. - TaskGroupID *resource.ID + TaskGroupID resource.ID // Execution specifies the execution of a program. Execution Execution // AdditionalExecution specifies the execution of another program. The @@ -71,7 +71,7 @@ type Spec struct { } // SpecFunc is a function that creates a spec. -type SpecFunc func(resource.Identity) (Spec, error) +type SpecFunc func(resource.ID) (Spec, error) // Execution specifies the program and arguments to execute type Execution struct { diff --git a/internal/task/task.go b/internal/task/task.go index 82b9e66..873e876 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -23,11 +23,10 @@ type Identifier string // Task is an execution of a CLI program. type Task struct { - resource.ID - - ModuleID *resource.ID - WorkspaceID *resource.ID - TaskGroupID *resource.ID + ID resource.MonotonicID + ModuleID resource.ID + WorkspaceID resource.ID + TaskGroupID resource.ID Identifier Identifier Program string Args []string @@ -116,7 +115,7 @@ func (f *factory) newTask(spec Spec) (*Task, error) { return nil, errors.New("a task cannot both be blocking and immediately") } task := &Task{ - ID: resource.NewID(resource.Task), + ID: resource.NewMonotonicID(resource.Task), ModuleID: spec.ModuleID, WorkspaceID: spec.WorkspaceID, TaskGroupID: spec.TaskGroupID, @@ -199,9 +198,8 @@ func (f *factory) newTask(spec Spec) (*Task, error) { return task, nil } -func (t *Task) String() string { - return t.Description -} +func (t *Task) GetID() resource.ID { return t.ID } +func (t *Task) String() string { return t.Description } // NewReader returns a reader which contains what has been written thus far to // the task buffer. diff --git a/internal/tui/actions.go b/internal/tui/actions.go index a62249e..28aedab 100644 --- a/internal/tui/actions.go +++ b/internal/tui/actions.go @@ -19,8 +19,8 @@ type ActionHandler struct { } type IDRetriever interface { - GetModuleIDs() ([]resource.Identity, error) - GetWorkspaceIDs() ([]resource.Identity, error) + GetModuleIDs() ([]resource.ID, error) + GetWorkspaceIDs() ([]resource.ID, error) } func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { @@ -42,7 +42,7 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { if err != nil { return ReportError(err) } - fn := func(moduleID resource.Identity) (task.Spec, error) { + fn := func(moduleID resource.ID) (task.Spec, error) { return m.Modules.Init(moduleID, upgrade) } return m.CreateTasks(fn, ids...) @@ -62,7 +62,7 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { parts := strings.Split(v, " ") prog := parts[0] args := parts[1:] - fn := func(moduleID resource.Identity) (task.Spec, error) { + fn := func(moduleID resource.ID) (task.Spec, error) { return m.Modules.Execute(moduleID, prog, args...) } return m.CreateTasks(fn, ids...) @@ -92,7 +92,7 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { if err != nil { return ReportError(err) } - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.Plans.Plan(workspaceID, createPlanOptions) } return m.CreateTasks(fn, ids...) @@ -105,7 +105,7 @@ func (m *ActionHandler) Update(msg tea.Msg) tea.Cmd { if err != nil { return ReportError(err) } - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.Plans.Apply(workspaceID, createPlanOptions) } return YesNoPrompt( diff --git a/internal/tui/explorer/model.go b/internal/tui/explorer/model.go index 3751118..51eedbe 100644 --- a/internal/tui/explorer/model.go +++ b/internal/tui/explorer/model.go @@ -26,7 +26,7 @@ type Maker struct { Helpers *tui.Helpers } -func (mm *Maker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { +func (mm *Maker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { builder := &treeBuilder{ wd: mm.Workdir, helpers: mm.Helpers, @@ -308,7 +308,7 @@ func (m model) filterVisible() bool { // respective *current* workspaces. An error is returned if all modules don't // have a current workspace or if any other type of rows are selected or are // currently the cursor row. -func (m model) GetWorkspaceIDs() ([]resource.Identity, error) { +func (m model) GetWorkspaceIDs() ([]resource.ID, error) { kind, ids := m.tracker.getSelectedOrCurrentIDs() switch kind { case resource.Workspace: @@ -335,7 +335,7 @@ func (m model) GetWorkspaceIDs() ([]resource.Identity, error) { // the IDs of their respective parent modules; if the rows are modules then it // returns their IDs. An error is returned if the rows are not workspaces or // modules. -func (m model) GetModuleIDs() ([]resource.Identity, error) { +func (m model) GetModuleIDs() ([]resource.ID, error) { kind, ids := m.tracker.getSelectedOrCurrentIDs() switch kind { case resource.Module: diff --git a/internal/tui/explorer/nodes.go b/internal/tui/explorer/nodes.go index b6b6f73..655404e 100644 --- a/internal/tui/explorer/nodes.go +++ b/internal/tui/explorer/nodes.go @@ -39,7 +39,7 @@ func (d dirNode) String() string { } type moduleNode struct { - id resource.ID + id resource.MonotonicID path string } @@ -56,7 +56,7 @@ func (m moduleNode) String() string { } type workspaceNode struct { - id resource.ID + id resource.MonotonicID name string current bool resourceCount string diff --git a/internal/tui/explorer/selector.go b/internal/tui/explorer/selector.go index aa0b1df..80209aa 100644 --- a/internal/tui/explorer/selector.go +++ b/internal/tui/explorer/selector.go @@ -15,12 +15,12 @@ var ( // not directories), and resources must be of the same kind. // selected. type selector struct { - selections map[resource.Identity]struct{} + selections map[resource.ID]struct{} kind *resource.Kind } func (s *selector) add(n node) error { - id, ok := n.ID().(resource.ID) + id, ok := n.ID().(resource.MonotonicID) if !ok { return ErrUnselectableNode } @@ -40,9 +40,9 @@ func (s *selector) reindex(nodes []node) { if len(s.selections) == 0 { return } - selections := make(map[resource.Identity]struct{}, len(s.selections)) + selections := make(map[resource.ID]struct{}, len(s.selections)) for _, n := range nodes { - id, ok := n.ID().(resource.ID) + id, ok := n.ID().(resource.MonotonicID) if !ok { continue } @@ -121,7 +121,7 @@ func (s *selector) addRange(cursor node, cursorIndex int, nodes ...node) error { } func (s *selector) remove(n node) { - id, ok := n.ID().(resource.ID) + id, ok := n.ID().(resource.MonotonicID) if !ok { // silently ignore request to remove non-resource node return @@ -133,7 +133,7 @@ func (s *selector) remove(n node) { } func (s *selector) removeAll() { - s.selections = make(map[resource.Identity]struct{}) + s.selections = make(map[resource.ID]struct{}) s.kind = nil } @@ -146,7 +146,7 @@ func (s *selector) toggle(n node) error { } func (s *selector) isSelected(n node) bool { - id, ok := n.ID().(resource.ID) + id, ok := n.ID().(resource.MonotonicID) if !ok { // non-resource nodes cannot be selected return false diff --git a/internal/tui/explorer/selector_test.go b/internal/tui/explorer/selector_test.go index e6c9404..325c488 100644 --- a/internal/tui/explorer/selector_test.go +++ b/internal/tui/explorer/selector_test.go @@ -8,10 +8,10 @@ import ( ) func TestSelector_isSelected(t *testing.T) { - mod1 := moduleNode{id: resource.NewID(resource.Module)} - mod2 := moduleNode{id: resource.NewID(resource.Module)} + mod1 := moduleNode{id: resource.NewMonotonicID(resource.Module)} + mod2 := moduleNode{id: resource.NewMonotonicID(resource.Module)} - s := selector{selections: make(map[resource.Identity]struct{})} + s := selector{selections: make(map[resource.ID]struct{})} s.add(mod1) assert.True(t, s.isSelected(mod1)) @@ -19,10 +19,10 @@ func TestSelector_isSelected(t *testing.T) { } func TestSelector_reindex(t *testing.T) { - mod1 := moduleNode{id: resource.NewID(resource.Module)} - mod2 := moduleNode{id: resource.NewID(resource.Module)} + mod1 := moduleNode{id: resource.NewMonotonicID(resource.Module)} + mod2 := moduleNode{id: resource.NewMonotonicID(resource.Module)} - s := selector{selections: make(map[resource.Identity]struct{})} + s := selector{selections: make(map[resource.ID]struct{})} s.add(mod1) s.add(mod2) diff --git a/internal/tui/explorer/tracker.go b/internal/tui/explorer/tracker.go index 840d9a2..4825bda 100644 --- a/internal/tui/explorer/tracker.go +++ b/internal/tui/explorer/tracker.go @@ -26,7 +26,7 @@ type tracker struct { func newTracker(tree *tree, height int) *tracker { t := &tracker{ selector: &selector{ - selections: make(map[resource.Identity]struct{}), + selections: make(map[resource.ID]struct{}), }, } t.reindex(tree, height) @@ -123,14 +123,14 @@ func (t *tracker) selectRange() error { return t.selector.addRange(t.cursorNode, t.cursorIndex, t.nodes...) } -func (t *tracker) getSelectedOrCurrentIDs() (resource.Kind, []resource.Identity) { +func (t *tracker) getSelectedOrCurrentIDs() (resource.Kind, []resource.ID) { if len(t.selections) == 0 { - id, ok := t.cursorNode.ID().(resource.ID) + id, ok := t.cursorNode.ID().(resource.MonotonicID) if !ok { // TODO: consider returning error return -1, nil } - return id.Kind, []resource.Identity{id} + return id.Kind, []resource.ID{id} } return *t.selector.kind, maps.Keys(t.selections) } diff --git a/internal/tui/explorer/tree.go b/internal/tui/explorer/tree.go index 55f10e5..da3e657 100644 --- a/internal/tui/explorer/tree.go +++ b/internal/tui/explorer/tree.go @@ -44,14 +44,14 @@ func (b *treeBuilder) newTree(filter string) (*tree, string) { modules := b.moduleService.List() workspaces := b.workspaceService.List(workspace.ListOptions{}) // Create set of current workspaces for assignment below. - currentWorkspaces := make(map[resource.Identity]bool) + currentWorkspaces := make(map[resource.ID]bool) for _, mod := range modules { if mod.CurrentWorkspaceID != nil { currentWorkspaces[mod.CurrentWorkspaceID] = true } } // Arrange workspaces by module, for attachment to modules in tree below. - workspaceNodes := make(map[resource.Identity][]workspaceNode, len(modules)) + workspaceNodes := make(map[resource.ID][]workspaceNode, len(modules)) for _, ws := range workspaces { wsNode := workspaceNode{ id: ws.ID, diff --git a/internal/tui/explorer/tree_test.go b/internal/tui/explorer/tree_test.go index fbcf1fe..c842770 100644 --- a/internal/tui/explorer/tree_test.go +++ b/internal/tui/explorer/tree_test.go @@ -12,29 +12,29 @@ import ( var ( mod1 = &module.Module{ - ID: resource.NewID(resource.Module), + ID: resource.NewMonotonicID(resource.Module), Path: "a", } mod2 = &module.Module{ - ID: resource.NewID(resource.Module), + ID: resource.NewMonotonicID(resource.Module), Path: "a/b", } mod3 = &module.Module{ - ID: resource.NewID(resource.Module), + ID: resource.NewMonotonicID(resource.Module), Path: "a/b/c", } ws1 = &workspace.Workspace{ - ID: resource.NewID(resource.Workspace), + ID: resource.NewMonotonicID(resource.Workspace), ModuleID: mod1.ID, Name: "ws1", } ws2 = &workspace.Workspace{ - ID: resource.NewID(resource.Workspace), + ID: resource.NewMonotonicID(resource.Workspace), ModuleID: mod2.ID, Name: "ws2", } ws3 = &workspace.Workspace{ - ID: resource.NewID(resource.Workspace), + ID: resource.NewMonotonicID(resource.Workspace), ModuleID: mod3.ID, Name: "ws3", } diff --git a/internal/tui/helpers.go b/internal/tui/helpers.go index a864668..8f85d21 100644 --- a/internal/tui/helpers.go +++ b/internal/tui/helpers.go @@ -46,7 +46,7 @@ func (h *Helpers) ModuleCurrentWorkspace(mod *module.Module) *workspace.Workspac return ws } -func (h *Helpers) CurrentWorkspaceName(workspaceID resource.Identity) string { +func (h *Helpers) CurrentWorkspaceName(workspaceID resource.ID) string { if workspaceID == nil { return "-" } @@ -117,7 +117,7 @@ func (h *Helpers) TaskModule(t *task.Task) *module.Module { if moduleID == nil { return nil } - mod, err := h.Modules.Get(*moduleID) + mod, err := h.Modules.Get(moduleID) if err != nil { return nil } @@ -144,7 +144,7 @@ func (h *Helpers) TaskWorkspace(t *task.Task) *workspace.Workspace { if workspaceID == nil { return nil } - ws, err := h.Workspaces.Get(*workspaceID) + ws, err := h.Workspaces.Get(workspaceID) if err != nil { return nil } @@ -304,7 +304,7 @@ func (h *Helpers) GroupReport(group *task.Group, table bool) string { // each invocation. If there is more than one id then a task group is created // and the user sent to the task group's page; otherwise if only id is provided, // the user is sent to the task's page. -func (h *Helpers) CreateTasks(fn task.SpecFunc, ids ...resource.Identity) tea.Cmd { +func (h *Helpers) CreateTasks(fn task.SpecFunc, ids ...resource.ID) tea.Cmd { return func() tea.Msg { switch len(ids) { case 0: @@ -360,10 +360,10 @@ func (h *Helpers) createTaskGroup(specs ...task.Spec) tea.Msg { if err != nil { return ReportError(fmt.Errorf("creating task group: %w", err)) } - return NewNavigationMsg(TaskGroupKind, WithParent(group.ID)) + return NewNavigationMsg(TaskGroupKind, WithParent(group.MonotonicID)) } -func (h *Helpers) Move(workspaceID resource.Identity, from state.ResourceAddress) tea.Cmd { +func (h *Helpers) Move(workspaceID resource.ID, from state.ResourceAddress) tea.Cmd { return CmdHandler(PromptMsg{ Prompt: "Enter destination address: ", InitialValue: string(from), @@ -371,7 +371,7 @@ func (h *Helpers) Move(workspaceID resource.Identity, from state.ResourceAddress if v == "" { return nil } - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return h.States.Move(workspaceID, from, state.ResourceAddress(v)) } return h.CreateTasks(fn, workspaceID) diff --git a/internal/tui/logs/list.go b/internal/tui/logs/list.go index 66a885b..11cf2e2 100644 --- a/internal/tui/logs/list.go +++ b/internal/tui/logs/list.go @@ -37,7 +37,7 @@ type ListMaker struct { Helpers *tui.Helpers } -func (m *ListMaker) Make(_ resource.Identity, width, height int) (tui.ChildModel, error) { +func (m *ListMaker) Make(_ resource.ID, width, height int) (tui.ChildModel, error) { columns := []table.Column{ timeColumn, levelColumn, diff --git a/internal/tui/logs/model.go b/internal/tui/logs/model.go index 5779110..58ec2ef 100644 --- a/internal/tui/logs/model.go +++ b/internal/tui/logs/model.go @@ -33,7 +33,7 @@ type Maker struct { Helpers *tui.Helpers } -func (mm *Maker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { +func (mm *Maker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { msg, err := mm.Logger.Get(id) if err != nil { return nil, err @@ -53,17 +53,17 @@ func (mm *Maker) Make(id resource.Identity, width, height int) (tui.ChildModel, { Key: timeAttrKey, Value: msg.Time.Format(timeFormat), - ID: resource.NewID(resource.LogAttr), + ID: resource.NewMonotonicID(resource.LogAttr), }, { Key: messageAttrKey, Value: msg.Message, - ID: resource.NewID(resource.LogAttr), + ID: resource.NewMonotonicID(resource.LogAttr), }, { Key: levelAttrKey, Value: coloredLogLevel(msg.Level), - ID: resource.NewID(resource.LogAttr), + ID: resource.NewMonotonicID(resource.LogAttr), }, } items = append(items, msg.Attributes...) diff --git a/internal/tui/messages.go b/internal/tui/messages.go index bab635a..ac4b063 100644 --- a/internal/tui/messages.go +++ b/internal/tui/messages.go @@ -22,7 +22,7 @@ func NewNavigationMsg(kind Kind, opts ...NavigateOption) NavigationMsg { type NavigateOption func(msg *NavigationMsg) -func WithParent(parent resource.Identity) NavigateOption { +func WithParent(parent resource.ID) NavigateOption { return func(msg *NavigationMsg) { msg.Page.ID = parent } diff --git a/internal/tui/model.go b/internal/tui/model.go index 8c4fe7d..77afe96 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -14,7 +14,7 @@ type ChildModel interface { // Maker makes new models type Maker interface { - Make(id resource.Identity, width, height int) (ChildModel, error) + Make(id resource.ID, width, height int) (ChildModel, error) } // Page identifies an instance of a model @@ -23,7 +23,7 @@ type Page struct { Kind Kind // ID of resource for a model. If the model does not have a single resource // but is say a listing of resources, then this is nil. - ID resource.Identity + ID resource.ID } // ModelHelpBindings is implemented by models that surface further help bindings diff --git a/internal/tui/pane_manager.go b/internal/tui/pane_manager.go index f0d8bcf..ec8e720 100644 --- a/internal/tui/pane_manager.go +++ b/internal/tui/pane_manager.go @@ -53,7 +53,7 @@ type pane struct { } type tablePane interface { - PreviewCurrentRow() (Kind, resource.Identity, bool) + PreviewCurrentRow() (Kind, resource.ID, bool) } // NewPaneManager constructs the pane manager with at least the explorer, which diff --git a/internal/tui/table/table.go b/internal/tui/table/table.go index 9f9c9ff..ddacb1c 100644 --- a/internal/tui/table/table.go +++ b/internal/tui/table/table.go @@ -32,19 +32,19 @@ type Model[V resource.Identifiable] struct { cols []Column rows []V rowRenderer RowRenderer[V] - rendered map[resource.Identity]RenderedRow + rendered map[resource.ID]RenderedRow border lipgloss.Border borderColor lipgloss.TerminalColor currentRowIndex int - currentRowID resource.Identity + currentRowID resource.ID // items are the unfiltered set of items available to the table. - items map[resource.Identity]V + items map[resource.ID]V sortFunc SortFunc[V] - selected map[resource.Identity]V + selected map[resource.ID]V selectable bool filter textinput.Model @@ -89,9 +89,9 @@ func New[V resource.Identifiable](cols []Column, fn RowRenderer[V], width, heigh m := Model[V]{ rowRenderer: fn, - items: make(map[resource.Identity]V), - rendered: make(map[resource.Identity]RenderedRow), - selected: make(map[resource.Identity]V), + items: make(map[resource.ID]V), + rendered: make(map[resource.ID]RenderedRow), + selected: make(map[resource.ID]V), selectable: true, filter: filter, border: lipgloss.NormalBorder(), @@ -248,12 +248,12 @@ func (m Model[V]) Update(msg tea.Msg) (Model[V], tea.Cmd) { } // PreviewCurrentRow -func (m *Model[V]) PreviewCurrentRow() (tui.Kind, resource.Identity, bool) { +func (m *Model[V]) PreviewCurrentRow() (tui.Kind, resource.ID, bool) { if _, ok := m.CurrentRow(); !ok { - return 0, resource.ID{}, false + return 0, resource.MonotonicID{}, false } if m.previewKind == nil { - return 0, resource.ID{}, false + return 0, resource.MonotonicID{}, false } return *m.previewKind, m.currentRowID, true } @@ -360,7 +360,7 @@ func (m *Model[V]) ToggleSelection() { // ToggleSelectionByID toggles the selection of the row with the given ID. If // the ID does not exist no action is taken. -func (m *Model[V]) ToggleSelectionByID(id resource.Identity) { +func (m *Model[V]) ToggleSelectionByID(id resource.ID) { if !m.selectable { return } @@ -390,7 +390,7 @@ func (m *Model[V]) DeselectAll() { if !m.selectable { return } - m.selected = make(map[resource.Identity]V) + m.selected = make(map[resource.ID]V) } // SelectRange selects a range of rows. If the current row is *below* a selected @@ -435,8 +435,8 @@ func (m *Model[V]) SelectRange() { // SetItems overwrites all existing items in the table with items. func (m *Model[V]) SetItems(items ...V) { - m.items = make(map[resource.Identity]V) - m.rendered = make(map[resource.Identity]RenderedRow) + m.items = make(map[resource.ID]V) + m.rendered = make(map[resource.ID]RenderedRow) m.AddItems(items...) } @@ -476,7 +476,7 @@ func (m *Model[V]) removeItem(item V) { } func (m *Model[V]) setRows(items ...V) { - selected := make(map[resource.Identity]V) + selected := make(map[resource.ID]V) m.rows = make([]V, 0, len(items)) for _, item := range items { if m.filterVisible() && !m.matchFilter(item) { diff --git a/internal/tui/table/table_test.go b/internal/tui/table/table_test.go index 98af065..cd97880 100644 --- a/internal/tui/table/table_test.go +++ b/internal/tui/table/table_test.go @@ -11,16 +11,16 @@ import ( ) var ( - resource0 = testResource{n: 0, ID: resource.NewID(resource.Workspace)} - resource1 = testResource{n: 1, ID: resource.NewID(resource.Workspace)} - resource2 = testResource{n: 2, ID: resource.NewID(resource.Workspace)} - resource3 = testResource{n: 3, ID: resource.NewID(resource.Workspace)} - resource4 = testResource{n: 4, ID: resource.NewID(resource.Workspace)} - resource5 = testResource{n: 5, ID: resource.NewID(resource.Workspace)} + resource0 = testResource{n: 0, MonotonicID: resource.NewMonotonicID(resource.Workspace)} + resource1 = testResource{n: 1, MonotonicID: resource.NewMonotonicID(resource.Workspace)} + resource2 = testResource{n: 2, MonotonicID: resource.NewMonotonicID(resource.Workspace)} + resource3 = testResource{n: 3, MonotonicID: resource.NewMonotonicID(resource.Workspace)} + resource4 = testResource{n: 4, MonotonicID: resource.NewMonotonicID(resource.Workspace)} + resource5 = testResource{n: 5, MonotonicID: resource.NewMonotonicID(resource.Workspace)} ) type testResource struct { - resource.ID + resource.MonotonicID n int } @@ -64,54 +64,54 @@ func TestTable_ToggleSelection(t *testing.T) { tbl.ToggleSelection() assert.Len(t, tbl.selected, 1) - assert.Equal(t, resource0, tbl.selected[resource0.ID]) + assert.Equal(t, resource0, tbl.selected[resource0.MonotonicID]) } func TestTable_SelectRange(t *testing.T) { tests := []struct { name string - selected []resource.Identity + selected []resource.ID cursor int - want []resource.Identity + want []resource.ID }{ { name: "select no range when nothing is selected, and cursor is on first row", - selected: []resource.Identity{}, - want: []resource.Identity{}, + selected: []resource.ID{}, + want: []resource.ID{}, }, { name: "select no range when nothing is selected, and cursor is on last row", - selected: []resource.Identity{}, - want: []resource.Identity{}, + selected: []resource.ID{}, + want: []resource.ID{}, }, { name: "select no range when cursor is on the only selected row", - selected: []resource.Identity{resource0.ID}, - want: []resource.Identity{resource0.ID}, + selected: []resource.ID{resource0.MonotonicID}, + want: []resource.ID{resource0.MonotonicID}, }, { name: "select all rows between selected top row and cursor on last row", - selected: []resource.Identity{resource0.ID}, // first row - cursor: 5, // last row - want: []resource.Identity{resource0.ID, resource1.ID, resource2.ID, resource3.ID, resource4.ID, resource5.ID}, + selected: []resource.ID{resource0.MonotonicID}, // first row + cursor: 5, // last row + want: []resource.ID{resource0.MonotonicID, resource1.MonotonicID, resource2.MonotonicID, resource3.MonotonicID, resource4.MonotonicID, resource5.MonotonicID}, }, { name: "select rows between selected top row and cursor in third row", - selected: []resource.Identity{resource0.ID}, // first row - cursor: 2, // third row - want: []resource.Identity{resource0.ID, resource1.ID, resource2.ID}, + selected: []resource.ID{resource0.MonotonicID}, // first row + cursor: 2, // third row + want: []resource.ID{resource0.MonotonicID, resource1.MonotonicID, resource2.MonotonicID}, }, { name: "select rows between selected top row and cursor in third row, ignoring selected last row", - selected: []resource.Identity{resource0.ID, resource5.ID}, // first and last row - cursor: 2, // third row - want: []resource.Identity{resource0.ID, resource1.ID, resource2.ID, resource5.ID}, + selected: []resource.ID{resource0.MonotonicID, resource5.MonotonicID}, // first and last row + cursor: 2, // third row + want: []resource.ID{resource0.MonotonicID, resource1.MonotonicID, resource2.MonotonicID, resource5.MonotonicID}, }, { name: "select rows between cursor in third row and selected last row", - selected: []resource.Identity{resource5.ID}, // last row - cursor: 2, // third row - want: []resource.Identity{resource2.ID, resource3.ID, resource4.ID, resource5.ID}, + selected: []resource.ID{resource5.MonotonicID}, // last row + cursor: 2, // third row + want: []resource.ID{resource2.MonotonicID, resource3.MonotonicID, resource4.MonotonicID, resource5.MonotonicID}, }, } for _, tt := range tests { @@ -132,8 +132,8 @@ func TestTable_SelectRange(t *testing.T) { } } -func sortStrings(i, j resource.Identity) int { - if i.(resource.ID).String() < j.(resource.ID).String() { +func sortStrings(i, j resource.ID) int { + if i.(resource.MonotonicID).String() < j.(resource.MonotonicID).String() { return -1 } return 1 diff --git a/internal/tui/task/cancel.go b/internal/tui/task/cancel.go index 5ea0e1f..c1267d0 100644 --- a/internal/tui/task/cancel.go +++ b/internal/tui/task/cancel.go @@ -11,7 +11,7 @@ import ( ) // cancel task(s) -func cancel(tasks *task.Service, taskIDs ...resource.Identity) tea.Cmd { +func cancel(tasks *task.Service, taskIDs ...resource.ID) tea.Cmd { var ( prompt string cmd tea.Cmd diff --git a/internal/tui/task/group.go b/internal/tui/task/group.go index 08bd8be..ebb29fa 100644 --- a/internal/tui/task/group.go +++ b/internal/tui/task/group.go @@ -16,7 +16,7 @@ type groupTaskMaker struct { *Maker } -func (m *groupTaskMaker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { +func (m *groupTaskMaker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { return m.make(id, width, height, false) } @@ -37,7 +37,7 @@ func NewGroupMaker(tasks *task.Service, plans *plan.Service, taskMaker *Maker, h } } -func (mm *GroupMaker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { +func (mm *GroupMaker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { group, err := mm.taskListMaker.Tasks.GetGroup(id) if err != nil { return nil, err diff --git a/internal/tui/task/group_list.go b/internal/tui/task/group_list.go index 5316e07..fd160a1 100644 --- a/internal/tui/task/group_list.go +++ b/internal/tui/task/group_list.go @@ -29,7 +29,7 @@ type GroupListMaker struct { Helpers *tui.Helpers } -func (m *GroupListMaker) Make(_ resource.Identity, width, height int) (tui.ChildModel, error) { +func (m *GroupListMaker) Make(_ resource.ID, width, height int) (tui.ChildModel, error) { columns := []table.Column{ taskGroupID, commandColumn, @@ -40,7 +40,7 @@ func (m *GroupListMaker) Make(_ resource.Identity, width, height int) (tui.Child renderer := func(g *task.Group) table.RenderedRow { row := table.RenderedRow{ commandColumn.Key: g.Command, - taskGroupID.Key: g.ID.String(), + taskGroupID.Key: g.MonotonicID.String(), taskGroupCount.Key: m.Helpers.GroupReport(g, true), ageColumn.Key: tui.Ago(time.Now(), g.Created), } @@ -83,7 +83,7 @@ func (m *groupList) Update(msg tea.Msg) tea.Cmd { switch { case key.Matches(msg, groupListKeys.Enter): if row, ok := m.table.CurrentRow(); ok { - return tui.NavigateTo(tui.TaskGroupKind, tui.WithParent(row.ID)) + return tui.NavigateTo(tui.TaskGroupKind, tui.WithParent(row.MonotonicID)) } } } diff --git a/internal/tui/task/list.go b/internal/tui/task/list.go index 1fe5c70..577e8cd 100644 --- a/internal/tui/task/list.go +++ b/internal/tui/task/list.go @@ -38,7 +38,7 @@ type ListTaskMaker struct { *Maker } -func (m *ListTaskMaker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { +func (m *ListTaskMaker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { return m.make(id, width, height, false) } @@ -61,7 +61,7 @@ type ListMaker struct { Helpers *tui.Helpers } -func (mm *ListMaker) Make(_ resource.Identity, width, height int) (tui.ChildModel, error) { +func (mm *ListMaker) Make(_ resource.ID, width, height int) (tui.ChildModel, error) { columns := []table.Column{ table.ModuleColumn, table.WorkspaceColumn, @@ -124,7 +124,7 @@ func (m *List) Update(msg tea.Msg) tea.Cmd { switch { case key.Matches(msg, keys.Common.Cancel): rows := m.SelectedOrCurrent() - taskIDs := make([]resource.Identity, len(rows)) + taskIDs := make([]resource.ID, len(rows)) for i, row := range rows { taskIDs[i] = row.ID } @@ -179,9 +179,9 @@ func (m List) HelpBindings() []key.Binding { return bindings } -func (m List) allPlans() ([]resource.Identity, error) { +func (m List) allPlans() ([]resource.ID, error) { rows := m.SelectedOrCurrent() - ids := make([]resource.Identity, len(rows)) + ids := make([]resource.ID, len(rows)) for i, row := range rows { if err := plan.IsApplyable(row); err != nil { return nil, fmt.Errorf("at least one task is not applyable: %w", err) @@ -191,31 +191,31 @@ func (m List) allPlans() ([]resource.Identity, error) { return ids, nil } -func (m List) GetModuleIDs() ([]resource.Identity, error) { +func (m List) GetModuleIDs() ([]resource.ID, error) { rows := m.SelectedOrCurrent() - ids := make([]resource.Identity, len(rows)) + ids := make([]resource.ID, len(rows)) for i, row := range rows { if row.ModuleID == nil { return nil, errors.New("valid only on modules") } - ids[i] = *row.ModuleID + ids[i] = row.ModuleID } return ids, nil } -func (m List) GetWorkspaceIDs() ([]resource.Identity, error) { +func (m List) GetWorkspaceIDs() ([]resource.ID, error) { rows := m.SelectedOrCurrent() - ids := make([]resource.Identity, len(rows)) + ids := make([]resource.ID, len(rows)) for i, row := range rows { if row.WorkspaceID != nil { - ids[i] = *row.WorkspaceID + ids[i] = row.WorkspaceID } else if row.ModuleID == nil { return nil, errors.New("valid only on tasks associated with a module or a workspace") } else { // task has a module ID but no workspace ID, so find out if its // module has a current workspace, and if so, use that. Otherwise // return error - mod, err := m.Modules.Get(*row.ModuleID) + mod, err := m.Modules.Get(row.ModuleID) if err != nil { return nil, err } diff --git a/internal/tui/task/model.go b/internal/tui/task/model.go index 080f8a6..981be68 100644 --- a/internal/tui/task/model.go +++ b/internal/tui/task/model.go @@ -32,11 +32,11 @@ type Maker struct { showInfo bool } -func (mm *Maker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { +func (mm *Maker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { return mm.make(id, width, height, true) } -func (mm *Maker) make(id resource.Identity, width, height int, border bool) (tui.ChildModel, error) { +func (mm *Maker) make(id resource.ID, width, height int, border bool) (tui.ChildModel, error) { task, err := mm.Tasks.Get(id) if err != nil { return nil, err @@ -330,29 +330,29 @@ type outputMsg struct { eof bool } -func (m Model) GetModuleIDs() ([]resource.Identity, error) { +func (m Model) GetModuleIDs() ([]resource.ID, error) { if m.task.ModuleID == nil { return nil, errors.New("valid only on modules") } - return []resource.Identity{*m.task.ModuleID}, nil + return []resource.ID{m.task.ModuleID}, nil } -func (m Model) GetWorkspaceIDs() ([]resource.Identity, error) { +func (m Model) GetWorkspaceIDs() ([]resource.ID, error) { if m.task.WorkspaceID != nil { - return []resource.Identity{*m.task.WorkspaceID}, nil + return []resource.ID{m.task.WorkspaceID}, nil } else if m.task.ModuleID == nil { return nil, errors.New("valid only on tasks associated with a module or a workspace") } else { // task has a module ID but no workspace ID, so find out if if // module has a current workspace, and if so, use that. Otherwise // return error - mod, err := m.Modules.Get(*m.task.ModuleID) + mod, err := m.Modules.Get(m.task.ModuleID) if err != nil { return nil, err } if mod.CurrentWorkspaceID == nil { return nil, errors.New("valid only on tasks associated with a module with a current workspace, or a workspace") } - return []resource.Identity{mod.CurrentWorkspaceID}, nil + return []resource.ID{mod.CurrentWorkspaceID}, nil } } diff --git a/internal/tui/top/model.go b/internal/tui/top/model.go index 0292f5b..50ad11e 100644 --- a/internal/tui/top/model.go +++ b/internal/tui/top/model.go @@ -46,7 +46,7 @@ type model struct { tasks *task.Service spinner *spinner.Model spinning bool - lastTaskID *resource.ID + lastTaskID *resource.MonotonicID err error info string } diff --git a/internal/tui/workspace/resource.go b/internal/tui/workspace/resource.go index 9e52f70..ea4fd5a 100644 --- a/internal/tui/workspace/resource.go +++ b/internal/tui/workspace/resource.go @@ -23,7 +23,7 @@ type ResourceMaker struct { disableBorders bool } -func (mm *ResourceMaker) Make(id resource.Identity, width, height int) (tui.ChildModel, error) { +func (mm *ResourceMaker) Make(id resource.ID, width, height int) (tui.ChildModel, error) { stateResource, err := mm.States.GetResource(id) if err != nil { return nil, err @@ -77,19 +77,19 @@ func (m *resourceModel) Update(msg tea.Msg) tea.Cmd { case tea.KeyMsg: switch { case key.Matches(msg, resourcesKeys.Taint): - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.states.Taint(workspaceID, m.resource.Address) } return m.CreateTasks(fn, m.resource.WorkspaceID) case key.Matches(msg, resourcesKeys.Untaint): - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.states.Untaint(workspaceID, m.resource.Address) } return m.CreateTasks(fn, m.resource.WorkspaceID) case key.Matches(msg, resourcesKeys.Move): return m.Move(m.resource.WorkspaceID, m.resource.Address) case key.Matches(msg, keys.Common.Delete): - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.states.Delete(workspaceID, m.resource.Address) } return tui.YesNoPrompt( @@ -103,7 +103,7 @@ func (m *resourceModel) Update(msg tea.Msg) tea.Cmd { case key.Matches(msg, keys.Common.Plan): // Create a targeted plan. createRunOptions.TargetAddrs = []state.ResourceAddress{m.resource.Address} - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.plans.Plan(workspaceID, createRunOptions) } return m.CreateTasks(fn, m.resource.WorkspaceID) diff --git a/internal/tui/workspace/resource_list.go b/internal/tui/workspace/resource_list.go index b8b6a2b..5b4508d 100644 --- a/internal/tui/workspace/resource_list.go +++ b/internal/tui/workspace/resource_list.go @@ -33,7 +33,7 @@ type ResourceListMaker struct { Helpers *tui.Helpers } -func (mm *ResourceListMaker) Make(workspaceID resource.Identity, width, height int) (tui.ChildModel, error) { +func (mm *ResourceListMaker) Make(workspaceID resource.ID, width, height int) (tui.ChildModel, error) { ws, err := mm.Workspaces.Get(workspaceID) if err != nil { return nil, err @@ -101,7 +101,7 @@ func (m *resourceList) Init() tea.Cmd { // reloadedMsg is sent when a state reload has finished. type reloadedMsg struct { - workspaceID resource.ID + workspaceID resource.MonotonicID err error } @@ -124,7 +124,7 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { switch { case key.Matches(msg, resourcesKeys.Enter): if row, ok := m.CurrentRow(); ok { - return tui.NavigateTo(tui.ResourceKind, tui.WithParent(row.ID)) + return tui.NavigateTo(tui.ResourceKind, tui.WithParent(row.MonotonicID)) } case key.Matches(msg, resourcesKeys.Reload): if m.reloading { @@ -151,7 +151,7 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { // no rows; do nothing return nil } - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.states.Delete(workspaceID, addrs...) } return tui.YesNoPrompt( @@ -178,7 +178,7 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { createRunOptions.TargetAddrs = m.selectedOrCurrentAddresses() // NOTE: even if the user hasn't selected any rows, we still proceed // to create a run without targeted resources. - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.plans.Plan(workspaceID, createRunOptions) } return m.CreateTasks(fn, m.workspace.ID) @@ -190,7 +190,7 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { // Create a targeted apply. createRunOptions.TargetAddrs = m.selectedOrCurrentAddresses() resourceIDs := m.SelectedOrCurrent() - fn := func(workspaceID resource.Identity) (task.Spec, error) { + fn := func(workspaceID resource.ID) (task.Spec, error) { return m.plans.Apply(workspaceID, createRunOptions) } return tui.YesNoPrompt( @@ -283,10 +283,10 @@ func (m *resourceList) BorderText() map[tui.BorderPosition]string { } } -func (m *resourceList) GetModuleIDs() ([]resource.Identity, error) { - return []resource.Identity{m.workspace.ModuleID}, nil +func (m *resourceList) GetModuleIDs() ([]resource.ID, error) { + return []resource.ID{m.workspace.ModuleID}, nil } -func (m *resourceList) GetWorkspaceIDs() ([]resource.Identity, error) { - return []resource.Identity{m.workspace.ID}, nil +func (m *resourceList) GetWorkspaceIDs() ([]resource.ID, error) { + return []resource.ID{m.workspace.ID}, nil } diff --git a/internal/tui/workspace/state_func.go b/internal/tui/workspace/state_func.go index 7d2ac84..e7cf005 100644 --- a/internal/tui/workspace/state_func.go +++ b/internal/tui/workspace/state_func.go @@ -7,11 +7,11 @@ import ( "github.com/leg100/pug/internal/task" ) -type stateFunc func(workspaceID resource.Identity, addr state.ResourceAddress) (task.Spec, error) +type stateFunc func(workspaceID resource.ID, addr state.ResourceAddress) (task.Spec, error) func (m resourceList) createStateCommand(fn stateFunc, addrs ...state.ResourceAddress) tea.Cmd { // Make N copies of the workspace ID where N is the number of addresses - workspaceIDs := make([]resource.Identity, len(addrs)) + workspaceIDs := make([]resource.ID, len(addrs)) for i := range workspaceIDs { workspaceIDs[i] = m.workspace.ID } @@ -32,7 +32,7 @@ type stateTaskFunc struct { i int } -func (f *stateTaskFunc) createTask(workspaceID resource.Identity) (task.Spec, error) { +func (f *stateTaskFunc) createTask(workspaceID resource.ID) (task.Spec, error) { t, err := f.fn(workspaceID, f.addrs[f.i]) f.i++ return t, err diff --git a/internal/workspace/cost.go b/internal/workspace/cost.go index 96bd8fd..ab8e0d1 100644 --- a/internal/workspace/cost.go +++ b/internal/workspace/cost.go @@ -21,7 +21,7 @@ type costTaskSpecCreator struct { // Cost creates a task that retrieves a breakdown of the costs of the // infrastructure deployed by the workspace. -func (s *costTaskSpecCreator) Cost(workspaceIDs ...resource.Identity) (task.Spec, error) { +func (s *costTaskSpecCreator) Cost(workspaceIDs ...resource.ID) (task.Spec, error) { if len(workspaceIDs) == 0 { return task.Spec{}, errors.New("no workspaces specified") } diff --git a/internal/workspace/reloader.go b/internal/workspace/reloader.go index 1ec9877..8d13cc2 100644 --- a/internal/workspace/reloader.go +++ b/internal/workspace/reloader.go @@ -33,7 +33,7 @@ func (s ReloadSummary) LogValue() slog.Value { ) } -func (r *reloader) createReloadTask(moduleID resource.Identity) error { +func (r *reloader) createReloadTask(moduleID resource.ID) error { spec, err := r.Reload(moduleID) if err != nil { return err @@ -47,13 +47,13 @@ func (r *reloader) createReloadTask(moduleID resource.Identity) error { // workspaces and pruning any workspaces no longer found to exist. // // TODO: separate into Load and Reload -func (r *reloader) Reload(moduleID resource.Identity) (task.Spec, error) { +func (r *reloader) Reload(moduleID resource.ID) (task.Spec, error) { mod, err := r.modules.Get(moduleID) if err != nil { return task.Spec{}, err } return task.Spec{ - ModuleID: &mod.ID, + ModuleID: mod.ID, Path: mod.Path, Execution: task.Execution{ TerraformCommand: []string{"workspace", "list"}, diff --git a/internal/workspace/reloader_test.go b/internal/workspace/reloader_test.go index 9b4d56e..d57da3b 100644 --- a/internal/workspace/reloader_test.go +++ b/internal/workspace/reloader_test.go @@ -51,7 +51,7 @@ func TestWorkspace_resetWorkspaces(t *testing.T) { require.NoError(t, err) // expect staging to be dropped - assert.Equal(t, []resource.Identity{staging.ID}, table.deleted) + assert.Equal(t, []resource.ID{staging.ID}, table.deleted) // expect prod to be added assert.Len(t, table.added, 1) @@ -62,12 +62,12 @@ func TestWorkspace_resetWorkspaces(t *testing.T) { } type fakeModuleService struct { - current resource.Identity + current resource.ID modules } -func (f *fakeModuleService) SetCurrent(moduleID, workspaceID resource.Identity) error { +func (f *fakeModuleService) SetCurrent(moduleID, workspaceID resource.ID) error { f.current = workspaceID return nil } @@ -75,16 +75,16 @@ func (f *fakeModuleService) SetCurrent(moduleID, workspaceID resource.Identity) type fakeWorkspaceTable struct { existing []*Workspace added []*Workspace - deleted []resource.Identity + deleted []resource.ID workspaceTable } -func (f *fakeWorkspaceTable) Add(id resource.Identity, row *Workspace) { +func (f *fakeWorkspaceTable) Add(id resource.ID, row *Workspace) { f.added = append(f.added, row) } -func (f *fakeWorkspaceTable) Delete(id resource.Identity) { +func (f *fakeWorkspaceTable) Delete(id resource.ID) { f.deleted = append(f.deleted, id) } diff --git a/internal/workspace/service.go b/internal/workspace/service.go index 0a39e9f..ec2850c 100644 --- a/internal/workspace/service.go +++ b/internal/workspace/service.go @@ -34,17 +34,17 @@ type ServiceOptions struct { } type workspaceTable interface { - Add(id resource.Identity, row *Workspace) - Update(id resource.Identity, updater func(existing *Workspace) error) (*Workspace, error) - Delete(id resource.Identity) - Get(id resource.Identity) (*Workspace, error) + Add(id resource.ID, row *Workspace) + Update(id resource.ID, updater func(existing *Workspace) error) (*Workspace, error) + Delete(id resource.ID) + Get(id resource.ID) (*Workspace, error) List() []*Workspace } type modules interface { - Get(id resource.Identity) (*module.Module, error) + Get(id resource.ID) (*module.Module, error) GetByPath(path string) (*module.Module, error) - SetCurrent(moduleID, workspaceID resource.Identity) error + SetCurrent(moduleID, workspaceID resource.ID) error Reload() ([]string, []string, error) List() []*module.Module } @@ -101,7 +101,7 @@ func (s *Service) LoadWorkspacesUponInit(sub <-chan resource.Event[*task.Task]) if moduleID == nil { continue } - mod, err := s.modules.Get(*moduleID) + mod, err := s.modules.Get(moduleID) if err != nil { continue } @@ -125,7 +125,7 @@ func (s *Service) Create(path, name string) (task.Spec, error) { return task.Spec{}, err } return task.Spec{ - ModuleID: &mod.ID, + ModuleID: mod.ID, Path: mod.Path, Execution: task.Execution{ TerraformCommand: []string{"workspace", "new"}, @@ -142,7 +142,7 @@ func (s *Service) Create(path, name string) (task.Spec, error) { }, nil } -func (s *Service) Get(workspaceID resource.Identity) (*Workspace, error) { +func (s *Service) Get(workspaceID resource.ID) (*Workspace, error) { return s.table.Get(workspaceID) } @@ -157,7 +157,7 @@ func (s *Service) GetByName(modulePath, name string) (*Workspace, error) { type ListOptions struct { // Filter by ID of workspace's module. - ModuleID resource.Identity + ModuleID resource.ID } func (s *Service) List(opts ListOptions) []*Workspace { @@ -174,7 +174,7 @@ func (s *Service) List(opts ListOptions) []*Workspace { // SelectWorkspace runs the `terraform workspace select ` // command, which sets the current workspace for the module. Once that's // finished it then updates the current workspace in pug itself too. -func (s *Service) SelectWorkspace(workspaceID resource.Identity) error { +func (s *Service) SelectWorkspace(workspaceID resource.ID) error { ws, err := s.table.Get(workspaceID) if err != nil { return err @@ -185,7 +185,7 @@ func (s *Service) SelectWorkspace(workspaceID resource.Identity) error { } // Create task to immediately set workspace as current workspace for module. _, err = s.tasks.Create(task.Spec{ - ModuleID: &mod.ID, + ModuleID: mod.ID, Path: mod.Path, Execution: task.Execution{ TerraformCommand: []string{"workspace", "select"}, @@ -207,7 +207,7 @@ func (s *Service) SelectWorkspace(workspaceID resource.Identity) error { } // Delete a workspace. Asynchronous. -func (s *Service) Delete(workspaceID resource.Identity) (task.Spec, error) { +func (s *Service) Delete(workspaceID resource.ID) (task.Spec, error) { ws, err := s.table.Get(workspaceID) if err != nil { return task.Spec{}, fmt.Errorf("deleting workspace: %w", err) @@ -217,7 +217,7 @@ func (s *Service) Delete(workspaceID resource.Identity) (task.Spec, error) { return task.Spec{}, err } return task.Spec{ - ModuleID: &mod.ID, + ModuleID: mod.ID, Path: mod.Path, Execution: task.Execution{ TerraformCommand: []string{"workspace", "delete"}, diff --git a/internal/workspace/sort.go b/internal/workspace/sort.go index 3d2fee5..b774992 100644 --- a/internal/workspace/sort.go +++ b/internal/workspace/sort.go @@ -6,7 +6,7 @@ import ( ) type moduleGetter interface { - Get(id resource.ID) (*module.Module, error) + Get(id resource.MonotonicID) (*module.Module, error) } // Sort sorts workspaces accordingly: diff --git a/internal/workspace/workspace.go b/internal/workspace/workspace.go index e5574a7..a4813ca 100644 --- a/internal/workspace/workspace.go +++ b/internal/workspace/workspace.go @@ -13,10 +13,9 @@ import ( ) type Workspace struct { - resource.ID - + ID resource.MonotonicID Name string - ModuleID resource.ID + ModuleID resource.MonotonicID ModulePath string Cost *float64 } @@ -26,7 +25,7 @@ func New(mod *module.Module, name string) (*Workspace, error) { return nil, fmt.Errorf("invalid workspace name: %s", name) } return &Workspace{ - ID: resource.NewID(resource.Workspace), + ID: resource.NewMonotonicID(resource.Workspace), Name: name, ModuleID: mod.ID, ModulePath: mod.Path, From 6e6df3ae0dfdc6c5353b5ede799d4e205407611c Mon Sep 17 00:00:00 2001 From: Louis Garman Date: Wed, 15 Jan 2025 09:22:38 +0000 Subject: [PATCH 4/5] wip --- internal/resource/id.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/resource/id.go b/internal/resource/id.go index d4ba97a..183ac97 100644 --- a/internal/resource/id.go +++ b/internal/resource/id.go @@ -3,6 +3,7 @@ package resource // ID uniquely identifies a Pug resource. type ID any +// Identifiable is a Pug resource with an identity. type Identifiable interface { GetID() ID } From fdbafa179a5dc40ec1145811e6e7d0eb423a0fa5 Mon Sep 17 00:00:00 2001 From: Louis Garman Date: Wed, 15 Jan 2025 09:34:48 +0000 Subject: [PATCH 5/5] wip --- internal/resource/id_monotonic.go | 10 ++-- internal/state/resource.go | 15 +++--- internal/state/service.go | 2 +- internal/state/state.go | 7 +-- internal/task/group.go | 10 ++-- internal/task/service.go | 2 +- internal/tui/helpers.go | 2 +- internal/tui/table/table_test.go | 69 +++++++++++++------------ internal/tui/task/group_list.go | 4 +- internal/tui/workspace/resource_list.go | 2 +- 10 files changed, 61 insertions(+), 62 deletions(-) diff --git a/internal/resource/id_monotonic.go b/internal/resource/id_monotonic.go index 20f7f5c..d84d896 100644 --- a/internal/resource/id_monotonic.go +++ b/internal/resource/id_monotonic.go @@ -11,7 +11,8 @@ var ( mu sync.Mutex ) -// MonotonicID is a unique identifier for a pug resource. +// MonotonicID is an identifier based on an ever-increasing serial number, and a +// kind to differentiate it from other kinds of identifiers. type MonotonicID struct { Serial uint Kind Kind @@ -30,12 +31,7 @@ func NewMonotonicID(kind Kind) MonotonicID { } } -// String provides a human readable description. +// String provides a human readable representation of the identifier. func (id MonotonicID) String() string { return fmt.Sprintf("#%d", id.Serial) } - -// GetID implements Identifiable -func (id MonotonicID) GetID() ID { - return id -} diff --git a/internal/state/resource.go b/internal/state/resource.go index f4052c5..6a7d62f 100644 --- a/internal/state/resource.go +++ b/internal/state/resource.go @@ -8,21 +8,16 @@ import ( // Resource is a pug state resource. type Resource struct { - resource.MonotonicID - + ID resource.MonotonicID WorkspaceID resource.ID Address ResourceAddress Attributes map[string]any Tainted bool } -func (r *Resource) String() string { - return string(r.Address) -} - func newResource(workspaceID resource.ID, addr ResourceAddress, attrs json.RawMessage) (*Resource, error) { res := &Resource{ - MonotonicID: resource.NewMonotonicID(resource.StateResource), + ID: resource.NewMonotonicID(resource.StateResource), WorkspaceID: workspaceID, Address: addr, } @@ -32,4 +27,10 @@ func newResource(workspaceID resource.ID, addr ResourceAddress, attrs json.RawMe return res, nil } +func (r *Resource) String() string { + return string(r.Address) +} + +func (r *Resource) GetID() resource.ID { return r.ID } + type ResourceAddress string diff --git a/internal/state/service.go b/internal/state/service.go index fb00e7c..1acc59d 100644 --- a/internal/state/service.go +++ b/internal/state/service.go @@ -54,7 +54,7 @@ func (s *Service) Get(workspaceID resource.ID) (*State, error) { func (s *Service) GetResource(resourceID resource.ID) (*Resource, error) { for _, state := range s.cache.List() { for _, res := range state.Resources { - if res.MonotonicID == resourceID { + if res.ID == resourceID { return res, nil } } diff --git a/internal/state/state.go b/internal/state/state.go index 5fbc520..fc19592 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -12,8 +12,7 @@ import ( ) type State struct { - resource.MonotonicID - + ID resource.MonotonicID WorkspaceID resource.ID Resources map[ResourceAddress]*Resource Serial int64 @@ -24,7 +23,7 @@ type State struct { func newState(workspaceID resource.ID, r io.Reader) (*State, error) { // Default to a serial of -1 to indicate that there is no state yet. state := &State{ - MonotonicID: resource.NewMonotonicID(resource.State), + ID: resource.NewMonotonicID(resource.State), WorkspaceID: workspaceID, Serial: -1, } @@ -88,6 +87,8 @@ func newState(workspaceID resource.ID, r io.Reader) (*State, error) { return state, nil } +func (s *State) GetID() resource.ID { return s.ID } + func (s *State) LogValue() slog.Value { return slog.GroupValue( slog.Int("resources", len(s.Resources)), diff --git a/internal/task/group.go b/internal/task/group.go index ff2a85b..d06a83c 100644 --- a/internal/task/group.go +++ b/internal/task/group.go @@ -10,8 +10,7 @@ import ( ) type Group struct { - resource.MonotonicID - + ID resource.MonotonicID Created time.Time Command string Tasks []*Task @@ -23,8 +22,8 @@ func newGroup(service *Service, specs ...Spec) (*Group, error) { return nil, errors.New("no specs provided") } g := &Group{ - MonotonicID: resource.NewMonotonicID(resource.TaskGroup), - Created: time.Now(), + ID: resource.NewMonotonicID(resource.TaskGroup), + Created: time.Now(), } // Validate specifications. There are some settings that are incompatible // with one another within a task group. @@ -84,7 +83,8 @@ func newGroup(service *Service, specs ...Spec) (*Group, error) { return g, nil } -func (g *Group) String() string { return g.Command } +func (g *Group) String() string { return g.Command } +func (g *Group) GetID() resource.ID { return g.ID } func (g *Group) IncludesTask(taskID resource.MonotonicID) bool { return slices.ContainsFunc(g.Tasks, func(tgt *Task) bool { diff --git a/internal/task/service.go b/internal/task/service.go index d15eab6..1304e6f 100644 --- a/internal/task/service.go +++ b/internal/task/service.go @@ -109,7 +109,7 @@ func (s *Service) CreateGroup(specs ...Spec) (*Group, error) { // AddGroup adds a task group to the DB. func (s *Service) AddGroup(group *Group) { - s.groups.Add(group.MonotonicID, group) + s.groups.Add(group.ID, group) } // Enqueue moves the task onto the global queue for processing. diff --git a/internal/tui/helpers.go b/internal/tui/helpers.go index 8f85d21..bb56d3a 100644 --- a/internal/tui/helpers.go +++ b/internal/tui/helpers.go @@ -360,7 +360,7 @@ func (h *Helpers) createTaskGroup(specs ...task.Spec) tea.Msg { if err != nil { return ReportError(fmt.Errorf("creating task group: %w", err)) } - return NewNavigationMsg(TaskGroupKind, WithParent(group.MonotonicID)) + return NewNavigationMsg(TaskGroupKind, WithParent(group.ID)) } func (h *Helpers) Move(workspaceID resource.ID, from state.ResourceAddress) tea.Cmd { diff --git a/internal/tui/table/table_test.go b/internal/tui/table/table_test.go index cd97880..7528366 100644 --- a/internal/tui/table/table_test.go +++ b/internal/tui/table/table_test.go @@ -11,27 +11,28 @@ import ( ) var ( - resource0 = testResource{n: 0, MonotonicID: resource.NewMonotonicID(resource.Workspace)} - resource1 = testResource{n: 1, MonotonicID: resource.NewMonotonicID(resource.Workspace)} - resource2 = testResource{n: 2, MonotonicID: resource.NewMonotonicID(resource.Workspace)} - resource3 = testResource{n: 3, MonotonicID: resource.NewMonotonicID(resource.Workspace)} - resource4 = testResource{n: 4, MonotonicID: resource.NewMonotonicID(resource.Workspace)} - resource5 = testResource{n: 5, MonotonicID: resource.NewMonotonicID(resource.Workspace)} + resource0 = testResource{n: 0, ID: resource.NewMonotonicID(resource.Workspace)} + resource1 = testResource{n: 1, ID: resource.NewMonotonicID(resource.Workspace)} + resource2 = testResource{n: 2, ID: resource.NewMonotonicID(resource.Workspace)} + resource3 = testResource{n: 3, ID: resource.NewMonotonicID(resource.Workspace)} + resource4 = testResource{n: 4, ID: resource.NewMonotonicID(resource.Workspace)} + resource5 = testResource{n: 5, ID: resource.NewMonotonicID(resource.Workspace)} ) type testResource struct { - resource.MonotonicID - - n int + ID resource.MonotonicID + n int } +func (r *testResource) GetID() resource.ID { return r.ID } + // setupTest sets up a table test with several rows. Each row is keyed with an // int, and the row item is an int corresponding to the key, for ease of // testing. The rows are sorted from lowest int to highest int. -func setupTest() Model[testResource] { - renderer := func(v testResource) RenderedRow { return nil } +func setupTest() Model[*testResource] { + renderer := func(v *testResource) RenderedRow { return nil } tbl := New(nil, renderer, 0, 0, - WithSortFunc(func(i, j testResource) int { + WithSortFunc(func(i, j *testResource) int { if i.n < j.n { return -1 } @@ -39,12 +40,12 @@ func setupTest() Model[testResource] { }), ) tbl.SetItems( - resource0, - resource1, - resource2, - resource3, - resource4, - resource5, + &resource0, + &resource1, + &resource2, + &resource3, + &resource4, + &resource5, ) return tbl } @@ -55,7 +56,7 @@ func TestTable_CurrentRow(t *testing.T) { got, ok := tbl.CurrentRow() require.True(t, ok) - assert.Equal(t, resource0, got) + assert.Equal(t, &resource0, got) } func TestTable_ToggleSelection(t *testing.T) { @@ -64,7 +65,7 @@ func TestTable_ToggleSelection(t *testing.T) { tbl.ToggleSelection() assert.Len(t, tbl.selected, 1) - assert.Equal(t, resource0, tbl.selected[resource0.MonotonicID]) + assert.Equal(t, &resource0, tbl.selected[resource0.ID]) } func TestTable_SelectRange(t *testing.T) { @@ -86,32 +87,32 @@ func TestTable_SelectRange(t *testing.T) { }, { name: "select no range when cursor is on the only selected row", - selected: []resource.ID{resource0.MonotonicID}, - want: []resource.ID{resource0.MonotonicID}, + selected: []resource.ID{resource0.ID}, + want: []resource.ID{resource0.ID}, }, { name: "select all rows between selected top row and cursor on last row", - selected: []resource.ID{resource0.MonotonicID}, // first row - cursor: 5, // last row - want: []resource.ID{resource0.MonotonicID, resource1.MonotonicID, resource2.MonotonicID, resource3.MonotonicID, resource4.MonotonicID, resource5.MonotonicID}, + selected: []resource.ID{resource0.ID}, // first row + cursor: 5, // last row + want: []resource.ID{resource0.ID, resource1.ID, resource2.ID, resource3.ID, resource4.ID, resource5.ID}, }, { name: "select rows between selected top row and cursor in third row", - selected: []resource.ID{resource0.MonotonicID}, // first row - cursor: 2, // third row - want: []resource.ID{resource0.MonotonicID, resource1.MonotonicID, resource2.MonotonicID}, + selected: []resource.ID{resource0.ID}, // first row + cursor: 2, // third row + want: []resource.ID{resource0.ID, resource1.ID, resource2.ID}, }, { name: "select rows between selected top row and cursor in third row, ignoring selected last row", - selected: []resource.ID{resource0.MonotonicID, resource5.MonotonicID}, // first and last row - cursor: 2, // third row - want: []resource.ID{resource0.MonotonicID, resource1.MonotonicID, resource2.MonotonicID, resource5.MonotonicID}, + selected: []resource.ID{resource0.ID, resource5.ID}, // first and last row + cursor: 2, // third row + want: []resource.ID{resource0.ID, resource1.ID, resource2.ID, resource5.ID}, }, { name: "select rows between cursor in third row and selected last row", - selected: []resource.ID{resource5.MonotonicID}, // last row - cursor: 2, // third row - want: []resource.ID{resource2.MonotonicID, resource3.MonotonicID, resource4.MonotonicID, resource5.MonotonicID}, + selected: []resource.ID{resource5.ID}, // last row + cursor: 2, // third row + want: []resource.ID{resource2.ID, resource3.ID, resource4.ID, resource5.ID}, }, } for _, tt := range tests { diff --git a/internal/tui/task/group_list.go b/internal/tui/task/group_list.go index fd160a1..cdc9ecb 100644 --- a/internal/tui/task/group_list.go +++ b/internal/tui/task/group_list.go @@ -40,7 +40,7 @@ func (m *GroupListMaker) Make(_ resource.ID, width, height int) (tui.ChildModel, renderer := func(g *task.Group) table.RenderedRow { row := table.RenderedRow{ commandColumn.Key: g.Command, - taskGroupID.Key: g.MonotonicID.String(), + taskGroupID.Key: g.ID.String(), taskGroupCount.Key: m.Helpers.GroupReport(g, true), ageColumn.Key: tui.Ago(time.Now(), g.Created), } @@ -83,7 +83,7 @@ func (m *groupList) Update(msg tea.Msg) tea.Cmd { switch { case key.Matches(msg, groupListKeys.Enter): if row, ok := m.table.CurrentRow(); ok { - return tui.NavigateTo(tui.TaskGroupKind, tui.WithParent(row.MonotonicID)) + return tui.NavigateTo(tui.TaskGroupKind, tui.WithParent(row.ID)) } } } diff --git a/internal/tui/workspace/resource_list.go b/internal/tui/workspace/resource_list.go index 5b4508d..312ea08 100644 --- a/internal/tui/workspace/resource_list.go +++ b/internal/tui/workspace/resource_list.go @@ -124,7 +124,7 @@ func (m *resourceList) Update(msg tea.Msg) tea.Cmd { switch { case key.Matches(msg, resourcesKeys.Enter): if row, ok := m.CurrentRow(); ok { - return tui.NavigateTo(tui.ResourceKind, tui.WithParent(row.MonotonicID)) + return tui.NavigateTo(tui.ResourceKind, tui.WithParent(row.ID)) } case key.Matches(msg, resourcesKeys.Reload): if m.reloading {