diff --git a/internal/plan/service.go b/internal/plan/service.go index 831bd6c..eb1da04 100644 --- a/internal/plan/service.go +++ b/internal/plan/service.go @@ -77,12 +77,15 @@ func (s *Service) ReloadAfterApply(sub <-chan resource.Event[*task.Task]) { if !IsApplyTask(event.Payload) { continue } - ws := event.Payload.Workspace() - if _, err := s.states.CreateReloadTask(ws.GetID()); err != nil { - s.logger.Error("reloading state after apply", "error", err, "workspace", ws) + workspaceID := event.Payload.WorkspaceID + if workspaceID == nil { continue } - s.logger.Debug("reloading state after apply", "workspace", ws) + 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) } } } diff --git a/internal/resource/common.go b/internal/resource/common.go index 3a3fd2b..7d2ddd6 100644 --- a/internal/resource/common.go +++ b/internal/resource/common.go @@ -16,64 +16,8 @@ func New(kind Kind, parent Resource) Common { } } -func (r Common) GetParent() Resource { - return r.Parent -} - -func (r Common) HasAncestor(id ID) bool { - // Every entity is considered an ancestor of the nil ID (equivalent to the - // ID of the "global" entity). - if id == GlobalID { - return true - } - if r.Parent == nil { - // Parent has no parents, so go no further - return false - } - if r.Parent.GetID() == id { - return true - } - // Check parents of parent - return r.Parent.HasAncestor(id) -} - -// Ancestors provides a list of successive parents, starting with the direct parents. -func (r Common) Ancestors() (ancestors []Resource) { - if r.Parent == nil { - return - } - ancestors = append(ancestors, r.Parent) - return append(ancestors, r.Parent.Ancestors()...) -} - -func (r Common) getCurrentOrAncestorKind(k Kind) Resource { - if r.GetKind() == k { - return r - } - for _, parent := range r.Ancestors() { - if parent.GetKind() == k { - return parent - } - } - return nil -} - -func (r Common) Module() Resource { - return r.getCurrentOrAncestorKind(Module) -} - -func (r Common) Workspace() Resource { - return r.getCurrentOrAncestorKind(Workspace) -} - func (r Common) Dependencies() []ID { - // Direct dependencies - deps := r.dependencies - // Indirect dependencies - for _, parent := range r.Ancestors() { - deps = append(deps, parent.Dependencies()...) - } - return deps + return r.dependencies } func (r Common) WithDependencies(deps ...ID) Common { diff --git a/internal/resource/resource.go b/internal/resource/resource.go index c2f9b6f..9e1fdec 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -10,23 +10,9 @@ type Resource interface { GetID() ID // GetKind retrieves the kind of resource. GetKind() Kind - // GetParent retrieves the resource's parent, the resource from which the - // resource was spawned. - GetParent() Resource - // HasAncestor determines whether the resource has an ancestor with the - // given ID. - HasAncestor(ID) bool - // Ancestors retrieves a list of the resource's ancestors, nearest first. - Ancestors() []Resource // String is a human-readable identifier for the resource. Not necessarily // unique across pug. String() string - // Module retrieves the resource's module. Returns nil if the resource does - // not have a module ancestor. - Module() Resource - // Workspace retrieves the resource's workspcae. Returns nil if the resource does - // not have a workspace ancestor. - Workspace() Resource // Dependencies returns both direct dependencies and indirect dependencies that // ancestor resources have. Dependencies() []ID diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go deleted file mode 100644 index e3cea04..0000000 --- a/internal/resource/resource_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package resource - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestResource(t *testing.T) { - mod := New(Module, GlobalResource) - ws := New(Workspace, mod) - task := New(Task, ws) - - t.Run("has ancestor", func(t *testing.T) { - assert.True(t, task.HasAncestor(mod.ID)) - assert.False(t, mod.HasAncestor(task.ID)) - }) - - t.Run("get ancestor of specific kind", func(t *testing.T) { - assert.Equal(t, mod, task.Module()) - assert.Equal(t, ws, task.Workspace()) - }) -} diff --git a/internal/state/reloader.go b/internal/state/reloader.go index 5630092..b89b352 100644 --- a/internal/state/reloader.go +++ b/internal/state/reloader.go @@ -18,7 +18,7 @@ func (r *reloader) Reload(workspaceID resource.ID) (task.Spec, error) { Command: []string{"state", "pull"}, JSON: true, BeforeExited: func(t *task.Task) (task.Summary, error) { - state, err := newState(t.Workspace(), t.NewReader(false)) + state, err := newState(workspaceID, t.NewReader(false)) if err != nil { return nil, fmt.Errorf("constructing pug state: %w", err) } diff --git a/internal/state/resource.go b/internal/state/resource.go index f32641b..a92ace2 100644 --- a/internal/state/resource.go +++ b/internal/state/resource.go @@ -10,19 +10,21 @@ import ( type Resource struct { resource.Common - Address ResourceAddress - Attributes map[string]any - Tainted bool + WorkspaceID resource.ID + Address ResourceAddress + Attributes map[string]any + Tainted bool } func (r *Resource) String() string { return string(r.Address) } -func newResource(state resource.Resource, addr ResourceAddress, attrs json.RawMessage) (*Resource, error) { +func newResource(workspaceID resource.ID, addr ResourceAddress, attrs json.RawMessage) (*Resource, error) { res := &Resource{ - Common: resource.New(resource.StateResource, state), - Address: addr, + Common: resource.New(resource.StateResource, resource.GlobalResource), + WorkspaceID: workspaceID, + Address: addr, } if err := json.Unmarshal(attrs, &res.Attributes); err != nil { return nil, err diff --git a/internal/state/state.go b/internal/state/state.go index 2a3b4ec..1a8e709 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -21,11 +21,11 @@ type State struct { Lineage string } -func newState(ws resource.Resource, 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{ - Common: resource.New(resource.State, ws), - WorkspaceID: ws.GetID(), + Common: resource.New(resource.State, resource.GlobalResource), + WorkspaceID: workspaceID, Serial: -1, } @@ -74,7 +74,7 @@ func newState(ws resource.Resource, r io.Reader) (*State, error) { addr := ResourceAddress(b.String()) var err error - m[addr], err = newResource(state, addr, instance.Attributes) + m[addr], err = newResource(workspaceID, addr, instance.Attributes) if err != nil { return nil, fmt.Errorf("decoding resource %s: %w", addr, err) } diff --git a/internal/tui/helpers.go b/internal/tui/helpers.go index 9595708..7e11934 100644 --- a/internal/tui/helpers.go +++ b/internal/tui/helpers.go @@ -33,13 +33,6 @@ type Helpers struct { Logger logging.Interface } -func (h *Helpers) WorkspaceName(res resource.Resource) string { - if ws := res.Workspace(); ws != nil { - return ws.String() - } - return "" -} - func (h *Helpers) ModuleCurrentWorkspace(mod *module.Module) *workspace.Workspace { if mod.CurrentWorkspaceID == nil { return nil @@ -52,18 +45,6 @@ func (h *Helpers) ModuleCurrentWorkspace(mod *module.Module) *workspace.Workspac return ws } -func (h *Helpers) Module(res resource.Resource) *module.Module { - if res.Module() == nil { - return nil - } - mod, ok := res.Module().(*module.Module) - if !ok { - h.Logger.Error("unable to unwrap module from resource interface", "resource", res) - return nil - } - return mod -} - func (h *Helpers) CurrentWorkspaceName(workspaceID *resource.ID) string { if workspaceID == nil { return "-" @@ -149,29 +130,42 @@ func (h *Helpers) TaskModulePath(t *task.Task) string { return "" } -// TaskWorkspace retrieves either the task's workspace if it belongs to a -// workspace, or if it belongs to a module, then it retrieves the module's -// current workspace -func (h *Helpers) TaskWorkspace(t *task.Task) (resource.Resource, bool) { - if ws := t.Workspace(); ws != nil { - return ws, true +// TaskWorkspace retrieves the task's workspace if it belongs to one. +func (h *Helpers) TaskWorkspace(t *task.Task) *workspace.Workspace { + workspaceID := t.WorkspaceID + if workspaceID == nil { + return nil } - if mod := h.TaskModule(t); mod != nil { - if ws := h.ModuleCurrentWorkspace(mod); ws != nil { - return ws, true - } - return nil, false + ws, err := h.Workspaces.Get(*workspaceID) + if err != nil { + return nil } - return nil, false + return ws } func (h *Helpers) TaskWorkspaceName(t *task.Task) string { if ws := h.TaskWorkspace(t); ws != nil { - return ws.Path + return ws.Name } return "" } +// TaskWorkspaceOrCurrentWorkspace retrieves either the task's workspace if it belongs to a +// workspace, or if it belongs to a module, then it retrieves the module's +// current workspace +func (h *Helpers) TaskWorkspaceOrCurrentWorkspace(t *task.Task) *workspace.Workspace { + if ws := h.TaskWorkspace(t); ws != nil { + return ws + } + if mod := h.TaskModule(t); mod != nil { + if ws := h.ModuleCurrentWorkspace(mod); ws != nil { + return ws + } + return nil + } + return nil +} + // TaskStatus provides a rendered colored task status. func (h *Helpers) TaskStatus(t *task.Task, background bool) string { var color lipgloss.Color @@ -387,14 +381,24 @@ func (h *Helpers) Breadcrumbs(title string, res resource.Resource, crumbs ...str return h.Breadcrumbs(title, resource.GlobalResource, cmd) case *state.Resource: addr := TitleAddress.Render(res.String()) - return h.Breadcrumbs(title, res.GetParent().GetParent(), addr) + ws, err := h.Workspaces.Get(res.WorkspaceID) + if err != nil { + h.Logger.Error("rendering breadcrumbs", "error", err) + return "" + } + return h.Breadcrumbs(title, ws, addr) case *task.Group: cmd := TitleCommand.Render(res.String()) id := TitleID.Render(res.GetID().String()) - return h.Breadcrumbs(title, res.GetParent(), cmd, id) + return h.Breadcrumbs(title, resource.GlobalResource, cmd, id) case *workspace.Workspace: name := TitleWorkspace.Render(res.String()) - return h.Breadcrumbs(title, res.GetParent(), append(crumbs, name)...) + mod, err := h.Modules.Get(res.ModuleID) + if err != nil { + h.Logger.Error("rendering breadcrumbs", "error", err) + return "" + } + return h.Breadcrumbs(title, mod, append(crumbs, name)...) case *module.Module: crumbs = append(crumbs, TitlePath.Render(res.String())) } diff --git a/internal/tui/module/list.go b/internal/tui/module/list.go index c615514..c8b53cf 100644 --- a/internal/tui/module/list.go +++ b/internal/tui/module/list.go @@ -127,8 +127,13 @@ func (m list) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case resource.Event[*task.Task]: // Re-render module whenever a task event is received belonging to the // module. - if mod := msg.Payload.Module(); mod != nil { - m.table.AddItems(mod.(*module.Module)) + if moduleID := msg.Payload.ModuleID; moduleID != nil { + mod, err := m.Modules.Get(*moduleID) + if err != nil { + m.Logger.Error("re-rendering module upon receiving task event", "error", err, "task", msg.Payload) + return m, nil + } + m.table.AddItems(mod) } case tea.KeyMsg: switch { diff --git a/internal/tui/task/list.go b/internal/tui/task/list.go index aabd95b..62e639b 100644 --- a/internal/tui/task/list.go +++ b/internal/tui/task/list.go @@ -82,7 +82,7 @@ func (mm *ListMaker) Make(_ resource.ID, width, height int) (tea.Model, error) { return table.RenderedRow{ taskIDColumn.Key: t.ID.String(), table.ModuleColumn.Key: mm.Helpers.TaskModulePath(t), - table.WorkspaceColumn.Key: mm.Helpers.WorkspaceName(t), + table.WorkspaceColumn.Key: mm.Helpers.TaskWorkspaceName(t), commandColumn.Key: t.String(), ageColumn.Key: tui.Ago(time.Now(), t.Updated), statusColumn.Key: mm.Helpers.TaskStatus(t, false), @@ -147,7 +147,7 @@ func (m List) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ) case key.Matches(msg, keys.Common.State): if row, ok := m.Table.CurrentRow(); ok { - if ws, ok := m.TaskWorkspace(row.Value); ok { + if ws := m.TaskWorkspaceOrCurrentWorkspace(row.Value); ws != nil { return m, tui.NavigateTo(tui.ResourceListKind, tui.WithParent(ws.GetID())) } else { return m, tui.ReportError(errors.New("task not associated with a workspace")) diff --git a/internal/tui/task/model.go b/internal/tui/task/model.go index 9c4a256..84419d5 100644 --- a/internal/tui/task/model.go +++ b/internal/tui/task/model.go @@ -142,7 +142,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.CreateTasksWithSpecs(spec), ) case key.Matches(msg, keys.Common.State): - if ws, ok := m.TaskWorkspace(m.task); ok { + if ws := m.TaskWorkspaceOrCurrentWorkspace(m.task); ws != nil { return m, tui.NavigateTo(tui.ResourceListKind, tui.WithParent(ws.GetID())) } else { return m, tui.ReportError(errors.New("task not associated with a workspace")) @@ -306,10 +306,10 @@ func (m model) HelpBindings() []key.Binding { keys.Common.Retry, localKeys.ToggleInfo, } - if mod := m.task.Module(); mod != nil { + if moduleID := m.task.ModuleID; moduleID != nil { bindings = append(bindings, keys.Common.Module) } - if ws := m.task.Workspace(); ws != nil { + if workspaceID := m.task.WorkspaceID; workspaceID != nil { bindings = append(bindings, keys.Common.Workspace) } if plan.IsApplyTask(m.task) { diff --git a/internal/tui/workspace/list.go b/internal/tui/workspace/list.go index 38ed669..f3d31bf 100644 --- a/internal/tui/workspace/list.go +++ b/internal/tui/workspace/list.go @@ -97,8 +97,13 @@ func (m list) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case resource.Event[*task.Task]: // Re-render workspace whenever a task event is received belonging to the // workspace. - if ws := msg.Payload.Workspace(); ws != nil { - m.table.AddItems(ws.(*workspace.Workspace)) + if workspaceID := msg.Payload.WorkspaceID; workspaceID != nil { + ws, err := m.Workspaces.Get(*workspaceID) + if err != nil { + m.Logger.Error("re-rendering workspace upon receiving task event", "error", err, "task", msg.Payload) + return m, nil + } + m.table.AddItems(ws) } case tea.KeyMsg: switch { diff --git a/internal/tui/workspace/resource.go b/internal/tui/workspace/resource.go index cf1d554..d958bb5 100644 --- a/internal/tui/workspace/resource.go +++ b/internal/tui/workspace/resource.go @@ -78,21 +78,21 @@ func (m resourceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { fn := func(workspaceID resource.ID) (task.Spec, error) { return m.states.Taint(workspaceID, m.resource.Address) } - return m, m.CreateTasks(fn, m.resource.Workspace().GetID()) + return m, m.CreateTasks(fn, m.resource.WorkspaceID) case key.Matches(msg, resourcesKeys.Untaint): fn := func(workspaceID resource.ID) (task.Spec, error) { return m.states.Untaint(workspaceID, m.resource.Address) } - return m, m.CreateTasks(fn, m.resource.Workspace().GetID()) + return m, m.CreateTasks(fn, m.resource.WorkspaceID) case key.Matches(msg, resourcesKeys.Move): - return m, m.Move(m.resource.Workspace().GetID(), m.resource.Address) + return m, m.Move(m.resource.WorkspaceID, m.resource.Address) case key.Matches(msg, keys.Common.Delete): fn := func(workspaceID resource.ID) (task.Spec, error) { return m.states.Delete(workspaceID, m.resource.Address) } return m, tui.YesNoPrompt( "Delete resource?", - m.CreateTasks(fn, m.resource.Workspace().GetID()), + m.CreateTasks(fn, m.resource.WorkspaceID), ) case key.Matches(msg, keys.Common.PlanDestroy): // Create a targeted destroy plan. @@ -104,7 +104,7 @@ func (m resourceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { fn := func(workspaceID resource.ID) (task.Spec, error) { return m.plans.Plan(workspaceID, createRunOptions) } - return m, m.CreateTasks(fn, m.resource.Workspace().GetID()) + return m, m.CreateTasks(fn, m.resource.WorkspaceID) } case tea.WindowSizeMsg: m.viewport.SetDimensions(m.viewportWidth(msg.Width), m.viewportHeight(msg.Height)) diff --git a/internal/workspace/service.go b/internal/workspace/service.go index 3ed687d..2a70937 100644 --- a/internal/workspace/service.go +++ b/internal/workspace/service.go @@ -93,7 +93,11 @@ func (s *Service) LoadWorkspacesUponInit(sub <-chan resource.Event[*task.Task]) if event.Payload.State != task.Exited { continue } - mod, err := s.modules.Get(event.Payload.Module().GetID()) + moduleID := event.Payload.ModuleID + if moduleID == nil { + continue + } + mod, err := s.modules.Get(*moduleID) if err != nil { continue }